diff --git a/.env.electron b/.env.electron new file mode 100644 index 00000000..9cde0b2a --- /dev/null +++ b/.env.electron @@ -0,0 +1 @@ +PLATFORM=electron \ No newline at end of file diff --git a/.env.mobile b/.env.mobile new file mode 100644 index 00000000..33e24dc8 --- /dev/null +++ b/.env.mobile @@ -0,0 +1 @@ +PLATFORM=mobile \ No newline at end of file diff --git a/.env.web b/.env.web new file mode 100644 index 00000000..74d1b12e --- /dev/null +++ b/.env.web @@ -0,0 +1 @@ +PLATFORM=web \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5fa65cf0..4f1c9854 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,12 @@ "@capacitor/app": "^6.0.0", "@capacitor/cli": "^6.2.0", "@capacitor/core": "^6.2.0", + "@capacitor/filesystem": "^6.0.3", "@capacitor/ios": "^6.2.0", + "@capacitor/share": "^6.0.3", "@dicebear/collection": "^5.4.1", "@dicebear/core": "^5.4.1", + "@electron/remote": "^2.1.2", "@ethersproject/hdnode": "^5.7.0", "@ethersproject/wallet": "^5.8.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", @@ -89,7 +92,7 @@ "@types/js-yaml": "^4.0.9", "@types/leaflet": "^1.9.8", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.11", + "@types/node": "^20.17.30", "@types/node-fetch": "^2.6.12", "@types/ramda": "^0.29.11", "@types/sqlite3": "^3.1.11", @@ -2878,6 +2881,15 @@ "tslib": "^2.1.0" } }, + "node_modules/@capacitor/filesystem": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-6.0.3.tgz", + "integrity": "sha512-PdIP/yOGAbG1lq1wbFbSPhXQ9/5lpTpeiok2NneawJOk6UXvy9W7QZXRo7wXAP7J6FdzU7bKfOORRXpOJpgXyw==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@capacitor/ios": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-6.2.1.tgz", @@ -2887,6 +2899,15 @@ "@capacitor/core": "^6.2.0" } }, + "node_modules/@capacitor/share": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@capacitor/share/-/share-6.0.3.tgz", + "integrity": "sha512-BkNM73Ix+yxQ7fkni8CrrGcp1kSl7u+YNoPLwWKQ1MuQ5Uav0d+CT8M67ie+3dc4jASmegnzlC6tkTmFcPTLeA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": "^6.0.0" + } + }, "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", @@ -3881,7 +3902,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.1.1", @@ -3903,7 +3923,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -3918,7 +3937,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -3928,7 +3946,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -3938,7 +3955,6 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -4069,6 +4085,15 @@ "node": ">=12" } }, + "node_modules/@electron/remote": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@electron/remote/-/remote-2.1.2.tgz", + "integrity": "sha512-EPwNx+nhdrTBxyCqXt/pftoQg/ybtWDW3DUWHafejvnB1ZGGfMpv6e15D8KeempocjXe78T7WreyGGb3mlZxdA==", + "license": "MIT", + "peerDependencies": { + "electron": ">= 13.0.0" + } + }, "node_modules/@electron/universal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz", @@ -9115,7 +9140,6 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -9390,7 +9414,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.0" @@ -9803,7 +9826,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, "license": "MIT", "dependencies": { "@types/http-cache-semantics": "*", @@ -9872,7 +9894,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { @@ -9929,7 +9950,6 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -10055,7 +10075,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -10195,7 +10214,6 @@ "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12667,7 +12685,6 @@ "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, "license": "MIT", "optional": true }, @@ -13022,7 +13039,6 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.6.0" @@ -13032,7 +13048,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, "license": "MIT", "dependencies": { "clone-response": "^1.0.2", @@ -13617,7 +13632,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" @@ -14899,7 +14913,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -14909,7 +14922,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -14936,7 +14949,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", @@ -15052,7 +15065,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, "license": "MIT", "optional": true }, @@ -15473,7 +15485,6 @@ "version": "33.4.8", "resolved": "https://registry.npmjs.org/electron/-/electron-33.4.8.tgz", "integrity": "sha512-dy/92HufGG66PslDMlXuK6uhO+70tgiZ4esReTZgDcZ0E67jCJ7S4/et4yZSEjXiT7IyjZTf72QwQbTpANxW4g==", - "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -15883,7 +15894,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, "license": "MIT", "optional": true }, @@ -16919,7 +16929,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", @@ -17813,7 +17822,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, "license": "MIT", "dependencies": { "pump": "^3.0.0" @@ -18001,7 +18009,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -18020,7 +18027,6 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -18037,7 +18043,6 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, "license": "(MIT OR CC0-1.0)", "optional": true, "engines": { @@ -18067,7 +18072,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "define-properties": "^1.2.1", @@ -18117,7 +18122,6 @@ "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.0.0", @@ -18224,7 +18228,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -18413,7 +18417,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "devOptional": true, "license": "BSD-2-Clause" }, "node_modules/http-errors": { @@ -18463,7 +18466,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", @@ -19887,7 +19889,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, "license": "MIT" }, "node_modules/json-parse-better-errors": { @@ -19949,7 +19950,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/json5": { @@ -20142,7 +20143,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -20861,7 +20861,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -21407,7 +21406,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -22818,7 +22816,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -23514,7 +23511,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -23843,7 +23839,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -24012,7 +24008,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -24832,7 +24827,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -25272,7 +25266,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -26508,7 +26501,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, "license": "MIT" }, "node_modules/resolve-from": { @@ -26544,7 +26536,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, "license": "MIT", "dependencies": { "lowercase-keys": "^2.0.0" @@ -26717,7 +26708,6 @@ "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, "license": "BSD-3-Clause", "optional": true, "dependencies": { @@ -27002,7 +26992,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, "license": "MIT", "optional": true }, @@ -28722,7 +28711,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "debug": "^4.1.0" diff --git a/package.json b/package.json index 1b2abd9b..68ad0aa1 100644 --- a/package.json +++ b/package.json @@ -46,9 +46,12 @@ "@capacitor/app": "^6.0.0", "@capacitor/cli": "^6.2.0", "@capacitor/core": "^6.2.0", + "@capacitor/filesystem": "^6.0.3", "@capacitor/ios": "^6.2.0", + "@capacitor/share": "^6.0.3", "@dicebear/collection": "^5.4.1", "@dicebear/core": "^5.4.1", + "@electron/remote": "^2.1.2", "@ethersproject/hdnode": "^5.7.0", "@ethersproject/wallet": "^5.8.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", @@ -123,7 +126,7 @@ "@types/js-yaml": "^4.0.9", "@types/leaflet": "^1.9.8", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.11", + "@types/node": "^20.17.30", "@types/node-fetch": "^2.6.12", "@types/ramda": "^0.29.11", "@types/sqlite3": "^3.1.11", diff --git a/src/components/ProfileSection.vue b/src/components/ProfileSection.vue new file mode 100644 index 00000000..d41d498e --- /dev/null +++ b/src/components/ProfileSection.vue @@ -0,0 +1,257 @@ +/** * @file ProfileSection.vue * @description Component for managing user +profile information * @author Matthew Raymer * @version 1.0.0 */ + + + + diff --git a/src/interfaces/identifier.ts b/src/interfaces/identifier.ts new file mode 100644 index 00000000..969a7928 --- /dev/null +++ b/src/interfaces/identifier.ts @@ -0,0 +1,22 @@ +/** + * Interface for a Decentralized Identifier (DID) + * @author Matthew Raymer + */ + +import { KeyMeta } from "../libs/crypto/vc"; + +export interface IKey { + id: string; + type: string; + controller: string; + ethereumAddress: string; + publicKeyHex: string; + privateKeyHex: string; + meta?: KeyMeta; +} + +export interface IIdentifier { + did: string; + keys: IKey[]; + services: any[]; +} diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 10280255..04fb7f85 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -5,3 +5,4 @@ export * from "./limits"; export * from "./records"; export * from "./user"; export * from "./deepLinks"; +export * from "./identifier"; diff --git a/src/libs/crypto/vc/index.ts b/src/libs/crypto/vc/index.ts index a77cd00d..0c4b4c5e 100644 --- a/src/libs/crypto/vc/index.ts +++ b/src/libs/crypto/vc/index.ts @@ -38,6 +38,10 @@ export interface KeyMeta { * The Webauthn credential ID in hex, if this is from a passkey */ passkeyCredIdHex?: string; + /** + * The derivation path for the key + */ + derivationPath?: string; } const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver }); diff --git a/src/services/DatabaseBackupService.ts b/src/services/DatabaseBackupService.ts new file mode 100644 index 00000000..9f92a8b7 --- /dev/null +++ b/src/services/DatabaseBackupService.ts @@ -0,0 +1,26 @@ +/** + * @file DatabaseBackupService.ts + * @description Base service class for handling database backup operations + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { PlatformServiceFactory } from "./PlatformServiceFactory"; + +export class DatabaseBackupService { + protected async handleBackup(): Promise { + throw new Error( + "handleBackup must be implemented by platform-specific service", + ); + } + + public static async createAndShareBackup( + base64Data: string, + arrayBuffer: ArrayBuffer, + blob: Blob, + ): Promise { + const factory = PlatformServiceFactory.getInstance(); + const service = await factory.createDatabaseBackupService(); + await service.handleBackup(base64Data, arrayBuffer, blob); + } +} diff --git a/src/services/PlatformServiceFactory.ts b/src/services/PlatformServiceFactory.ts new file mode 100644 index 00000000..9c0c1646 --- /dev/null +++ b/src/services/PlatformServiceFactory.ts @@ -0,0 +1,40 @@ +/** + * @file PlatformServiceFactory.ts + * @description Factory for creating platform-specific service implementations + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { DatabaseBackupService } from "./DatabaseBackupService"; + +export class PlatformServiceFactory { + private static instance: PlatformServiceFactory; + private platform: string; + + private constructor() { + this.platform = import.meta.env.VITE_PLATFORM || "web"; + } + + public static getInstance(): PlatformServiceFactory { + if (!PlatformServiceFactory.instance) { + PlatformServiceFactory.instance = new PlatformServiceFactory(); + } + return PlatformServiceFactory.instance; + } + + public async createDatabaseBackupService(): Promise { + try { + // Use Vite's dynamic import for platform-specific implementation + const { default: PlatformService } = await import( + `./platforms/${this.platform}/DatabaseBackupService` + ); + return new PlatformService(); + } catch (error) { + console.error( + `Failed to load platform-specific service for ${this.platform}:`, + error, + ); + throw error; + } + } +} diff --git a/src/services/ProfileService.ts b/src/services/ProfileService.ts new file mode 100644 index 00000000..5a74c09b --- /dev/null +++ b/src/services/ProfileService.ts @@ -0,0 +1,105 @@ +/** + * @file ProfileService.ts + * @description Service class for handling user profile operations + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { logger } from "../utils/logger"; +import { getHeaders } from "../libs/endorserServer"; +import type { UserProfile } from "@/types/interfaces"; + +export class ProfileService { + /** + * Saves a user profile to the server + * @param activeDid - The user's active DID + * @param partnerApiServer - The partner API server URL + * @param profile - The profile data to save + * @returns Promise + */ + static async saveProfile( + activeDid: string, + partnerApiServer: string, + profile: Partial, + ): Promise { + try { + const headers = await getHeaders(activeDid); + const response = await fetch( + `${partnerApiServer}/api/partner/userProfile`, + { + method: "POST", + headers, + body: JSON.stringify(profile), + }, + ); + + if (!response.ok) { + throw new Error(`Failed to save profile: ${response.statusText}`); + } + } catch (error) { + logger.error("Error saving profile:", error); + throw error; + } + } + + /** + * Deletes a user profile from the server + * @param activeDid - The user's active DID + * @param partnerApiServer - The partner API server URL + * @returns Promise + */ + static async deleteProfile( + activeDid: string, + partnerApiServer: string, + ): Promise { + try { + const headers = await getHeaders(activeDid); + const response = await fetch( + `${partnerApiServer}/api/partner/userProfile`, + { + method: "DELETE", + headers, + }, + ); + + if (!response.ok) { + throw new Error(`Failed to delete profile: ${response.statusText}`); + } + } catch (error) { + logger.error("Error deleting profile:", error); + throw error; + } + } + + /** + * Loads a user profile from the server + * @param activeDid - The user's active DID + * @param partnerApiServer - The partner API server URL + * @returns Promise + */ + static async loadProfile( + activeDid: string, + partnerApiServer: string, + ): Promise { + try { + const headers = await getHeaders(activeDid); + const response = await fetch( + `${partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`, + { headers }, + ); + + if (response.status === 404) { + return null; + } + + if (!response.ok) { + throw new Error(`Failed to load profile: ${response.statusText}`); + } + + return await response.json(); + } catch (error) { + logger.error("Error loading profile:", error); + throw error; + } + } +} diff --git a/src/services/RateLimitsService.ts b/src/services/RateLimitsService.ts new file mode 100644 index 00000000..1ac5f1b7 --- /dev/null +++ b/src/services/RateLimitsService.ts @@ -0,0 +1,88 @@ +/** + * @file RateLimitsService.ts + * @description Service class for handling rate limit operations + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { logger } from "../utils/logger"; +import { getHeaders } from "../libs/endorserServer"; +import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits"; + +export class RateLimitsService { + /** + * Fetches rate limits for a given DID + * @param apiServer - The API server URL + * @param activeDid - The user's active DID + * @returns Promise + */ + static async fetchRateLimits( + apiServer: string, + activeDid: string, + ): Promise { + try { + const headers = await getHeaders(activeDid); + const response = await fetch( + `${apiServer}/api/endorser/rateLimits/${activeDid}`, + { headers }, + ); + + if (!response.ok) { + throw new Error(`Failed to fetch rate limits: ${response.statusText}`); + } + + return await response.json(); + } catch (error) { + logger.error("Error fetching rate limits:", error); + throw error; + } + } + + /** + * Fetches image rate limits for a given DID + * @param apiServer - The API server URL + * @param activeDid - The user's active DID + * @returns Promise + */ + static async fetchImageRateLimits( + apiServer: string, + activeDid: string, + ): Promise { + try { + const headers = await getHeaders(activeDid); + const response = await fetch( + `${apiServer}/api/endorser/imageRateLimits/${activeDid}`, + { headers }, + ); + + if (!response.ok) { + throw new Error( + `Failed to fetch image rate limits: ${response.statusText}`, + ); + } + + return await response.json(); + } catch (error) { + logger.error("Error fetching image rate limits:", error); + throw error; + } + } + + /** + * Formats rate limit error messages + * @param error - The error object + * @returns string + */ + static formatRateLimitError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === "object" && error !== null) { + const err = error as { + response?: { data?: { error?: { message?: string } } }; + }; + return err.response?.data?.error?.message || "An unknown error occurred"; + } + return "An unknown error occurred"; + } +} diff --git a/src/services/platforms/electron/DatabaseBackupService.ts b/src/services/platforms/electron/DatabaseBackupService.ts new file mode 100644 index 00000000..b812ec9d --- /dev/null +++ b/src/services/platforms/electron/DatabaseBackupService.ts @@ -0,0 +1,18 @@ +import { DatabaseBackupService } from "../../DatabaseBackupService"; +import { dialog } from "electron"; +import * as fs from "fs"; +import * as path from "path"; + +export default class ElectronDatabaseBackupService extends DatabaseBackupService { + protected async handleBackup(base64Data: string): Promise { + const { filePath } = await dialog.showSaveDialog({ + title: "Save Database Backup", + defaultPath: path.join(process.env.HOME || "", "database-backup.json"), + filters: [{ name: "JSON", extensions: ["json"] }], + }); + + if (filePath) { + fs.writeFileSync(filePath, base64Data, "base64"); + } + } +} diff --git a/src/services/platforms/mobile/DatabaseBackupService.ts b/src/services/platforms/mobile/DatabaseBackupService.ts new file mode 100644 index 00000000..6f962ee0 --- /dev/null +++ b/src/services/platforms/mobile/DatabaseBackupService.ts @@ -0,0 +1,31 @@ +/** + * @file DatabaseBackupService.ts + * @description Mobile-specific implementation of the DatabaseBackupService + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { DatabaseBackupService } from "../../DatabaseBackupService"; +import { Filesystem } from "@capacitor/filesystem"; +import { Share } from "@capacitor/share"; + +export default class MobileDatabaseBackupService extends DatabaseBackupService { + protected async handleBackup(base64Data: string): Promise { + // Mobile platform handling + const fileName = `database-backup-${new Date().toISOString()}.json`; + const path = `backups/${fileName}`; + + await Filesystem.writeFile({ + path, + data: base64Data, + directory: "CACHE", + recursive: true, + }); + + await Share.share({ + title: "Database Backup", + text: "Here's your database backup", + url: path, + }); + } +} diff --git a/src/services/platforms/web/DatabaseBackupService.ts b/src/services/platforms/web/DatabaseBackupService.ts new file mode 100644 index 00000000..f0f41942 --- /dev/null +++ b/src/services/platforms/web/DatabaseBackupService.ts @@ -0,0 +1,22 @@ +/** + * @file DatabaseBackupService.ts + * @description Web-specific implementation of the DatabaseBackupService + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { DatabaseBackupService } from "../../DatabaseBackupService"; + +export default class WebDatabaseBackupService extends DatabaseBackupService { + protected async handleBackup(blob: Blob): Promise { + // Web platform handling + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `database-backup-${new Date().toISOString()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } +} diff --git a/src/types/capacitor.d.ts b/src/types/capacitor.d.ts new file mode 100644 index 00000000..2ad13a41 --- /dev/null +++ b/src/types/capacitor.d.ts @@ -0,0 +1,64 @@ +/** + * Type declarations for Capacitor modules used in the application. + * @author Matthew Raymer + */ + +declare module "@capacitor/filesystem" { + export interface FileWriteOptions { + path: string; + data: string; + directory?: string; + encoding?: string; + recursive?: boolean; + } + + export interface FileReadResult { + data: string; + } + + export interface FileDeleteOptions { + path: string; + directory?: string; + } + + export interface FilesystemDirectory { + Cache: "CACHE"; + Documents: "DOCUMENTS"; + Data: "DATA"; + External: "EXTERNAL"; + ExternalStorage: "EXTERNAL_STORAGE"; + } + + export interface Filesystem { + writeFile(options: FileWriteOptions): Promise; + readFile(options: { + path: string; + directory?: string; + }): Promise; + deleteFile(options: FileDeleteOptions): Promise; + } + + export const Filesystem: Filesystem; + export const Directory: FilesystemDirectory; + export const Encoding: { + UTF8: "utf8"; + ASCII: "ascii"; + UTF16: "utf16"; + }; +} + +declare module "@capacitor/share" { + export interface ShareOptions { + title?: string; + text?: string; + url?: string; + dialogTitle?: string; + files?: string[]; + } + + export interface Share { + share(options: ShareOptions): Promise; + } + + export const Share: Share; +} diff --git a/src/types/index.ts b/src/types/index.ts index c6757cdf..c749df9c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,10 @@ +/** + * Index file for all type declarations. + * @author Matthew Raymer + */ + +export * from "./interfaces"; + import { GiveSummaryRecord, GiveVerifiableCredential } from "interfaces"; export interface GiveRecordWithContactInfo extends GiveSummaryRecord { diff --git a/src/types/interfaces.ts b/src/types/interfaces.ts new file mode 100644 index 00000000..c751e7d8 --- /dev/null +++ b/src/types/interfaces.ts @@ -0,0 +1,96 @@ +/** + * Type declarations for custom interfaces used in the application. + * @author Matthew Raymer + */ + +import { GiveVerifiableCredential } from "../interfaces"; + +export interface IIdentifier { + did: string; + provider: string; + keys: Array<{ + kid: string; + kms: string; + type: string; + publicKeyHex: string; + meta?: any; + }>; + services: Array<{ + id: string; + type: string; + serviceEndpoint: string; + description?: string; + }>; +} + +export interface ExportProgress { + status: "preparing" | "exporting" | "complete" | "error"; + message?: string; + error?: Error; +} + +export interface UserProfile { + /** User's profile description */ + description: string; + /** User's location information */ + location?: { + /** Latitude coordinate */ + lat: number; + /** Longitude coordinate */ + lng: number; + }; + /** User's given name */ + givenName?: string; + /** User's family name */ + familyName?: string; +} + +export interface ErrorResponse { + error: string; + message: string; + statusCode: number; +} + +export interface LeafletMouseEvent { + latlng: { + lat: number; + lng: number; + }; +} + +export interface GiveRecordWithContactInfo { + type?: string; + agentDid: string; + amount: number; + amountConfirmed: number; + description: string; + fullClaim: GiveVerifiableCredential; + fulfillsHandleId: string; + fulfillsPlanHandleId?: string; + fulfillsType?: string; + handleId: string; + issuedAt: string; + issuerDid: string; + jwtId: string; + providerPlanHandleId?: string; + recipientDid: string; + unit: string; + giver: { + known: boolean; + displayName: string; + profileImageUrl?: string; + }; + issuer: { + known: boolean; + displayName: string; + profileImageUrl?: string; + }; + receiver: { + known: boolean; + displayName: string; + profileImageUrl?: string; + }; + providerPlanName?: string; + recipientProjectName?: string; + image?: string; +} diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index ef5772fa..072cf6ce 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -57,12 +57,7 @@ > @@ -83,7 +78,7 @@ />
-
+
-
-
- - Loading profile... -
-
- Public Profile - -
- - -
- - -
-
-

- For your security, choose a location nearby but not exactly at your - place. -

- - - - - -
-
-
- - -
-
-
Loading...
-
Saving...
-
+ :active-did="activeDid" + :partner-api-server="partnerApiServer" + @profile-updated="handleProfileUpdate" + />
(); +// Update the error type definitions +interface ErrorDetail { + message?: string; + [key: string]: unknown; +} + +interface ApiErrorResponse { + error: string | ErrorDetail; + [key: string]: unknown; +} + +interface ApiError { + status?: number; + response?: { + data?: ApiErrorResponse; + }; + message?: string; +} + +interface AxiosErrorDetail { + status: number; + response?: { + data?: ApiErrorResponse; + }; + message?: string; +} + @Component({ components: { EntityIcon, ImageMethodDialog, - LeafletMouseEvent, LMap, LMarker, LTileLayer, @@ -999,6 +930,7 @@ const inputImportFileNameRef = ref(); QuickNav, TopMessage, UserNameDialog, + ProfileSection, }, }) export default class AccountViewView extends Vue { @@ -1091,11 +1023,11 @@ export default class AccountViewView extends Vue { this.includeUserProfileLocation = true; } } else { - // won't get here because axios throws an error instead - throw Error("Unable to load profile."); + throw new Error("Unable to load profile."); } - } catch (error) { - if (error.status === 404) { + } catch (error: unknown) { + const typedError = error as { status?: number }; + if (typedError.status === 404) { // this is ok: the profile is not yet created } else { logConsoleAndDb( @@ -1259,7 +1191,7 @@ export default class AccountViewView extends Vue { }); } - readableDate(timeStr: string) { + readableDate(timeStr?: string): string { return timeStr ? timeStr.substring(0, timeStr.indexOf("T")) : "?"; } @@ -1440,109 +1372,75 @@ export default class AccountViewView extends Vue { } /** - * Asynchronously exports the database into a downloadable JSON file. + * Exports the database to a JSON file, handling platform-specific requirements. * - * @throws Will notify the user if there is an export error. - */ - public async exportDatabase() { - try { - // Generate the blob from the database - const blob = await this.generateDatabaseBlob(); - - // Create a temporary URL for the blob - this.downloadUrl = this.createBlobURL(blob); - - // Trigger the download - this.downloadDatabaseBackup(this.downloadUrl); - - // Notify the user that the download has started - this.notifyDownloadStarted(); - - // Revoke the temporary URL -- after a pause to avoid DuckDuckGo download failure - setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); - } catch (error) { - this.handleExportError(error); - } - } - - /** - * Generates a blob object representing the database. + * @internal + * @callGraph + * - Called by: template click handler + * - Calls: Filesystem.writeFile(), Share.share(), URL.createObjectURL() * - * @returns {Promise} The generated blob object. - */ - private async generateDatabaseBlob(): Promise { - return await db.export({ prettyJson: true }); - } - - /** - * Creates a temporary URL for a blob object. + * @chain + * 1. Generate database blob + * 2. Convert to base64 for mobile platforms + * 3. Handle platform-specific export: + * - Mobile: Use Filesystem API and Share API + * - Web: Use URL.createObjectURL and download link + * - Electron: Use dialog and fs * - * @param {Blob} blob - The blob object. - * @returns {string} The temporary URL for the blob. - */ - private createBlobURL(blob: Blob): string { - return URL.createObjectURL(blob); - } - - /** - * Triggers the download of the database backup. + * @requires + * - db: Dexie database instance + * - Filesystem API (for mobile) + * - Share API (for mobile) + * - fs module (for Electron) + * - dialog module (for Electron) * - * @param {string} url - The temporary URL for the blob. + * @modifies + * - downloadLinkRef: Sets href and triggers download + * - State: Updates UI feedback */ - private downloadDatabaseBackup(url: string) { - const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; - downloadAnchor.href = url; - downloadAnchor.download = `${db.name}-backup.json`; - downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo - } - - public computedStartDownloadLinkClassNames() { - return { - hidden: this.downloadUrl, - }; - } + async exportDatabase() { + try { + // Generate database blob + const blob = await (Dexie as any).export(db, { + prettyJson: true, + }); - public computedDownloadLinkClassNames() { - return { - hidden: !this.downloadUrl, - }; - } + // Convert blob to base64 for mobile platforms + const arrayBuffer = await blob.arrayBuffer(); + const base64Data = Buffer.from(arrayBuffer).toString("base64"); - /** - * Notifies the user that the download has started. - */ - private notifyDownloadStarted() { - this.$notify( - { - group: "alert", - type: "success", - title: "Download Started", - text: "See your downloads directory for the backup. It is in the Dexie format.", - }, - -1, - ); - } + await DatabaseBackupService.createAndShareBackup( + base64Data, + arrayBuffer, + blob, + ); - /** - * Handles errors during the database export process. - * - * @param {Error} error - The error object. - */ - private handleExportError(error: unknown) { - logger.error("Export Error:", error); - this.$notify( - { - group: "alert", - type: "danger", - title: "Export Error", - text: "There was an error exporting the data.", - }, - 3000, - ); + this.$notify( + { + group: "alert", + type: "success", + title: "Export Complete", + text: "Your database has been exported successfully.", + }, + 5000, + ); + } catch (error: unknown) { + if (error instanceof Error) { + const errorMessage = error.message; + this.limitsMessage = errorMessage || "Bad server response."; + logger.error("Got bad response retrieving limits:", error); + } else { + this.limitsMessage = "Got an error retrieving limits."; + logger.error("Got some error retrieving limits:", error); + } + } } async uploadImportFile(event: Event) { - inputImportFileNameRef.value = (event.target as EventTarget).files[0]; + const target = event.target as HTMLInputElement; + if (target.files) { + inputImportFileNameRef.value = target.files[0]; + } } showContactImport() { @@ -1614,17 +1512,16 @@ export default class AccountViewView extends Vue { reader.readAsText(inputImportFileNameRef.value as Blob); } - private progressCallback(progress: ImportProgress) { + private progressCallback(progress: DexieExportProgress) { logger.log( - `Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`, + `Export progress: ${progress.completedTables} of ${progress.totalTables} tables completed.`, ); if (progress.done) { - // console.log(`Imported ${progress.completedTables} tables.`); this.$notify( { group: "alert", type: "success", - title: "Import Complete", + title: "Export Complete", text: "", }, 5000, @@ -1654,47 +1551,17 @@ export default class AccountViewView extends Vue { this.limitsMessage = ""; try { - const resp = await fetchEndorserRateLimits( + const response = await RateLimitsService.fetchRateLimits( this.apiServer, - this.axios, did, ); - if (resp.status === 200) { - this.endorserLimits = resp.data; - if (!this.isRegistered) { - // the user was not known to be registered, but now they are (because we got no error) so let's record it - try { - await updateAccountSettings(did, { isRegistered: true }); - this.isRegistered = true; - } catch (err) { - logger.error("Got an error updating settings:", err); - this.$notify( - { - group: "alert", - type: "danger", - title: "Update Error", - text: "Unable to update your settings. Check claim limits again.", - }, - 5000, - ); - } - } - try { - const imageResp = await fetchImageRateLimits(this.axios, did); - if (imageResp.status === 200) { - this.imageLimits = imageResp.data; - } else { - this.limitsMessage = "You don't have access to upload images."; - } - } catch { - this.limitsMessage = "You cannot upload images."; - } - } - } catch (error) { - this.handleRateLimitsError(error); + this.endorserLimits = response; + } catch (error: unknown) { + this.limitsMessage = RateLimitsService.formatRateLimitError(error); + logger.error("Error fetching rate limits:", error); + } finally { + this.loadingLimits = false; } - - this.loadingLimits = false; } /** @@ -1704,25 +1571,61 @@ export default class AccountViewView extends Vue { */ private handleRateLimitsError(error: unknown) { if (error instanceof AxiosError) { - if (error.status == 400 || error.status == 404) { - // no worries: they probably just aren't registered and don't have any limits + const axiosError = error as AxiosErrorDetail; + if (axiosError.status === 400 || axiosError.status === 404) { logger.log( "Got 400 or 404 response retrieving limits which probably means they're not registered:", error, ); this.limitsMessage = "No limits were found, so no actions are allowed."; } else { - const data = error.response?.data as ErrorResponse; - this.limitsMessage = - (data?.error?.message as string) || "Bad server response."; + const data = axiosError.response?.data as ApiErrorResponse; + const errorMessage = + typeof data?.error === "string" + ? data.error + : (data?.error as ErrorDetail)?.message || "Bad server response."; + this.limitsMessage = errorMessage; logger.error("Got bad response retrieving limits:", error); } + } else if (this.isApiError(error)) { + this.limitsMessage = this.getErrorMessage(error); + logger.error("Got API error retrieving limits:", error); } else { this.limitsMessage = "Got an error retrieving limits."; logger.error("Got some error retrieving limits:", error); } } + private isApiError(error: unknown): error is ApiError { + return ( + typeof error === "object" && + error !== null && + "status" in error && + "response" in error + ); + } + + private getErrorMessage(error: ApiError): string { + if (error.response?.data) { + const data = error.response.data; + if (typeof data.error === "string") { + return data.error; + } + return (data.error as ErrorDetail)?.message || "Bad server response."; + } + return error.message || "An unexpected error occurred"; + } + + private handleError(error: unknown): string { + if (this.isApiError(error)) { + return this.getErrorMessage(error); + } + if (error instanceof Error) { + return error.message; + } + return "An unknown error occurred"; + } + async onClickSaveApiServer() { await db.open(); await db.settings.update(MASTER_SETTINGS_KEY, { @@ -1877,50 +1780,28 @@ export default class AccountViewView extends Vue { async saveProfile() { this.savingProfile = true; try { - const headers = await getHeaders(this.activeDid); - const payload: UserProfile = { + await ProfileService.saveProfile(this.activeDid, this.partnerApiServer, { description: this.userProfileDesc, - }; - if (this.userProfileLatitude && this.userProfileLongitude) { - payload.locLat = this.userProfileLatitude; - payload.locLon = this.userProfileLongitude; - } else if (this.includeUserProfileLocation) { - this.$notify( - { - group: "alert", - type: "toast", - title: "", - text: "No profile location is saved.", - }, - 3000, - ); - } - const response = await this.axios.post( - this.partnerApiServer + "/api/partner/userProfile", - payload, - { headers }, + location: this.includeUserProfileLocation + ? { + lat: this.userProfileLatitude, + lng: this.userProfileLongitude, + } + : undefined, + }); + + this.$notify( + { + group: "alert", + type: "success", + title: "Profile Saved", + text: "Your profile has been updated successfully.", + }, + 3000, ); - if (response.status === 201) { - this.$notify( - { - group: "alert", - type: "success", - title: "Profile Saved", - text: "Your profile has been updated successfully.", - }, - 3000, - ); - } else { - // won't get here because axios throws an error on non-success - throw Error("Profile not saved"); - } - } catch (error) { + } catch (error: unknown) { + const errorMessage = this.handleAxiosError(error); logConsoleAndDb("Error saving profile: " + errorStringForLog(error)); - const errorMessage: string = - error.response?.data?.error?.message || - error.response?.data?.error || - error.message || - "There was an error saving your profile."; this.$notify( { group: "alert", @@ -1982,35 +1863,23 @@ export default class AccountViewView extends Vue { async deleteProfile() { this.savingProfile = true; try { - const headers = await getHeaders(this.activeDid); - const response = await this.axios.delete( - this.partnerApiServer + "/api/partner/userProfile", - { headers }, + await ProfileService.deleteProfile(this.activeDid, this.partnerApiServer); + this.userProfileDesc = ""; + this.userProfileLatitude = 0; + this.userProfileLongitude = 0; + this.includeUserProfileLocation = false; + this.$notify( + { + group: "alert", + type: "success", + title: "Profile Deleted", + text: "Your profile has been deleted successfully.", + }, + 3000, ); - if (response.status === 204) { - this.userProfileDesc = ""; - this.userProfileLatitude = 0; - this.userProfileLongitude = 0; - this.includeUserProfileLocation = false; - this.$notify( - { - group: "alert", - type: "success", - title: "Profile Deleted", - text: "Your profile has been deleted successfully.", - }, - 3000, - ); - } else { - throw Error("Profile not deleted"); - } - } catch (error) { + } catch (error: unknown) { + const errorMessage = this.handleAxiosError(error); logConsoleAndDb("Error deleting profile: " + errorStringForLog(error)); - const errorMessage: string = - error.response?.data?.error?.message || - error.response?.data?.error || - error.message || - "There was an error deleting your profile."; this.$notify( { group: "alert", @@ -2024,5 +1893,54 @@ export default class AccountViewView extends Vue { this.savingProfile = false; } } + + notifyDownloadStarted() { + this.$notify( + { + group: "alert", + type: "success", + title: "Download Started", + text: "See your downloads directory for the backup. It is in the Dexie format.", + }, + -1, + ); + } + + showNameDialog() { + (this.$refs.userNameDialog as UserNameDialog).open((name?: string) => { + if (name) { + this.givenName = name; + } + }); + } + + computedStartDownloadLinkClassNames(): string { + return "block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"; + } + + computedDownloadLinkClassNames(): string { + return "block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"; + } + + // Update error handling for type safety + private handleAxiosError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === "object" && error !== null) { + const err = error as { + response?: { data?: { error?: { message?: string } } }; + }; + return err.response?.data?.error?.message || "An unknown error occurred"; + } + return "An unknown error occurred"; + } + + handleProfileUpdate(updatedProfile: UserProfile) { + this.userProfileDesc = updatedProfile.description; + this.userProfileLatitude = updatedProfile.location?.lat || 0; + this.userProfileLongitude = updatedProfile.location?.lng || 0; + this.includeUserProfileLocation = !!updatedProfile.location; + } } diff --git a/src/views/ProjectsView.vue b/src/views/ProjectsView.vue index ddbf0ab3..96371dd2 100644 --- a/src/views/ProjectsView.vue +++ b/src/views/ProjectsView.vue @@ -279,11 +279,7 @@ import ProjectIcon from "../components/ProjectIcon.vue"; import TopMessage from "../components/TopMessage.vue"; import UserNameDialog from "../components/UserNameDialog.vue"; import { Contact } from "../db/tables/contacts"; -import { - didInfo, - getHeaders, - getPlanFromCache, -} from "../libs/endorserServer"; +import { didInfo, getHeaders, getPlanFromCache } from "../libs/endorserServer"; import { OfferSummaryRecord, PlanData } from "../interfaces/records"; import * as libsUtil from "../libs/util"; import { OnboardPage } from "../libs/util"; diff --git a/tsconfig.json b/tsconfig.json index b973a9d6..9b7b3936 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ "@/db/*": ["db/*"], "@/libs/*": ["libs/*"], "@/constants/*": ["constants/*"], - "@/store/*": ["store/*"] + "@/store/*": ["store/*"], + "@/types/*": ["types/*"] }, "lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs }, diff --git a/vite.config.base.ts b/vite.config.base.ts new file mode 100644 index 00000000..c3702bc8 --- /dev/null +++ b/vite.config.base.ts @@ -0,0 +1,46 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import path from "path"; + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + 'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'), + 'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'), + 'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'), + stream: 'stream-browserify', + util: 'util', + crypto: 'crypto-browserify' + }, + mainFields: ['module', 'jsnext:main', 'jsnext', 'main'], + }, + optimizeDeps: { + include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'], + esbuildOptions: { + define: { + global: 'globalThis' + } + } + }, + build: { + sourcemap: true, + target: 'esnext', + chunkSizeWarningLimit: 1000, + commonjsOptions: { + include: [/node_modules/], + transformMixedEsModules: true + }, + rollupOptions: { + external: ['stream', 'util', 'crypto'], + output: { + globals: { + stream: 'stream', + util: 'util', + crypto: 'crypto' + } + } + } + } +}); \ No newline at end of file diff --git a/vite.config.mobile.ts b/vite.config.mobile.ts new file mode 100644 index 00000000..54641d07 --- /dev/null +++ b/vite.config.mobile.ts @@ -0,0 +1,28 @@ +import { defineConfig, loadEnv } from "vite"; +import baseConfig from "./vite.config.base"; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + + return { + ...baseConfig, + define: { + 'import.meta.env.VITE_PLATFORM': JSON.stringify('mobile'), + }, + build: { + ...baseConfig.build, + outDir: 'dist/mobile', + rollupOptions: { + ...baseConfig.build.rollupOptions, + output: { + ...baseConfig.build.rollupOptions.output, + manualChunks: { + // Mobile-specific chunk splitting + vendor: ['vue', 'vue-router', 'pinia'], + capacitor: ['@capacitor/core', '@capacitor/filesystem', '@capacitor/share'], + } + } + } + } + }; +}); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index c3702bc8..0e8918dd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,46 +1,18 @@ -import { defineConfig } from "vite"; -import vue from "@vitejs/plugin-vue"; -import path from "path"; +import { defineConfig, loadEnv } from "vite"; +import baseConfig from "./vite.config.base"; -export default defineConfig({ - plugins: [vue()], - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - 'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'), - 'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'), - 'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'), - stream: 'stream-browserify', - util: 'util', - crypto: 'crypto-browserify' - }, - mainFields: ['module', 'jsnext:main', 'jsnext', 'main'], - }, - optimizeDeps: { - include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'], - esbuildOptions: { - define: { - global: 'globalThis' - } - } - }, - build: { - sourcemap: true, - target: 'esnext', - chunkSizeWarningLimit: 1000, - commonjsOptions: { - include: [/node_modules/], - transformMixedEsModules: true - }, - rollupOptions: { - external: ['stream', 'util', 'crypto'], - output: { - globals: { - stream: 'stream', - util: 'util', - crypto: 'crypto' - } - } - } - } +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => { + // Load env file based on `mode` in the current working directory. + // Set the third parameter to '' to load all env regardless of the `VITE_` prefix. + const env = loadEnv(mode, process.cwd(), ''); + const platform = env.PLATFORM || 'web'; + + // Load platform-specific config + const platformConfig = require(`./vite.config.${platform}`).default; + + return { + ...baseConfig, + ...platformConfig, + }; }); \ No newline at end of file diff --git a/vite.config.web.ts b/vite.config.web.ts new file mode 100644 index 00000000..f7376975 --- /dev/null +++ b/vite.config.web.ts @@ -0,0 +1,27 @@ +import { defineConfig, loadEnv } from "vite"; +import baseConfig from "./vite.config.base"; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + + return { + ...baseConfig, + define: { + 'import.meta.env.VITE_PLATFORM': JSON.stringify('web'), + }, + build: { + ...baseConfig.build, + outDir: 'dist/web', + rollupOptions: { + ...baseConfig.build.rollupOptions, + output: { + ...baseConfig.build.rollupOptions.output, + manualChunks: { + // Web-specific chunk splitting + vendor: ['vue', 'vue-router', 'pinia'], + } + } + } + } + }; +}); \ No newline at end of file