diff --git a/formulus/__tests__/App.test.tsx b/formulus/__tests__/App.test.tsx
index 9b31e9e4f..658b85e3d 100644
--- a/formulus/__tests__/App.test.tsx
+++ b/formulus/__tests__/App.test.tsx
@@ -4,11 +4,11 @@
import React from 'react';
import {render} from '@testing-library/react-native';
+import {jest, describe, test, expect} from '@jest/globals';
// Mock the App component instead of trying to render the real one
// This avoids issues with native modules and database initialization
jest.mock('../App', () => {
- const React = require('react');
const {View} = require('react-native');
return function MockedApp() {
return ;
diff --git a/formulus/assets/images/logo.png b/formulus/assets/images/logo.png
index a5994bc28..3d1c74bd7 100644
Binary files a/formulus/assets/images/logo.png and b/formulus/assets/images/logo.png differ
diff --git a/formulus/package-lock.json b/formulus/package-lock.json
index 8c83429f2..0aed3e734 100644
--- a/formulus/package-lock.json
+++ b/formulus/package-lock.json
@@ -54,6 +54,7 @@
"@types/react": "^19.0.0",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^19.0.0",
+ "baseline-browser-mapping": "^2.9.11",
"eslint": "^8.19.0",
"eslint-config-prettier": "8.10.2",
"jest": "^29.7.0",
@@ -2334,109 +2335,6 @@
"node": "20 || >=22"
}
},
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
- "version": "9.2.2",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
- "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@isaacs/cliui/node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
"node_modules/@isaacs/ttlcache": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz",
@@ -2485,9 +2383,9 @@
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -3030,15 +2928,15 @@
}
},
"node_modules/@nestjs/common": {
- "version": "11.1.6",
- "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.6.tgz",
- "integrity": "sha512-krKwLLcFmeuKDqngG2N/RuZHCs2ycsKcxWIDgcm7i1lf3sQ0iG03ci+DsP/r3FcT/eJDFsIHnKtNta2LIi7PzQ==",
+ "version": "11.1.11",
+ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.11.tgz",
+ "integrity": "sha512-R/+A8XFqLgN8zNs2twhrOaE7dJbRQhdPX3g46am4RT/x8xGLqDphrXkUIno4cGUZHxbczChBAaAPTdPv73wDZA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "file-type": "21.0.0",
+ "file-type": "21.2.0",
"iterare": "1.2.1",
- "load-esm": "1.0.2",
+ "load-esm": "1.0.3",
"tslib": "2.8.1",
"uid": "2.0.2"
},
@@ -3062,9 +2960,9 @@
}
},
"node_modules/@nestjs/core": {
- "version": "11.1.6",
- "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz",
- "integrity": "sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==",
+ "version": "11.1.11",
+ "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.11.tgz",
+ "integrity": "sha512-H9i+zT3RvHi7tDc+lCmWHJ3ustXveABCr+Vcpl96dNOxgmrx4elQSTC4W93Mlav2opfLV+p0UTHY6L+bpUA4zA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -3072,7 +2970,7 @@
"@nuxt/opencollective": "0.4.1",
"fast-safe-stringify": "2.1.1",
"iterare": "1.2.1",
- "path-to-regexp": "8.2.0",
+ "path-to-regexp": "8.3.0",
"tslib": "2.8.1",
"uid": "2.0.2"
},
@@ -3251,25 +3149,25 @@
"license": "MIT"
},
"node_modules/@openapitools/openapi-generator-cli": {
- "version": "2.23.1",
- "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.23.1.tgz",
- "integrity": "sha512-Kd5EZqzbcIXf6KRlpUrheHMzQNRHsJWzAGrm4ncWCNhnQl+Mh6TsFcqq+hIetgiFCknWBH6cZ2f37SxPxaon4w==",
+ "version": "2.27.0",
+ "resolved": "https://registry.npmjs.org/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.27.0.tgz",
+ "integrity": "sha512-JJWO9joe6TudGdHuwBi+NhEMIwXcz1B2FawBsJ5SjgTMQxSjArkChq+ro6Ovv6q1zfWavCs/q9uYeNPtRRcQBA==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@nestjs/axios": "4.0.1",
- "@nestjs/common": "11.1.6",
- "@nestjs/core": "11.1.6",
+ "@nestjs/common": "11.1.11",
+ "@nestjs/core": "11.1.11",
"@nuxtjs/opencollective": "0.3.2",
- "axios": "1.11.0",
+ "axios": "1.13.2",
"chalk": "4.1.2",
"commander": "8.3.0",
- "compare-versions": "4.1.4",
+ "compare-versions": "6.1.1",
"concurrently": "9.2.1",
"console.table": "0.10.0",
- "fs-extra": "11.3.1",
- "glob": "11.0.3",
+ "fs-extra": "11.3.3",
+ "glob": "13.0.0",
"inquirer": "8.2.7",
"proxy-agent": "6.5.0",
"reflect-metadata": "0.2.2",
@@ -4199,15 +4097,14 @@
"license": "MIT"
},
"node_modules/@tokenizer/inflate": {
- "version": "0.2.7",
- "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
- "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
+ "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "debug": "^4.4.0",
- "fflate": "^0.8.2",
- "token-types": "^6.0.0"
+ "debug": "^4.4.3",
+ "token-types": "^6.1.1"
},
"engines": {
"node": ">=18"
@@ -5164,9 +5061,9 @@
}
},
"node_modules/axios": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
- "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5378,9 +5275,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.3",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.3.tgz",
- "integrity": "sha512-mcE+Wr2CAhHNWxXN/DdTI+n4gsPc5QpXpWnyCQWiQYIYZX+ZMJ8juXZgjRa/0/YPJo/NSsgW15/YgmI4nbysYw==",
+ "version": "2.9.11",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
+ "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -5409,24 +5306,24 @@
}
},
"node_modules/body-parser": {
- "version": "1.20.3",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
- "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "bytes": "3.1.2",
+ "bytes": "~3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
- "destroy": "1.2.0",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "on-finished": "2.4.1",
- "qs": "6.13.0",
- "raw-body": "2.5.2",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
"type-is": "~1.6.18",
- "unpipe": "1.0.0"
+ "unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8",
@@ -5443,6 +5340,27 @@
"ms": "2.0.0"
}
},
+ "node_modules/body-parser/node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/body-parser/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -5450,6 +5368,16 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/body-parser/node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@@ -5961,9 +5889,9 @@
}
},
"node_modules/compare-versions": {
- "version": "4.1.4",
- "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.4.tgz",
- "integrity": "sha512-FemMreK9xNyL8gQevsdRMrvO4lFCkQP7qbuktn1q8ndcNk1+0mz7lgE7b/sNvbhVgY4w6tMN1FDp6aADjqw2rw==",
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz",
+ "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==",
"dev": true,
"license": "MIT"
},
@@ -6527,13 +6455,6 @@
"node": ">= 0.4"
}
},
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/easy-table": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/easy-table/-/easy-table-1.1.0.tgz",
@@ -7674,13 +7595,6 @@
"bser": "2.1.1"
}
},
- "node_modules/fflate": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
- "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@@ -7721,15 +7635,15 @@
}
},
"node_modules/file-type": {
- "version": "21.0.0",
- "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz",
- "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==",
+ "version": "21.2.0",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.2.0.tgz",
+ "integrity": "sha512-vCYBgFOrJQLoTzDyAXAL/RFfKnXXpUYt4+tipVy26nJJhT7ftgGETf2tAQF59EEL61i3MrorV/PG6tf7LJK7eg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@tokenizer/inflate": "^0.2.7",
- "strtok3": "^10.2.2",
- "token-types": "^6.0.0",
+ "@tokenizer/inflate": "^0.4.1",
+ "strtok3": "^10.3.4",
+ "token-types": "^6.1.1",
"uint8array-extras": "^1.4.0"
},
"engines": {
@@ -7887,36 +7801,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/foreground-child": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
- "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "cross-spawn": "^7.0.6",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/foreground-child/node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
@@ -7944,9 +7828,9 @@
}
},
"node_modules/fs-extra": {
- "version": "11.3.1",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz",
- "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==",
+ "version": "11.3.3",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
+ "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8132,22 +8016,16 @@
}
},
"node_modules/glob": {
- "version": "11.0.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
- "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz",
+ "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "foreground-child": "^3.3.1",
- "jackspeak": "^4.1.1",
- "minimatch": "^10.0.3",
+ "minimatch": "^10.1.1",
"minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
- },
"engines": {
"node": "20 || >=22"
},
@@ -8169,11 +8047,11 @@
}
},
"node_modules/glob/node_modules/minimatch": {
- "version": "10.0.3",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
- "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
+ "version": "10.1.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
+ "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
@@ -9277,22 +9155,6 @@
"node": ">= 0.4"
}
},
- "node_modules/jackspeak": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
- "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
@@ -10392,9 +10254,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -10584,9 +10446,9 @@
"license": "MIT"
},
"node_modules/load-esm": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz",
- "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.3.tgz",
+ "integrity": "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==",
"dev": true,
"funding": [
{
@@ -11091,9 +10953,9 @@
}
},
"node_modules/metro-config/node_modules/js-yaml": {
- "version": "3.14.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
- "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
+ "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -11919,13 +11781,6 @@
"node": ">= 14"
}
},
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "dev": true,
- "license": "BlueOak-1.0.0"
- },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -12003,9 +11858,9 @@
"license": "MIT"
},
"node_modules/path-scurry": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
- "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
+ "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -12020,23 +11875,24 @@
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
- "version": "11.2.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
- "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==",
+ "version": "11.2.4",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
+ "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
"dev": true,
- "license": "ISC",
+ "license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/path-to-regexp": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
- "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=16"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/path-type": {
@@ -12335,13 +12191,13 @@
"license": "MIT"
},
"node_modules/qs": {
- "version": "6.13.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
- "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "version": "6.14.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"devOptional": true,
"license": "BSD-3-Clause",
"dependencies": {
- "side-channel": "^1.0.6"
+ "side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
@@ -12408,21 +12264,52 @@
}
},
"node_modules/raw-body": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
- "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
- "bytes": "3.1.2",
- "http-errors": "2.0.0",
- "iconv-lite": "0.4.24",
- "unpipe": "1.0.0"
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
+ "node_modules/raw-body/node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/raw-body/node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
@@ -13988,32 +13875,6 @@
"node": ">=8"
}
},
- "node_modules/string-width-cjs": {
- "name": "string-width",
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/string-width/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@@ -14133,20 +13994,6 @@
"node": ">=8"
}
},
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/strip-bom": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
@@ -15089,25 +14936,6 @@
"node": ">=8"
}
},
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/formulus/package.json b/formulus/package.json
index 82897e503..1d8c8aaad 100644
--- a/formulus/package.json
+++ b/formulus/package.json
@@ -63,9 +63,10 @@
"@types/react": "^19.0.0",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^19.0.0",
+ "baseline-browser-mapping": "^2.9.11",
"eslint": "^8.19.0",
- "jest": "^29.7.0",
"eslint-config-prettier": "8.10.2",
+ "jest": "^29.7.0",
"prettier": "2.8.8",
"react-test-renderer": "19.0.0",
"ts-node": "^10.9.2",
diff --git a/formulus/src/api/synkronus/Auth.ts b/formulus/src/api/synkronus/Auth.ts
index add1da2e7..ea2ab9013 100644
--- a/formulus/src/api/synkronus/Auth.ts
+++ b/formulus/src/api/synkronus/Auth.ts
@@ -8,13 +8,49 @@ export interface UserInfo {
role: UserRole;
}
+const decodeBase64 = (input: string): string => {
+ const atobFn = (globalThis as any).atob as
+ | ((data: string) => string)
+ | undefined;
+ if (typeof atobFn === 'function') {
+ return atobFn(input);
+ }
+
+ const chars =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+ let str = '';
+ let i = 0;
+
+ // Basic base64 decoder fallback
+ while (i < input.length) {
+ const enc1 = chars.indexOf(input.charAt(i++));
+ const enc2 = chars.indexOf(input.charAt(i++));
+ const enc3 = chars.indexOf(input.charAt(i++));
+ const enc4 = chars.indexOf(input.charAt(i++));
+
+ const chr1 = enc1 * 4 + Math.floor(enc2 / 16);
+ const chr2 = (enc2 % 16) * 16 + Math.floor(enc3 / 4);
+ const chr3 = (enc3 % 4) * 64 + enc4;
+
+ str += String.fromCharCode(chr1);
+ if (enc3 !== 64) {
+ str += String.fromCharCode(chr2);
+ }
+ if (enc4 !== 64) {
+ str += String.fromCharCode(chr3);
+ }
+ }
+
+ return str;
+};
+
// Decode JWT payload without verification (claims are in the middle part)
function decodeJwtPayload(token: string): any {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = parts[1];
- const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
+ const decoded = decodeBase64(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded);
} catch {
return null;
@@ -34,10 +70,10 @@ export const login = async (
loginRequest: {username, password},
});
- const {token, refreshToken, expiresAt} = res.data;
+ const {token, refreshToken: refreshTokenValue, expiresAt} = res.data;
await AsyncStorage.setItem('@token', token);
- await AsyncStorage.setItem('@refreshToken', refreshToken);
+ await AsyncStorage.setItem('@refreshToken', refreshTokenValue);
await AsyncStorage.setItem('@tokenExpiresAt', expiresAt.toString());
// Decode JWT to get user info
@@ -100,9 +136,9 @@ export const refreshToken = async () => {
refreshToken: (await AsyncStorage.getItem('@refreshToken')) ?? '',
},
});
- const {token, refreshToken, expiresAt} = res.data;
+ const {token, refreshToken: refreshTokenValue, expiresAt} = res.data;
await AsyncStorage.setItem('@token', token);
- await AsyncStorage.setItem('@refreshToken', refreshToken);
+ await AsyncStorage.setItem('@refreshToken', refreshTokenValue);
await AsyncStorage.setItem('@tokenExpiresAt', expiresAt.toString());
return true;
};
diff --git a/formulus/src/components/CustomAppWebView.tsx b/formulus/src/components/CustomAppWebView.tsx
index ef4775307..a825af986 100644
--- a/formulus/src/components/CustomAppWebView.tsx
+++ b/formulus/src/components/CustomAppWebView.tsx
@@ -6,7 +6,7 @@ import React, {
useImperativeHandle,
useMemo,
} from 'react';
-import {View, ActivityIndicator, AppState} from 'react-native';
+import {View, ActivityIndicator, AppState, StyleSheet} from 'react-native';
import {WebView} from 'react-native-webview';
import {useIsFocused} from '@react-navigation/native';
import {Platform} from 'react-native';
@@ -249,7 +249,7 @@ const CustomAppWebView = forwardRef<
if (!isScriptReady) {
return (
-
+
);
@@ -308,7 +308,7 @@ const CustomAppWebView = forwardRef<
startInLoadingState={true}
originWhitelist={['*']}
renderLoading={() => (
-
+
)}
@@ -316,4 +316,12 @@ const CustomAppWebView = forwardRef<
);
});
+const styles = StyleSheet.create({
+ centered: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
export default CustomAppWebView;
diff --git a/formulus/src/components/FormplayerModal.tsx b/formulus/src/components/FormplayerModal.tsx
index e6a7e37f3..e0f93ed7f 100644
--- a/formulus/src/components/FormplayerModal.tsx
+++ b/formulus/src/components/FormplayerModal.tsx
@@ -405,7 +405,7 @@ const FormplayerModal = forwardRef(
{currentObservationId ? 'Edit Observation' : 'New Observation'}
-
+
= ({
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
/[xy]/g,
function (c) {
- const r = (Math.random() * 16) | 0;
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
+ const r = Math.floor(Math.random() * 16);
+ const v = c === 'x' ? r : (r % 4) + 8;
return v.toString(16);
},
);
diff --git a/formulus/src/database/repositories/__tests__/WatermelonDBRepo.test.ts b/formulus/src/database/repositories/__tests__/WatermelonDBRepo.test.ts
index d83d9d098..6b1e2d38a 100644
--- a/formulus/src/database/repositories/__tests__/WatermelonDBRepo.test.ts
+++ b/formulus/src/database/repositories/__tests__/WatermelonDBRepo.test.ts
@@ -507,88 +507,5 @@ describe('WatermelonDBRepo', () => {
}
});
- // Test the synchronize method
- test.skip('synchronize should pull and push observations correctly', async () => {
- // Arrange - create some local observations that need syncing
- const localObservation1: Partial = {
- formType: 'sync-test-local-1',
- data: {source: 'local', value: 1},
- };
-
- const localObservation2: Partial = {
- formType: 'sync-test-local-2',
- data: {source: 'local', value: 2},
- };
-
- // Save the local observations
- const localId1 = await repo.saveObservation(localObservation1);
- const localId2 = await repo.saveObservation(localObservation2);
-
- // Verify local observations were saved
- const collection = database.get('observations');
- const initialCount = await collection.query().fetchCount();
- expect(initialCount).toBe(2);
-
- // Create mock server data to be pulled
- const serverObservations = [
- {
- observationId: 'server-obs-1',
- formType: 'sync-test-server-1',
- formVersion: '1.0',
- data: {source: 'server', value: 1},
- deleted: false,
- },
- {
- observationId: 'server-obs-2',
- formType: 'sync-test-server-2',
- formVersion: '1.0',
- data: {source: 'server', value: 2},
- deleted: false,
- },
- ];
-
- // Create mock functions for pull and push
- const mockPullChanges = jest.fn().mockResolvedValue(serverObservations);
-
- const pushedObservations: Observation[] = [];
- const mockPushChanges = jest
- .fn()
- .mockImplementation((observations: Observation[]) => {
- pushedObservations.push(...observations);
- return Promise.resolve();
- });
-
- // Act - perform synchronization
- await repo.synchronize(mockPullChanges, mockPushChanges);
-
- // Assert - verify pull was called
- expect(mockPullChanges).toHaveBeenCalledTimes(1);
-
- // Verify server observations were saved locally
- const afterSyncCount = await collection.query().fetchCount();
- expect(afterSyncCount).toBe(4); // 2 local + 2 server
-
- // Verify we can retrieve the server observations
- const serverObs1 = await repo.getObservation('server-obs-1');
- const serverObs2 = await repo.getObservation('server-obs-2');
-
- expect(serverObs1).not.toBeNull();
- expect(serverObs2).not.toBeNull();
-
- if (serverObs1 && serverObs2) {
- expect(serverObs1.formType).toBe('sync-test-server-1');
- expect(serverObs2.formType).toBe('sync-test-server-2');
- }
-
- // Verify push was called with local observations
- expect(mockPushChanges).toHaveBeenCalledTimes(1);
- expect(pushedObservations.length).toBe(2);
-
- // Verify local observations were marked as synced
- const syncedObs1 = await repo.getObservation(localId1);
- const syncedObs2 = await repo.getObservation(localId2);
-
- expect(syncedObs1?.syncedAt).toBeTruthy();
- expect(syncedObs2?.syncedAt).toBeTruthy();
- });
+ test.todo('synchronize should pull and push observations correctly');
});
diff --git a/formulus/src/navigation/MainAppNavigator.tsx b/formulus/src/navigation/MainAppNavigator.tsx
index f5994d07d..06260bd9e 100644
--- a/formulus/src/navigation/MainAppNavigator.tsx
+++ b/formulus/src/navigation/MainAppNavigator.tsx
@@ -3,7 +3,6 @@ import {createStackNavigator} from '@react-navigation/stack';
import {useFocusEffect} from '@react-navigation/native';
import MainTabNavigator from './MainTabNavigator';
import WelcomeScreen from '../screens/WelcomeScreen';
-import SettingsScreen from '../screens/SettingsScreen';
import FormManagementScreen from '../screens/FormManagementScreen';
import ObservationDetailScreen from '../screens/ObservationDetailScreen';
import {MainAppStackParamList} from '../types/NavigationTypes';
@@ -51,11 +50,6 @@ const MainAppNavigator: React.FC = () => {
component={MainTabNavigator}
options={{headerShown: false}}
/>
-
();
+
+type TabBarIconProps = {
+ color: string;
+ size: number;
+};
+
+const renderHomeIcon = ({color, size}: TabBarIconProps) => (
+
+);
+
+const renderFormsIcon = ({color, size}: TabBarIconProps) => (
+
+);
+
+const renderObservationsIcon = ({color, size}: TabBarIconProps) => (
+
+);
+
+const renderSyncIcon = ({color, size}: TabBarIconProps) => (
+
+);
+
+const renderMoreIcon = ({color, size}: TabBarIconProps) => (
+
+);
const MainTabNavigator: React.FC = () => {
const insets = useSafeAreaInsets();
@@ -34,47 +63,58 @@ const MainTabNavigator: React.FC = () => {
name="Home"
component={HomeScreen}
options={{
- tabBarIcon: ({color, size}) => (
-
- ),
+ tabBarIcon: renderHomeIcon,
}}
/>
(
-
- ),
+ tabBarIcon: renderFormsIcon,
}}
/>
(
-
- ),
+ tabBarIcon: renderObservationsIcon,
}}
/>
(
-
- ),
+ tabBarIcon: renderSyncIcon,
+ }}
+ />
+ null,
+ }}
+ />
+ null,
+ }}
+ />
+ null,
}}
/>
(
-
- ),
+ tabBarIcon: renderMoreIcon,
}}
- listeners={({navigation, _route}) => ({
+ listeners={({navigation}) => ({
tabPress: _e => {
const state = navigation.getState();
const currentRoute = state.routes[state.index];
diff --git a/formulus/src/screens/AboutScreen.tsx b/formulus/src/screens/AboutScreen.tsx
new file mode 100644
index 000000000..21ddc16e1
--- /dev/null
+++ b/formulus/src/screens/AboutScreen.tsx
@@ -0,0 +1,155 @@
+import React, {useEffect, useState} from 'react';
+import {View, Text, StyleSheet, Image, ScrollView, Linking} from 'react-native';
+import {SafeAreaView} from 'react-native-safe-area-context';
+import colors from '../theme/colors';
+import {appVersionService} from '../services/AppVersionService';
+
+const AboutScreen: React.FC = () => {
+ const [version, setVersion] = useState('');
+
+ useEffect(() => {
+ const loadVersion = async () => {
+ try {
+ const v = await appVersionService.getFullVersion();
+ setVersion(v);
+ } catch (_e) {
+ setVersion('');
+ }
+ };
+
+ loadVersion();
+ }, []);
+
+ return (
+
+
+ About
+ Information about this app
+
+
+
+
+
+
+ ODE
+ {!!version && v{version}}
+
+
+
+
+ Formulus
+
+ Formulus is the mobile app for collecting and synchronizing forms
+ and observations.
+
+
+
+
+ Support
+
+ If you need help, contact your system administrator.
+
+
+
+
+ Free & Open Source
+
+ This application is free and open source software. We would love to
+ hear your feedback and welcome contributions.
+
+
+ Linking.openURL('https://forum.opendataensemble.org')
+ }
+ suppressHighlighting={true}>
+ https://forum.opendataensemble.org
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.neutral[50],
+ },
+ header: {
+ padding: 16,
+ backgroundColor: colors.neutral.white,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.neutral[200],
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ color: colors.neutral[900],
+ marginBottom: 4,
+ },
+ subtitle: {
+ fontSize: 14,
+ color: colors.neutral[600],
+ },
+ content: {
+ padding: 16,
+ paddingBottom: 32,
+ },
+ brandRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 12,
+ marginBottom: 16,
+ },
+ logo: {
+ width: 56,
+ height: 56,
+ },
+ brandText: {
+ flex: 1,
+ },
+ appName: {
+ fontSize: 20,
+ fontWeight: '700',
+ color: colors.neutral[900],
+ },
+ version: {
+ marginTop: 2,
+ fontSize: 12,
+ color: colors.neutral[600],
+ },
+ card: {
+ backgroundColor: colors.neutral.white,
+ borderRadius: 12,
+ padding: 16,
+ marginBottom: 12,
+ shadowColor: colors.neutral.black,
+ shadowOffset: {width: 0, height: 2},
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ cardTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: colors.neutral[900],
+ marginBottom: 8,
+ },
+ cardText: {
+ fontSize: 14,
+ color: colors.neutral[600],
+ lineHeight: 20,
+ },
+ link: {
+ color: colors.brand.primary[500],
+ marginTop: 12,
+ fontWeight: '600',
+ },
+});
+
+export default AboutScreen;
diff --git a/formulus/src/screens/FormsScreen.tsx b/formulus/src/screens/FormsScreen.tsx
index 44a6c6038..4d3988f39 100644
--- a/formulus/src/screens/FormsScreen.tsx
+++ b/formulus/src/screens/FormsScreen.tsx
@@ -43,8 +43,8 @@ const FormsScreen: React.FC = () => {
) {
await refresh();
}
- } catch (error) {
- console.error('Error opening form:', error);
+ } catch (err) {
+ console.error('Error opening form:', err);
Alert.alert('Error', 'Failed to open form. Please try again.');
}
};
diff --git a/formulus/src/screens/HelpScreen.tsx b/formulus/src/screens/HelpScreen.tsx
new file mode 100644
index 000000000..6eff5e54d
--- /dev/null
+++ b/formulus/src/screens/HelpScreen.tsx
@@ -0,0 +1,112 @@
+import React from 'react';
+import {View, Text, StyleSheet, ScrollView, Linking} from 'react-native';
+import {SafeAreaView} from 'react-native-safe-area-context';
+import colors from '../theme/colors';
+
+const HelpScreen: React.FC = () => {
+ return (
+
+
+ Help & Support
+ Get help and share feedback
+
+
+
+
+ Community Forum
+
+ We would love to hear your feedback and welcome contributions.
+
+
+ Linking.openURL('https://forum.opendataensemble.org')
+ }
+ suppressHighlighting={true}>
+ https://forum.opendataensemble.org
+
+
+
+
+ Troubleshooting
+
+ If something is not working as expected:
+
+
+ 1. Check your internet connection.
+
+
+ 2. Try syncing again from the Sync tab.
+
+
+ 3. If the issue persists, reach out via the forum.
+
+
+
+
+ Administrator
+
+ For account setup, server configuration, or access issues, contact
+ your system administrator.
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.neutral[50],
+ },
+ header: {
+ padding: 16,
+ backgroundColor: colors.neutral.white,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.neutral[200],
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: 'bold',
+ color: colors.neutral[900],
+ marginBottom: 4,
+ },
+ subtitle: {
+ fontSize: 14,
+ color: colors.neutral[600],
+ },
+ content: {
+ padding: 16,
+ paddingBottom: 32,
+ },
+ card: {
+ backgroundColor: colors.neutral.white,
+ borderRadius: 12,
+ padding: 16,
+ marginBottom: 12,
+ shadowColor: colors.neutral.black,
+ shadowOffset: {width: 0, height: 2},
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 3,
+ },
+ cardTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: colors.neutral[900],
+ marginBottom: 8,
+ },
+ cardText: {
+ fontSize: 14,
+ color: colors.neutral[600],
+ lineHeight: 20,
+ },
+ link: {
+ color: colors.brand.primary[500],
+ marginTop: 12,
+ fontWeight: '600',
+ },
+});
+
+export default HelpScreen;
diff --git a/formulus/src/screens/MoreScreen.tsx b/formulus/src/screens/MoreScreen.tsx
index 9e722ec6d..55954e5a0 100644
--- a/formulus/src/screens/MoreScreen.tsx
+++ b/formulus/src/screens/MoreScreen.tsx
@@ -16,6 +16,14 @@ import {
import MenuDrawer from '../components/MenuDrawer';
import {logout} from '../api/synkronus/Auth';
+type MainAppDrawerScreen = 'FormManagement';
+
+const isMainAppDrawerScreen = (
+ screen: string,
+): screen is MainAppDrawerScreen => {
+ return screen === 'FormManagement';
+};
+
type MoreScreenNavigationProp = CompositeNavigationProp<
BottomTabNavigationProp,
StackNavigationProp
@@ -42,8 +50,14 @@ const MoreScreen: React.FC = () => {
const handleNavigate = (screen: string) => {
setDrawerVisible(false);
// Navigate to screens in the MainAppStack
- if (screen === 'Settings' || screen === 'FormManagement') {
- navigation.navigate(screen as keyof MainAppStackParamList);
+ if (isMainAppDrawerScreen(screen)) {
+ navigation.navigate(screen);
+ } else if (screen === 'Settings') {
+ navigation.navigate('Settings');
+ } else if (screen === 'About') {
+ navigation.navigate('About');
+ } else if (screen === 'Help') {
+ navigation.navigate('Help');
} else {
// Other screens not yet implemented - stay on Home for now
console.log('Navigate to:', screen, '(not yet implemented)');
diff --git a/formulus/src/screens/ObservationsScreen.tsx b/formulus/src/screens/ObservationsScreen.tsx
index ae0fbafaf..67a9eb453 100644
--- a/formulus/src/screens/ObservationsScreen.tsx
+++ b/formulus/src/screens/ObservationsScreen.tsx
@@ -124,8 +124,8 @@ const ObservationsScreen: React.FC = () => {
) {
await refresh();
}
- } catch (error) {
- console.error('Error editing observation:', error);
+ } catch (err) {
+ console.error('Error editing observation:', err);
Alert.alert('Error', 'Failed to edit observation. Please try again.');
}
};
@@ -144,8 +144,8 @@ const ObservationsScreen: React.FC = () => {
const formService = await FormService.getInstance();
await formService.deleteObservation(observation.observationId);
await refresh();
- } catch (error) {
- console.error('Error deleting observation:', error);
+ } catch (err) {
+ console.error('Error deleting observation:', err);
Alert.alert('Error', 'Failed to delete observation.');
}
},
diff --git a/formulus/src/screens/SettingsScreen.tsx b/formulus/src/screens/SettingsScreen.tsx
index eff272af1..1b6647e75 100644
--- a/formulus/src/screens/SettingsScreen.tsx
+++ b/formulus/src/screens/SettingsScreen.tsx
@@ -9,24 +9,25 @@ import {
Image,
ScrollView,
Alert,
+ type AlertButton,
} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {useNavigation} from '@react-navigation/native';
-import {StackNavigationProp} from '@react-navigation/stack';
+import {BottomTabNavigationProp} from '@react-navigation/bottom-tabs';
import * as Keychain from 'react-native-keychain';
import {login, getUserInfo, UserInfo} from '../api/synkronus/Auth';
import {serverConfigService} from '../services/ServerConfigService';
import QRScannerModal from '../components/QRScannerModal';
import {QRSettingsService} from '../services/QRSettingsService';
-import {MainAppStackParamList} from '../types/NavigationTypes';
+import {MainTabParamList} from '../types/NavigationTypes';
import {colors} from '../theme/colors';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import {ToastService} from '../services/ToastService';
import {serverSwitchService} from '../services/ServerSwitchService';
import {syncService} from '../services/SyncService';
-type SettingsScreenNavigationProp = StackNavigationProp<
- MainAppStackParamList,
+type SettingsScreenNavigationProp = BottomTabNavigationProp<
+ MainTabParamList,
'Settings'
>;
@@ -92,7 +93,7 @@ const SettingsScreen = () => {
? `Unsynced observations: ${pendingObservations}\nUnsynced attachments: ${pendingAttachments}\n\nSync is recommended before switching.`
: 'Switching servers will wipe all local data for the previous server.';
- const buttons = hasPending
+ const buttons: AlertButton[] = hasPending
? [
{
text: 'Cancel',
@@ -213,7 +214,7 @@ const SettingsScreen = () => {
const userInfo = await login(username, password);
setLoggedInUser(userInfo);
ToastService.showShort('Successfully logged in!');
- navigation.navigate('MainApp');
+ navigation.navigate('Home');
} catch (error: any) {
console.error('Login failed:', error);
const errorMessage =
@@ -257,7 +258,7 @@ const SettingsScreen = () => {
const userInfo = await login(settings.username, settings.password);
setLoggedInUser(userInfo);
ToastService.showShort('Successfully logged in!');
- navigation.navigate('MainApp');
+ navigation.navigate('Home');
} catch (error: any) {
console.error('Auto-login failed:', error);
const errorMessage =
diff --git a/formulus/src/types/NavigationTypes.ts b/formulus/src/types/NavigationTypes.ts
index edf991768..56a2a98f9 100644
--- a/formulus/src/types/NavigationTypes.ts
+++ b/formulus/src/types/NavigationTypes.ts
@@ -3,13 +3,15 @@ export type MainTabParamList = {
Forms: undefined;
Observations: undefined;
Sync: undefined;
+ Settings: undefined;
+ About: undefined;
+ Help: undefined;
More: {openDrawer?: number} | undefined;
};
export type MainAppStackParamList = {
Welcome: undefined;
MainApp: undefined;
- Settings: undefined;
FormManagement: undefined;
ObservationDetail: {observationId: string};
};
diff --git a/formulus/src/webview/FormulusMessageHandlers.ts b/formulus/src/webview/FormulusMessageHandlers.ts
index fd54b9471..b56958f15 100644
--- a/formulus/src/webview/FormulusMessageHandlers.ts
+++ b/formulus/src/webview/FormulusMessageHandlers.ts
@@ -396,8 +396,8 @@ export function createFormulusMessageHandlers(): FormulusMessageHandlers {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
/[xy]/g,
function (c) {
- const r = (Math.random() * 16) | 0;
- const v = c == 'x' ? r : (r & 0x3) | 0x8;
+ const r = Math.floor(Math.random() * 16);
+ const v = c === 'x' ? r : (r % 4) + 8;
return v.toString(16);
},
);
@@ -416,8 +416,6 @@ export function createFormulusMessageHandlers(): FormulusMessageHandlers {
},
);
- // Use RNFS to copy the camera image to both attachment locations
- const RNFS = require('react-native-fs');
const attachmentsDirectory = `${RNFS.DocumentDirectoryPath}/attachments`;
const pendingUploadDirectory = `${RNFS.DocumentDirectoryPath}/attachments/pending_upload`;
@@ -580,8 +578,6 @@ export function createFormulusMessageHandlers(): FormulusMessageHandlers {
result.data &&
result.data.base64
) {
- const RNFS = require('react-native-fs');
-
// Generate a unique filename
const timestamp = Date.now();
const filename = `signature_${timestamp}.png`;
@@ -900,7 +896,6 @@ export function createFormulusMessageHandlers(): FormulusMessageHandlers {
console.log('Audio recording completed:', result);
// Get file stats for metadata
- const RNFS = require('react-native-fs');
const fileStats = await RNFS.stat(audioPath);
// Create AudioResult object matching our interface