diff --git a/.gitignore b/.gitignore index b68af9de6b..f2d10e869a 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,7 @@ tsconfig.tsbuildinfo ingest-attachment-*.zip tsdoc-metadata.json pods/front/dist +mobile/www *.cpuprofile *.pyc metrics.txt diff --git a/README.md b/README.md index 16a2c23c89..4e93f1e139 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ For detailed information about the platform architecture, services, and their in - [Installation](#installation) - [Build and run](#build-and-run) - [Run in development mode](#run-in-development-mode) + - [Mobile (iOS and Android)](#mobile-ios-and-android) - [Update project structure and database](#update-project-structure-and-database) - [Troubleshooting](#troubleshooting) - [Build \& Watch](#build--watch) @@ -234,6 +235,32 @@ Then go to Select "Sign up" on the right panel and click the "Sign up with password" link at the bottom. Enter the new user's credentials, then proceed to create a workspace for them. +## Mobile (iOS and Android) + +The Platform includes a Capacitor-based mobile app that wraps the same web frontend. Prerequisites: Node.js, Rush, and for native builds **Xcode** (macOS) and/or **Android Studio**. + +1. Build the web app once (from the repo root): + + ```bash + rush build --to @hcengineering/prod + ``` + +2. Copy web assets into the mobile project and sync native projects: + + ```bash + cd mobile + npm run sync + ``` + +3. Open and run the app: + + ```bash + npm run open:ios # Opens Xcode (macOS only) + npm run open:android # Opens Android Studio + ``` + +For development, you can point the app at the webpack dev server by keeping `server.url` in `mobile/capacitor.config.ts` (e.g. `http://localhost:8080`). For production builds, remove or override the `server` setting so the app loads bundled content from `www/`. See [mobile/README.md](mobile/README.md) for details. + ## Update project structure and database If the project's structure is updated, it may be necessary to relink and rebuild the projects. diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index e71bb67660..4683869576 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -5617,6 +5617,21 @@ importers: specifier: ^29.1.1 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@swc/core@1.15.1)(@types/node@22.19.0)(typescript@5.9.3)))(typescript@5.9.3) + ../../mobile: + devDependencies: + '@capacitor/android': + specifier: ^7.0.0 + version: 7.4.5(@capacitor/core@7.4.5) + '@capacitor/cli': + specifier: ^7.0.0 + version: 7.4.5 + '@capacitor/core': + specifier: ^7.0.0 + version: 7.4.5 + '@capacitor/ios': + specifier: ^7.0.0 + version: 7.4.5(@capacitor/core@7.4.5) + ../../models/achievement: dependencies: '@hcengineering/achievement': @@ -42141,6 +42156,24 @@ packages: '@bufbuild/protobuf@1.10.1': resolution: {integrity: sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==} + '@capacitor/android@7.4.5': + resolution: {integrity: sha512-G76C+PGIFkG+hxj6JLlY003txRaGZwsXBPo4b4xiZMZaD6DG++sDCoumYl/5xL40dL2b8P5tb3CKf/khWyL2Gw==} + peerDependencies: + '@capacitor/core': ^7.4.0 + + '@capacitor/cli@7.4.5': + resolution: {integrity: sha512-ApKxpzcWTqmyTD+WxBvL7UJt+x0xr9Vqa+S4Iqbl/R2kC0tf1ay22wE9pOrdWXbTqFbNhBuUuRvTvZ6Edp7QzQ==} + engines: {node: '>=20.0.0'} + hasBin: true + + '@capacitor/core@7.4.5': + resolution: {integrity: sha512-JnUywi0WRbqm4QYoplNsMQ7KAVuyrQFha0dOH9YDVCyrVHUvPcvrnK+9j5elUMgy+pOh3ve+pk6BVoySr91c+A==} + + '@capacitor/ios@7.4.5': + resolution: {integrity: sha512-i9cfo15Vn0GvFJ0Pm2zPhxQrK3NW5ISrf7bdzmKWUM5TUaCnWkCO4Zxsgjm8/VMXwt/gabA/di1O1c9pC3enCA==} + peerDependencies: + '@capacitor/core': ^7.4.0 + '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} @@ -42633,6 +42666,38 @@ packages: cpu: [x64] os: [win32] + '@ionic/cli-framework-output@2.2.8': + resolution: {integrity: sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-array@2.1.6': + resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-fs@3.1.7': + resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-object@2.1.6': + resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-process@2.1.12': + resolution: {integrity: sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-stream@3.1.7': + resolution: {integrity: sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-subprocess@3.0.1': + resolution: {integrity: sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-terminal@2.3.5': + resolution: {integrity: sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==} + engines: {node: '>=16.0.0'} + '@isaacs/balanced-match@4.0.1': resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} engines: {node: 20 || >=22} @@ -44811,6 +44876,9 @@ packages: '@types/express@4.17.25': resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/fs-extra@8.1.5': + resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} + '@types/fs-extra@9.0.13': resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} @@ -45045,6 +45113,9 @@ packages: '@types/serve-static@1.15.10': resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/slice-ansi@4.0.0': + resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==} + '@types/snappyjs@0.7.1': resolution: {integrity: sha512-OxjzJ6cQZstysMh6PEwZWmK9qlKZyezHJKOkcUkZDooSFuog2votUEKkxMaTq51UQF3cJkXKQ+XGlj4FSl8JQQ==} @@ -45742,6 +45813,10 @@ packages: bowser@2.12.1: resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} + bplist-parser@0.3.2: + resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} + engines: {node: '>= 5.10.0'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -46098,6 +46173,10 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@14.0.2: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} @@ -46932,6 +47011,10 @@ packages: engines: {node: '>= 12.20.55'} hasBin: true + elementtree@0.1.7: + resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==} + engines: {node: '>= 0.4.0'} + email-addresses@5.0.0: resolution: {integrity: sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==} @@ -47728,6 +47811,10 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@13.0.0: + resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} + engines: {node: 20 || >=22} + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -48119,6 +48206,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + install@0.13.0: resolution: {integrity: sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==} engines: {node: '>= 0.10'} @@ -49386,6 +49477,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + native-run@2.0.3: + resolution: {integrity: sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==} + engines: {node: '>=16.0.0'} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -49856,6 +49952,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} @@ -50497,6 +50597,11 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rimraf@6.1.2: + resolution: {integrity: sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==} + engines: {node: 20 || >=22} + hasBin: true + roarr@2.15.4: resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} engines: {node: '>=8.0'} @@ -50596,6 +50701,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + sax@1.1.4: + resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==} + sax@1.4.3: resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} @@ -50773,6 +50881,10 @@ packages: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + slice-ansi@5.0.0: resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} engines: {node: '>=12'} @@ -50874,6 +50986,10 @@ packages: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -51195,6 +51311,7 @@ packages: tar@6.2.1: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me tar@7.5.2: resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} @@ -51598,6 +51715,10 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + unused-filename@4.0.1: resolution: {integrity: sha512-ZX6U1J04K1FoSUeoX1OicAhw4d0aro2qo+L8RhJkiGTNtBNkd/Fi1Wxoc9HzcVu6HfOzm0si/N15JjxFmD1z6A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -52865,6 +52986,40 @@ snapshots: '@bufbuild/protobuf@1.10.1': {} + '@capacitor/android@7.4.5(@capacitor/core@7.4.5)': + dependencies: + '@capacitor/core': 7.4.5 + + '@capacitor/cli@7.4.5': + dependencies: + '@ionic/cli-framework-output': 2.2.8 + '@ionic/utils-subprocess': 3.0.1 + '@ionic/utils-terminal': 2.3.5 + commander: 12.1.0 + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 11.3.2 + kleur: 4.1.5 + native-run: 2.0.3 + open: 8.4.2 + plist: 3.1.0 + prompts: 2.4.2 + rimraf: 6.1.2 + semver: 7.7.3 + tar: 6.2.1 + tslib: 2.8.1 + xml2js: 0.6.2 + transitivePeerDependencies: + - supports-color + + '@capacitor/core@7.4.5': + dependencies: + tslib: 2.8.1 + + '@capacitor/ios@7.4.5(@capacitor/core@7.4.5)': + dependencies: + '@capacitor/core': 7.4.5 + '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 @@ -53324,6 +53479,82 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@ionic/cli-framework-output@2.2.8': + dependencies: + '@ionic/utils-terminal': 2.3.5 + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-array@2.1.6': + dependencies: + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-fs@3.1.7': + dependencies: + '@types/fs-extra': 8.1.5 + debug: 4.4.3 + fs-extra: 9.1.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-object@2.1.6': + dependencies: + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-process@2.1.12': + dependencies: + '@ionic/utils-object': 2.1.6 + '@ionic/utils-terminal': 2.3.5 + debug: 4.4.3 + signal-exit: 3.0.7 + tree-kill: 1.2.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-stream@3.1.7': + dependencies: + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-subprocess@3.0.1': + dependencies: + '@ionic/utils-array': 2.1.6 + '@ionic/utils-fs': 3.1.7 + '@ionic/utils-process': 2.1.12 + '@ionic/utils-stream': 3.1.7 + '@ionic/utils-terminal': 2.3.5 + cross-spawn: 7.0.6 + debug: 4.4.3 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@ionic/utils-terminal@2.3.5': + dependencies: + '@types/slice-ansi': 4.0.0 + debug: 4.4.3 + signal-exit: 3.0.7 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + tslib: 2.8.1 + untildify: 4.0.0 + wrap-ansi: 7.0.0 + transitivePeerDependencies: + - supports-color + '@isaacs/balanced-match@4.0.1': {} '@isaacs/brace-expansion@5.0.0': @@ -56112,6 +56343,10 @@ snapshots: '@types/qs': 6.9.18 '@types/serve-static': 1.15.10 + '@types/fs-extra@8.1.5': + dependencies: + '@types/node': 22.19.0 + '@types/fs-extra@9.0.13': dependencies: '@types/node': 22.19.0 @@ -56406,6 +56641,8 @@ snapshots: '@types/node': 22.19.0 '@types/send': 0.17.6 + '@types/slice-ansi@4.0.0': {} + '@types/snappyjs@0.7.1': {} '@types/sockjs@0.3.36': @@ -56996,8 +57233,7 @@ snapshots: dependencies: tslib: 2.8.1 - astral-regex@2.0.0: - optional: true + astral-regex@2.0.0: {} async-exit-hook@2.0.1: {} @@ -57216,6 +57452,10 @@ snapshots: bowser@2.12.1: {} + bplist-parser@0.3.2: + dependencies: + big-integer: 1.6.52 + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -57654,6 +57894,8 @@ snapshots: commander@10.0.1: {} + commander@12.1.0: {} + commander@14.0.2: {} commander@2.11.0: {} @@ -58567,6 +58809,10 @@ snapshots: transitivePeerDependencies: - supports-color + elementtree@0.1.7: + dependencies: + sax: 1.1.4 + email-addresses@5.0.0: {} emittery@0.13.1: {} @@ -59594,6 +59840,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@13.0.0: + dependencies: + minimatch: 10.1.1 + minipass: 7.1.2 + path-scurry: 2.0.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -60079,6 +60331,8 @@ snapshots: inherits@2.0.4: {} + ini@4.1.3: {} + install@0.13.0: {} internal-slot@1.1.0: @@ -61637,6 +61891,22 @@ snapshots: nanoid@3.3.11: {} + native-run@2.0.3: + dependencies: + '@ionic/utils-fs': 3.1.7 + '@ionic/utils-terminal': 2.3.5 + bplist-parser: 0.3.2 + debug: 4.4.3 + elementtree: 0.1.7 + ini: 4.1.3 + plist: 3.1.0 + split2: 4.2.0 + through2: 4.0.2 + tslib: 2.8.1 + yauzl: 2.10.0 + transitivePeerDependencies: + - supports-color + natural-compare@1.4.0: {} negotiator@0.6.3: {} @@ -62113,6 +62383,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.2 + minipass: 7.1.2 + path-to-regexp@0.1.12: {} path-to-regexp@6.3.0: {} @@ -62807,6 +63082,11 @@ snapshots: dependencies: glob: 7.2.3 + rimraf@6.1.2: + dependencies: + glob: 13.0.0 + package-json-from-dist: 1.0.1 + roarr@2.15.4: dependencies: boolean: 3.2.0 @@ -62916,6 +63196,8 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.1 + sax@1.1.4: {} + sax@1.4.3: {} saxes@6.0.0: @@ -63145,6 +63427,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 optional: true + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + slice-ansi@5.0.0: dependencies: ansi-styles: 6.2.3 @@ -63286,6 +63574,8 @@ snapshots: split-on-first@1.1.0: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} sprintf-js@1.1.3: @@ -64170,6 +64460,8 @@ snapshots: unpipe@1.0.0: {} + untildify@4.0.0: {} + unused-filename@4.0.1: dependencies: escape-string-regexp: 5.0.0 diff --git a/common/scripts/docker-server.js b/common/scripts/docker-server.js index 82d30763e9..80c14ec2b6 100644 --- a/common/scripts/docker-server.js +++ b/common/scripts/docker-server.js @@ -1,23 +1,23 @@ -// -// Copyright © 2026 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -const path = require('path') -const { spawnSync } = require('child_process') - -const repoRoot = path.resolve(__dirname, '..', '..') -const args = ['docker:build', '-p', '20', '--to', '@hcengineering/pod-server', '--to', '@hcengineering/tool'] -const result = spawnSync('rush', args, { stdio: 'inherit', cwd: repoRoot }) -// Mimic "|| true" - always exit 0 so rush docker-server can continue to docker:up:server -process.exit(0) +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const path = require('path') +const { spawnSync } = require('child_process') + +const repoRoot = path.resolve(__dirname, '..', '..') +const args = ['docker:build', '-p', '20', '--to', '@hcengineering/pod-server', '--to', '@hcengineering/tool'] +const result = spawnSync('rush', args, { stdio: 'inherit', cwd: repoRoot }) +// Mimic "|| true" - always exit 0 so rush docker-server can continue to docker:up:server +process.exit(0) diff --git a/common/scripts/docker.js b/common/scripts/docker.js index a207f5e3ab..30e6891ce4 100644 --- a/common/scripts/docker.js +++ b/common/scripts/docker.js @@ -1,55 +1,55 @@ -// -// Copyright © 2026 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -const path = require('path') -const { spawnSync } = require('child_process') - -const repoRoot = path.resolve(__dirname, '..', '..') -const packages = [ - '@hcengineering/pod-server', - '@hcengineering/pod-front', - '@hcengineering/prod', - '@hcengineering/pod-account', - '@hcengineering/pod-workspace', - '@hcengineering/pod-collaborator', - '@hcengineering/tool', - '@hcengineering/pod-print', - '@hcengineering/pod-sign', - '@hcengineering/pod-analytics-collector', - '@hcengineering/rekoni-service', - '@hcengineering/pod-ai-bot', - '@hcengineering/import-tool', - '@hcengineering/pod-stats', - '@hcengineering/pod-fulltext', - '@hcengineering/pod-love', - '@hcengineering/pod-mail', - '@hcengineering/pod-datalake', - '@hcengineering/pod-mail-worker', - '@hcengineering/pod-export', - '@hcengineering/pod-media', - '@hcengineering/pod-preview', - '@hcengineering/pod-link-preview', - '@hcengineering/pod-external', - '@hcengineering/pod-backup', - '@hcengineering/backup-api-pod', - '@hcengineering/pod-billing', - '@hcengineering/pod-process', - '@hcengineering/pod-rating', - '@hcengineering/pod-payment' -] - -const args = ['docker:build', '-p', '20', ...packages.flatMap(p => ['--to', p])] -const result = spawnSync('rush', args, { stdio: 'inherit', cwd: repoRoot }) -process.exit(result.status !== null ? result.status : 1) +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const path = require('path') +const { spawnSync } = require('child_process') + +const repoRoot = path.resolve(__dirname, '..', '..') +const packages = [ + '@hcengineering/pod-server', + '@hcengineering/pod-front', + '@hcengineering/prod', + '@hcengineering/pod-account', + '@hcengineering/pod-workspace', + '@hcengineering/pod-collaborator', + '@hcengineering/tool', + '@hcengineering/pod-print', + '@hcengineering/pod-sign', + '@hcengineering/pod-analytics-collector', + '@hcengineering/rekoni-service', + '@hcengineering/pod-ai-bot', + '@hcengineering/import-tool', + '@hcengineering/pod-stats', + '@hcengineering/pod-fulltext', + '@hcengineering/pod-love', + '@hcengineering/pod-mail', + '@hcengineering/pod-datalake', + '@hcengineering/pod-mail-worker', + '@hcengineering/pod-export', + '@hcengineering/pod-media', + '@hcengineering/pod-preview', + '@hcengineering/pod-link-preview', + '@hcengineering/pod-external', + '@hcengineering/pod-backup', + '@hcengineering/backup-api-pod', + '@hcengineering/pod-billing', + '@hcengineering/pod-process', + '@hcengineering/pod-rating', + '@hcengineering/pod-payment' +] + +const args = ['docker:build', '-p', '20', ...packages.flatMap(p => ['--to', p])] +const result = spawnSync('rush', args, { stdio: 'inherit', cwd: repoRoot }) +process.exit(result.status !== null ? result.status : 1) diff --git a/common/scripts/docker_build.js b/common/scripts/docker_build.js index da1b55b91c..c01cb24227 100644 --- a/common/scripts/docker_build.js +++ b/common/scripts/docker_build.js @@ -1,55 +1,55 @@ -// -// Copyright © 2026 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -const fs = require('fs') -const path = require('path') -const { execSync, spawnSync } = require('child_process') - -const image = process.argv[2] -if (!image) { - console.error('Usage: node docker_build.js [.]') - process.exit(1) -} - -const version = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim() -const cleanup = process.env.DOCKER_BUILD_CLEANUP === 'true' -const extra = (process.env.DOCKER_EXTRA || '').trim().split(/\s+/).filter(Boolean) - -console.log('Building version:', version) - -const dockerArgs = ['build', '-t', image, '-t', `${image}:${version}`, ...extra, '.'] -const result = spawnSync('docker', dockerArgs, { stdio: 'inherit', cwd: process.cwd() }) -if (result.status !== 0) { - process.exit(result.status) -} - -if (cleanup) { - console.log('Cleaning up build artifacts...') - const dirs = ['bundle', 'dist', '.rush'] - for (const dir of dirs) { - const fullPath = path.join(process.cwd(), dir) - if (fs.existsSync(fullPath)) { - console.log(' Removing', dir + '/') - fs.rmSync(fullPath, { recursive: true, force: true }) - } - } - // Skip du -sh on Windows (no du); optional on Unix - try { - const size = execSync('du -sh . 2>/dev/null | cut -f1', { encoding: 'utf8' }).trim() - if (size) console.log(' Size after cleanup:', size) - } catch (_) { - // ignore - } -} +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const fs = require('fs') +const path = require('path') +const { execSync, spawnSync } = require('child_process') + +const image = process.argv[2] +if (!image) { + console.error('Usage: node docker_build.js [.]') + process.exit(1) +} + +const version = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim() +const cleanup = process.env.DOCKER_BUILD_CLEANUP === 'true' +const extra = (process.env.DOCKER_EXTRA || '').trim().split(/\s+/).filter(Boolean) + +console.log('Building version:', version) + +const dockerArgs = ['build', '-t', image, '-t', `${image}:${version}`, ...extra, '.'] +const result = spawnSync('docker', dockerArgs, { stdio: 'inherit', cwd: process.cwd() }) +if (result.status !== 0) { + process.exit(result.status) +} + +if (cleanup) { + console.log('Cleaning up build artifacts...') + const dirs = ['bundle', 'dist', '.rush'] + for (const dir of dirs) { + const fullPath = path.join(process.cwd(), dir) + if (fs.existsSync(fullPath)) { + console.log(' Removing', dir + '/') + fs.rmSync(fullPath, { recursive: true, force: true }) + } + } + // Skip du -sh on Windows (no du); optional on Unix + try { + const size = execSync('du -sh . 2>/dev/null | cut -f1', { encoding: 'utf8' }).trim() + if (size) console.log(' Size after cleanup:', size) + } catch (_) { + // ignore + } +} diff --git a/common/scripts/package-desktop-copy.js b/common/scripts/package-desktop-copy.js index c0a7ef5bda..125f02662f 100644 --- a/common/scripts/package-desktop-copy.js +++ b/common/scripts/package-desktop-copy.js @@ -1,48 +1,48 @@ -// -// Copyright © 2026 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -const fs = require('fs') -const path = require('path') - -const withIcons = process.argv.includes('--with-icons') - -// Run from desktop-package or qms-desktop-package directory (cwd = package dir) -const distDir = path.join(process.cwd(), 'dist') -const desktopDist = path.join(process.cwd(), '..', 'desktop', 'dist') -const desktopEnv = path.join(process.cwd(), '..', 'desktop', '.env') - -if (fs.existsSync(distDir)) { - fs.rmSync(distDir, { recursive: true, force: true }) -} - -fs.cpSync(desktopDist, distDir, { recursive: true }) - -if (fs.existsSync(desktopEnv)) { - fs.cpSync(desktopEnv, path.join(distDir, '.env')) -} - -if (withIcons) { - const publicDir = path.join(distDir, 'ui', 'public') - fs.mkdirSync(publicDir, { recursive: true }) - const srcDir = path.join(process.cwd(), 'src') - const iconPng = path.join(srcDir, 'AppIcon.png') - const iconIco = path.join(srcDir, 'AppIcon.ico') - if (fs.existsSync(iconPng)) { - fs.cpSync(iconPng, path.join(publicDir, 'AppIcon.png')) - } - if (fs.existsSync(iconIco)) { - fs.cpSync(iconIco, path.join(publicDir, 'AppIcon.ico')) - } -} +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const fs = require('fs') +const path = require('path') + +const withIcons = process.argv.includes('--with-icons') + +// Run from desktop-package or qms-desktop-package directory (cwd = package dir) +const distDir = path.join(process.cwd(), 'dist') +const desktopDist = path.join(process.cwd(), '..', 'desktop', 'dist') +const desktopEnv = path.join(process.cwd(), '..', 'desktop', '.env') + +if (fs.existsSync(distDir)) { + fs.rmSync(distDir, { recursive: true, force: true }) +} + +fs.cpSync(desktopDist, distDir, { recursive: true }) + +if (fs.existsSync(desktopEnv)) { + fs.cpSync(desktopEnv, path.join(distDir, '.env')) +} + +if (withIcons) { + const publicDir = path.join(distDir, 'ui', 'public') + fs.mkdirSync(publicDir, { recursive: true }) + const srcDir = path.join(process.cwd(), 'src') + const iconPng = path.join(srcDir, 'AppIcon.png') + const iconIco = path.join(srcDir, 'AppIcon.ico') + if (fs.existsSync(iconPng)) { + fs.cpSync(iconPng, path.join(publicDir, 'AppIcon.png')) + } + if (fs.existsSync(iconIco)) { + fs.cpSync(iconIco, path.join(publicDir, 'AppIcon.ico')) + } +} diff --git a/common/scripts/package-pod-front-copy.js b/common/scripts/package-pod-front-copy.js index ba56fd29f7..e5df66cdf2 100644 --- a/common/scripts/package-pod-front-copy.js +++ b/common/scripts/package-pod-front-copy.js @@ -1,42 +1,42 @@ -// -// Copyright © 2026 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -const fs = require('fs') -const path = require('path') - -const cwd = process.cwd() -const distDir = path.join(cwd, 'dist') -const prodDist = path.join(cwd, '..', '..', 'dev', 'prod', 'dist') -const prodPublic = path.join(cwd, '..', '..', 'dev', 'prod', 'public') - -if (fs.existsSync(distDir)) { - fs.rmSync(distDir, { recursive: true, force: true }) -} - -fs.cpSync(prodDist, distDir, { recursive: true }) - -if (fs.existsSync(prodPublic)) { - const entries = fs.readdirSync(prodPublic, { withFileTypes: true }) - for (const entry of entries) { - const src = path.join(prodPublic, entry.name) - const dest = path.join(distDir, entry.name) - fs.cpSync(src, dest, { recursive: true }) - } -} - -const configJson = path.join(distDir, 'config.json') -if (fs.existsSync(configJson)) { - fs.rmSync(configJson, { force: true }) -} +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const fs = require('fs') +const path = require('path') + +const cwd = process.cwd() +const distDir = path.join(cwd, 'dist') +const prodDist = path.join(cwd, '..', '..', 'dev', 'prod', 'dist') +const prodPublic = path.join(cwd, '..', '..', 'dev', 'prod', 'public') + +if (fs.existsSync(distDir)) { + fs.rmSync(distDir, { recursive: true, force: true }) +} + +fs.cpSync(prodDist, distDir, { recursive: true }) + +if (fs.existsSync(prodPublic)) { + const entries = fs.readdirSync(prodPublic, { withFileTypes: true }) + for (const entry of entries) { + const src = path.join(prodPublic, entry.name) + const dest = path.join(distDir, entry.name) + fs.cpSync(src, dest, { recursive: true }) + } +} + +const configJson = path.join(distDir, 'config.json') +if (fs.existsSync(configJson)) { + fs.rmSync(configJson, { force: true }) +} diff --git a/common/scripts/run-with-version.js b/common/scripts/run-with-version.js index c732f70867..43696823c4 100644 --- a/common/scripts/run-with-version.js +++ b/common/scripts/run-with-version.js @@ -1,32 +1,32 @@ -// -// Copyright © 2026 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -const path = require('path') -const { execSync, spawnSync } = require('child_process') - -const scriptsDir = __dirname -const modelVersion = execSync('node show_version.js', { encoding: 'utf8', cwd: scriptsDir }).trim() -const version = execSync('node show_tag.js', { encoding: 'utf8', cwd: scriptsDir }).trim() - -process.env.MODEL_VERSION = modelVersion -process.env.VERSION = version - -const [cmd, ...args] = process.argv.slice(2) -if (!cmd) { - process.exit(1) -} -const command = [cmd, ...args].join(' ') -const result = spawnSync(command, { stdio: 'inherit', env: process.env, shell: true }) -process.exit(result.status !== null ? result.status : 1) +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const path = require('path') +const { execSync, spawnSync } = require('child_process') + +const scriptsDir = __dirname +const modelVersion = execSync('node show_version.js', { encoding: 'utf8', cwd: scriptsDir }).trim() +const version = execSync('node show_tag.js', { encoding: 'utf8', cwd: scriptsDir }).trim() + +process.env.MODEL_VERSION = modelVersion +process.env.VERSION = version + +const [cmd, ...args] = process.argv.slice(2) +if (!cmd) { + process.exit(1) +} +const command = [cmd, ...args].join(' ') +const result = spawnSync(command, { stdio: 'inherit', env: process.env, shell: true }) +process.exit(result.status !== null ? result.status : 1) diff --git a/dev/prod/public/config-mobile.json b/dev/prod/public/config-mobile.json new file mode 100644 index 0000000000..c224ac0f33 --- /dev/null +++ b/dev/prod/public/config-mobile.json @@ -0,0 +1,36 @@ +{ + "ACCOUNTS_URL": "http://localhost:8080/account", + "COLLABORATOR_URL": "ws://huly.local:3078", + "PREVIEW_URL": "http://huly.local:4040", + "FILES_URL": "http://huly.local:4030/blob/:workspace/:blobId/:filename", + "UPLOAD_URL": "http://localhost:8080/files", + "DATALAKE_URL": "http://huly.local:4030", + "TELEGRAM_URL": "http://huly.local:8086", + "GMAIL_URL": "http://huly.local:8093", + "CALENDAR_URL": "http://localhost:8095", + "REKONI_URL": "http://localhost:8080/rekoni", + "GITHUB_APP": "uberflow-dev", + "GITHUB_CLIENTID": "Iv1.43f9cac43bd68617", + "GITHUB_URL": "http://huly.local:3500", + "LAST_NAME_FIRST": "true", + "PRINT_URL": "http://huly.local:4005", + "SIGN_URL": "http://huly.local:4006", + "AI_URL": "http://huly.local:4010", + "BRANDING_URL": "http://localhost:8080/branding.json", + "VERSION": null, + "MODEL_VERSION": null, + "STATS_URL": "http://huly.local:4900", + "PASSWORD_STRICTNESS": "none", + "STREAM_URL": "http://huly.local:1080/recording", + "PAYMENT_URL": "http://huly.local:3040", + "PUBLIC_SCHEDULE_URL": "http://huly.local:8060", + "CALDAV_SERVER_URL": "http://huly.local:9070", + "EXPORT_URL": "http://huly.local:4009", + "COMMUNICATION_API_ENABLED": "true", + "BACKUP_URL": "http://huly.local:4039/api/backup", + "PULSE_URL": "ws://huly.local:8099/ws", + "HULYLAKE_URL": "http://huly.local:8096", + "EXCLUDED_APPLICATIONS_FOR_ANONYMOUS": "[\"chunter\", \"notification\"]", + "ANALYTICS_COLLECTOR_URL": "http://huly.local:4017", + "LINK_PREVIEW_URL": "http://huly.local:4041" +} diff --git a/dev/prod/src/main.ts b/dev/prod/src/main.ts index ea4d431004..bd5dee07ee 100644 --- a/dev/prod/src/main.ts +++ b/dev/prod/src/main.ts @@ -17,6 +17,43 @@ import { createApp } from '@hcengineering/ui' import { configurePlatform } from './platform' -configurePlatform().then(() => { - createApp(document.body) -}) +/** + * Check if running inside a Capacitor native app. + */ +function isCapacitorEnvironment (): boolean { + return ( + typeof window !== 'undefined' && + window.Capacitor !== undefined + ) +} + +/** + * Show a basic error UI when the app fails to initialize. + * This is a fallback for when the native shell error screen isn't shown. + */ +function showInitializationError (error: Error): void { + console.error('Failed to initialize platform:', error) + + // In Capacitor, the native shell should handle errors, so we don't need to show UI + if (isCapacitorEnvironment()) { + console.log('Running in Capacitor - native shell should handle error display') + return + } + + // For web browsers, show a simple error message + document.body.innerHTML = ` +
+

Connection Error

+

Unable to connect to the server.
Please check if the server is running and try again.

+ +
+ ` +} + +configurePlatform() + .then(() => { + createApp(document.body) + }) + .catch((error: Error) => { + showInitializationError(error) + }) diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index 7446fc8c3b..361bbb2ac8 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -431,8 +431,19 @@ export async function configurePlatform() { configureI18n() const config: Config = await loadServerConfig(configs[clientType ?? ''] ?? '/config.json') - const branding: BrandingMap = - config.BRANDING_URL !== undefined ? await (await fetch(config.BRANDING_URL, { keepalive: true })).json() : {} + + // Load branding with fallback to empty object if fetch fails + let branding: BrandingMap = {} + if (config.BRANDING_URL !== undefined) { + try { + const brandingResponse = await fetch(config.BRANDING_URL, { keepalive: true }) + if (brandingResponse.ok) { + branding = await brandingResponse.json() + } + } catch (e) { + console.warn('Failed to load branding config:', e) + } + } const myBranding = branding[window.location.host] ?? {} console.log('loading configuration', config) diff --git a/foundations/net/common/scripts/docker_build.js b/foundations/net/common/scripts/docker_build.js index b365c97c5e..47509ab046 100644 --- a/foundations/net/common/scripts/docker_build.js +++ b/foundations/net/common/scripts/docker_build.js @@ -1,55 +1,55 @@ -// -// Copyright © 2024 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -const fs = require('fs') -const path = require('path') -const { execSync, spawnSync } = require('child_process') - -const image = process.argv[2] -if (!image) { - console.error('Usage: node docker_build.js [.]') - process.exit(1) -} - -const version = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim() -const cleanup = process.env.DOCKER_BUILD_CLEANUP === 'true' -const extra = (process.env.DOCKER_EXTRA || '').trim().split(/\s+/).filter(Boolean) - -console.log('Building version:', version) - -const dockerArgs = ['build', '-t', image, '-t', `${image}:${version}`, ...extra, '.'] -const result = spawnSync('docker', dockerArgs, { stdio: 'inherit', cwd: process.cwd() }) -if (result.status !== 0) { - process.exit(result.status) -} - -if (cleanup) { - console.log('Cleaning up build artifacts...') - const dirs = ['bundle', 'dist', '.rush'] - for (const dir of dirs) { - const fullPath = path.join(process.cwd(), dir) - if (fs.existsSync(fullPath)) { - console.log(' Removing', dir + '/') - fs.rmSync(fullPath, { recursive: true, force: true }) - } - } - // Skip du -sh on Windows (no du); optional on Unix - try { - const size = execSync('du -sh . 2>/dev/null | cut -f1', { encoding: 'utf8' }).trim() - if (size) console.log(' Size after cleanup:', size) - } catch (_) { - // ignore - } -} +// +// Copyright © 2024 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +const fs = require('fs') +const path = require('path') +const { execSync, spawnSync } = require('child_process') + +const image = process.argv[2] +if (!image) { + console.error('Usage: node docker_build.js [.]') + process.exit(1) +} + +const version = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim() +const cleanup = process.env.DOCKER_BUILD_CLEANUP === 'true' +const extra = (process.env.DOCKER_EXTRA || '').trim().split(/\s+/).filter(Boolean) + +console.log('Building version:', version) + +const dockerArgs = ['build', '-t', image, '-t', `${image}:${version}`, ...extra, '.'] +const result = spawnSync('docker', dockerArgs, { stdio: 'inherit', cwd: process.cwd() }) +if (result.status !== 0) { + process.exit(result.status) +} + +if (cleanup) { + console.log('Cleaning up build artifacts...') + const dirs = ['bundle', 'dist', '.rush'] + for (const dir of dirs) { + const fullPath = path.join(process.cwd(), dir) + if (fs.existsSync(fullPath)) { + console.log(' Removing', dir + '/') + fs.rmSync(fullPath, { recursive: true, force: true }) + } + } + // Skip du -sh on Windows (no du); optional on Unix + try { + const size = execSync('du -sh . 2>/dev/null | cut -f1', { encoding: 'utf8' }).trim() + if (size) console.log(' Size after cleanup:', size) + } catch (_) { + // ignore + } +} diff --git a/mobile/README.md b/mobile/README.md new file mode 100644 index 0000000000..b719301e6a --- /dev/null +++ b/mobile/README.md @@ -0,0 +1,86 @@ +# Platform Mobile App (Capacitor) + +iOS and Android app that wraps the Platform web frontend using [Capacitor](https://capacitorjs.com/). The same Svelte app built by `dev/prod` is copied into `www/` and loaded by the native shell. + +## Prerequisites + +- Node.js and Rush (see root [README](../README.md)) +- **iOS**: macOS with Xcode +- **Android**: [Android Studio](https://developer.android.com/studio) + +## Quick start + +From the repo root: + +1. Build the web app: `rush build --to @hcengineering/prod` +2. `cd mobile` +3. `npm run sync` (copies web assets to `www/` and runs `cap sync`) +4. `npm run open:ios` or `npm run open:android` to open the native IDE + +## Scripts + +| Script | Description | +|--------|-------------| +| `npm run build:web` | Copy `dev/prod/dist` and `dev/prod/public` into `mobile/www`. Run after building the web app. | +| `npm run sync` | Run `build:web` then `cap sync` (updates native projects with `www` content). | +| `npm run open:ios` | Open the iOS project in Xcode. | +| `npm run open:android` | Open the Android project in Android Studio. | +| `npm run run:ios` | Run on iOS simulator/device. | +| `npm run run:android` | Run on Android emulator/device. | + +## Configuration + +- **capacitor.config.ts**: `appId` is `io.huly.platform`; `appName` is `Huly`. Change these if you need a different bundle ID or display name. +- **Local-first load**: The app loads from the bundled `www/` directory (no `server.url`), so it opens without requiring a connection to any server. The login page and shell are served from local resources. +- **Backend URL**: The web app reads `/config.json` from the bundle. The copy script uses `dev/prod/public/config-mobile.json` as `www/config.json` when present (full backend URLs); otherwise it copies `config.json`. For production, use a config that points to your production backend. On Android emulator, replace `localhost` with `10.0.2.2` in `config-mobile.json` if the app cannot reach the backend. +- **Server unavailable**: When the user tries to use backend features (e.g. login) and the server is unreachable, the app shows a “Server unavailable” message in the login (or relevant) UI. To use the webpack dev server for live reload, temporarily add `server: { url: 'http://localhost:8080', cleartext: true }` to `capacitor.config.ts` and run `npx cap sync`. + +## Native Shell Architecture + +The mobile app implements a native shell that handles error states when the server is unavailable: + +### Error Handling + +- **Android**: `MainActivity.java` includes a custom `WebViewClient` that intercepts WebView load errors (connection refused, timeout, no network) and displays a native error screen with a Retry button. +- **iOS**: `CustomBridgeViewController.swift` implements `WKNavigationDelegate` to intercept navigation errors and show a native error overlay. + +### Network Monitoring + +Both platforms monitor network connectivity: +- When network is lost, the native shell shows an error screen +- When network is restored, the error message updates to indicate retry is available +- The `@capacitor/network` plugin provides JavaScript-level network status if needed + +### Splash Screen + +The `@capacitor/splash-screen` plugin is configured with manual control (`launchAutoHide: false`). The native shell keeps the splash screen visible until the WebView successfully loads, providing a smooth transition. + +### Error Screen + +When the server is unavailable: +1. Native error screen appears with app branding +2. User sees "Connection Error" message with description +3. "Retry" button reloads the WebView +4. On success, error screen hides and web app is displayed + +## Icons and splash + +App and splash icons use the Huly branding from [dev/prod/public/huly/](../dev/prod/public/huly/) (same source as the web app: `icon-1024.png`, etc.). To refresh mobile icons after updating Huly assets, copy `dev/prod/public/huly/icon-1024.png` to: + +- **Android**: `android/app/src/main/res/mipmap-*/ic_launcher.png`, `ic_launcher_foreground.png`, `ic_launcher_round.png`, and `drawable*/splash.png`. +- **iOS**: `ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png` and `Splash.imageset/splash-2732x2732*.png`. + +## Web assets + +The `www/` directory is generated by `npm run build:web` and is ignored by git. Always run `npm run sync` after building the web app or changing public assets so the native projects have the latest content. + +## Adding platforms (first-time setup) + +If the `ios/` or `android/` folders are missing (e.g. after a fresh clone), run once from `mobile/`: + +```bash +npm run add:ios +npm run add:android +``` + +Then run `npm run sync` before opening the native projects. diff --git a/mobile/android/.gitignore b/mobile/android/.gitignore new file mode 100644 index 0000000000..48354a3dfc --- /dev/null +++ b/mobile/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/mobile/android/app/.gitignore b/mobile/android/app/.gitignore new file mode 100644 index 0000000000..043df802a2 --- /dev/null +++ b/mobile/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle new file mode 100644 index 0000000000..d748a8a4cc --- /dev/null +++ b/mobile/android/app/build.gradle @@ -0,0 +1,52 @@ +apply plugin: 'com.android.application' + +android { + namespace "io.huly.platform" + compileSdk rootProject.ext.compileSdkVersion + sourceSets { + main.assets.exclude '**/*.gz' + } + defaultConfig { + applicationId "io.huly.platform" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Exclude *.gz to avoid "Duplicate resources" (same base file + .gz both present from web build). + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~:*.gz' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/mobile/android/app/capacitor.build.gradle b/mobile/android/app/capacitor.build.gradle new file mode 100644 index 0000000000..db26f4aa8e --- /dev/null +++ b/mobile/android/app/capacitor.build.gradle @@ -0,0 +1,20 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + implementation project(':capacitor-network') + implementation project(':capacitor-splash-screen') + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/mobile/android/app/proguard-rules.pro b/mobile/android/app/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/mobile/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/mobile/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/mobile/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000000..f2c2217efa --- /dev/null +++ b/mobile/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..7e5a2ff47d --- /dev/null +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/app/src/main/java/io/huly/platform/MainActivity.java b/mobile/android/app/src/main/java/io/huly/platform/MainActivity.java new file mode 100644 index 0000000000..d5c8fd52ce --- /dev/null +++ b/mobile/android/app/src/main/java/io/huly/platform/MainActivity.java @@ -0,0 +1,332 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package io.huly.platform; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebView; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.getcapacitor.Bridge; +import com.getcapacitor.BridgeActivity; +import com.getcapacitor.BridgeWebViewClient; + +/** + * Main activity for the Huly mobile app. + * Implements native error handling for WebView load failures and network monitoring. + */ +public class MainActivity extends BridgeActivity { + private static final String TAG = "HulyMainActivity"; + + // Error codes we handle + private static final int ERROR_HOST_LOOKUP = -2; // ERR_NAME_NOT_RESOLVED + private static final int ERROR_CONNECT = -6; // ERR_CONNECTION_REFUSED + private static final int ERROR_TIMEOUT = -8; // ERR_TIMED_OUT + private static final int ERROR_IO = -7; // ERR_IO_PENDING + private static final int ERROR_UNKNOWN = -1; // ERROR_UNKNOWN + + private View errorView; + private boolean isShowingError = false; + private boolean hasLoadedSuccessfully = false; + private ConnectivityManager.NetworkCallback networkCallback; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Set up the custom WebViewClient after the bridge is ready + Bridge bridge = this.getBridge(); + if (bridge != null) { + WebView webView = bridge.getWebView(); + if (webView != null) { + webView.setWebViewClient(new HulyWebViewClient(bridge)); + } + } + + // Set up error view + setupErrorView(); + + // Set up network monitoring + setupNetworkMonitoring(); + + // Check initial connectivity + if (!isNetworkAvailable()) { + Log.w(TAG, "No network available on startup"); + // Don't show error immediately, let WebView try to load first + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + // Unregister network callback + if (networkCallback != null) { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm != null) { + cm.unregisterNetworkCallback(networkCallback); + } + } + } + + /** + * Set up the error view that overlays the WebView when errors occur. + */ + private void setupErrorView() { + errorView = getLayoutInflater().inflate(R.layout.error_screen, null); + + Button retryButton = errorView.findViewById(R.id.retryButton); + if (retryButton != null) { + retryButton.setOnClickListener(v -> retryLoading()); + } + + // Add error view to the content but keep it hidden + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ); + + // Find the root view and add error view + View rootView = findViewById(android.R.id.content); + if (rootView instanceof FrameLayout) { + ((FrameLayout) rootView).addView(errorView, params); + } + + errorView.setVisibility(View.GONE); + } + + /** + * Set up network connectivity monitoring. + */ + private void setupNetworkMonitoring() { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) return; + + networkCallback = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(@NonNull Network network) { + Log.d(TAG, "Network available"); + // If we're showing error and network is back, offer retry + runOnUiThread(() -> { + if (isShowingError) { + updateErrorMessage(getString(R.string.error_network_restored)); + } + }); + } + + @Override + public void onLost(@NonNull Network network) { + Log.d(TAG, "Network lost"); + // Only show error if we haven't loaded successfully + runOnUiThread(() -> { + if (!hasLoadedSuccessfully) { + showErrorScreen(getString(R.string.error_no_network)); + } + }); + } + }; + + NetworkRequest request = new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(); + + cm.registerNetworkCallback(request, networkCallback); + } + + /** + * Check if network is currently available. + */ + private boolean isNetworkAvailable() { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) return false; + + Network network = cm.getActiveNetwork(); + if (network == null) return false; + + NetworkCapabilities caps = cm.getNetworkCapabilities(network); + return caps != null && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + } + + /** + * Show the native error screen. + */ + private void showErrorScreen(String message) { + if (errorView == null) return; + + isShowingError = true; + + TextView messageView = errorView.findViewById(R.id.errorMessage); + if (messageView != null && message != null) { + messageView.setText(message); + } + + errorView.setVisibility(View.VISIBLE); + + // Hide WebView + Bridge bridge = getBridge(); + if (bridge != null && bridge.getWebView() != null) { + bridge.getWebView().setVisibility(View.INVISIBLE); + } + } + + /** + * Hide the error screen and show WebView. + */ + private void hideErrorScreen() { + if (errorView == null) return; + + isShowingError = false; + errorView.setVisibility(View.GONE); + + // Show WebView + Bridge bridge = getBridge(); + if (bridge != null && bridge.getWebView() != null) { + bridge.getWebView().setVisibility(View.VISIBLE); + } + } + + /** + * Update the error message without changing visibility. + */ + private void updateErrorMessage(String message) { + if (errorView == null) return; + + TextView messageView = errorView.findViewById(R.id.errorMessage); + if (messageView != null && message != null) { + messageView.setText(message); + } + } + + /** + * Retry loading the WebView content. + */ + private void retryLoading() { + Log.d(TAG, "Retrying WebView load"); + + hideErrorScreen(); + hasLoadedSuccessfully = false; + + Bridge bridge = getBridge(); + if (bridge != null && bridge.getWebView() != null) { + bridge.getWebView().reload(); + } + } + + /** + * Custom WebViewClient that intercepts load errors and shows native error UI. + */ + private class HulyWebViewClient extends BridgeWebViewClient { + public HulyWebViewClient(Bridge bridge) { + super(bridge); + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + super.onPageStarted(view, url, favicon); + Log.d(TAG, "Page started loading: " + url); + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + Log.d(TAG, "Page finished loading: " + url); + + // Mark as successfully loaded + hasLoadedSuccessfully = true; + + // Hide error screen if it was showing + runOnUiThread(() -> hideErrorScreen()); + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + super.onReceivedError(view, request, error); + + // Only handle errors for the main frame + if (request.isForMainFrame()) { + int errorCode = error.getErrorCode(); + String description = error.getDescription().toString(); + String url = request.getUrl().toString(); + + Log.e(TAG, "WebView error: code=" + errorCode + ", desc=" + description + ", url=" + url); + + // Handle connection-related errors + if (isConnectionError(errorCode)) { + runOnUiThread(() -> { + String message = getErrorMessage(errorCode); + showErrorScreen(message); + }); + } + } + } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + super.onReceivedError(view, errorCode, description, failingUrl); + + Log.e(TAG, "WebView error (legacy): code=" + errorCode + ", desc=" + description + ", url=" + failingUrl); + + // Handle connection-related errors + if (isConnectionError(errorCode)) { + runOnUiThread(() -> { + String message = getErrorMessage(errorCode); + showErrorScreen(message); + }); + } + } + + /** + * Check if the error code is a connection-related error. + */ + private boolean isConnectionError(int errorCode) { + return errorCode == ERROR_HOST_LOOKUP || + errorCode == ERROR_CONNECT || + errorCode == ERROR_TIMEOUT || + errorCode == ERROR_IO || + errorCode == ERROR_UNKNOWN; + } + + /** + * Get a user-friendly error message for the error code. + */ + private String getErrorMessage(int errorCode) { + switch (errorCode) { + case ERROR_HOST_LOOKUP: + case ERROR_CONNECT: + return getString(R.string.error_server_unavailable); + case ERROR_TIMEOUT: + return getString(R.string.error_connection_timeout); + default: + if (!isNetworkAvailable()) { + return getString(R.string.error_no_network); + } + return getString(R.string.error_server_unavailable); + } + } + } +} diff --git a/mobile/android/app/src/main/res/drawable-land-hdpi/splash.png b/mobile/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-land-mdpi/splash.png b/mobile/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-land-xhdpi/splash.png b/mobile/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/mobile/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/mobile/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-port-hdpi/splash.png b/mobile/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-port-mdpi/splash.png b/mobile/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-port-xhdpi/splash.png b/mobile/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/mobile/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/mobile/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..c7bd21dbd8 --- /dev/null +++ b/mobile/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml b/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..d5fccc538c --- /dev/null +++ b/mobile/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobile/android/app/src/main/res/drawable/retry_button_background.xml b/mobile/android/app/src/main/res/drawable/retry_button_background.xml new file mode 100644 index 0000000000..67e3bbfae0 --- /dev/null +++ b/mobile/android/app/src/main/res/drawable/retry_button_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/mobile/android/app/src/main/res/drawable/splash.png b/mobile/android/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000000..fd84f9b333 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable/splash.png differ diff --git a/mobile/android/app/src/main/res/drawable/splash_background.xml b/mobile/android/app/src/main/res/drawable/splash_background.xml new file mode 100644 index 0000000000..bff600e3b3 --- /dev/null +++ b/mobile/android/app/src/main/res/drawable/splash_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/mobile/android/app/src/main/res/layout/activity_main.xml b/mobile/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..b5ad138701 --- /dev/null +++ b/mobile/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/mobile/android/app/src/main/res/layout/error_screen.xml b/mobile/android/app/src/main/res/layout/error_screen.xml new file mode 100644 index 0000000000..62cb49524e --- /dev/null +++ b/mobile/android/app/src/main/res/layout/error_screen.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + +