WIP: Platform-specific service architecture and configuration refactor
This commit introduces a platform-specific service architecture and configuration system, with the following changes: - Created platform-specific service implementations for database backup: - Web: Uses URL.createObjectURL and download link - Mobile: Uses Capacitor Filesystem and Share APIs - Base: Abstract DatabaseBackupService class - Implemented PlatformServiceFactory for dynamic service loading: - Singleton pattern for factory management - Dynamic imports based on platform environment - Error handling for service loading failures - Refactored Vite configuration: - Split into base and platform-specific configs - Added environment-based platform detection - Improved build optimization settings - Enhanced error handling: - Added type-safe error interfaces - Improved error message formatting - Better error logging with context - Updated AccountViewView: - Moved profile management to ProfileSection component - Improved type safety in error handling - Enhanced database backup functionality Note: This is a work in progress. Some features may need additional testing and refinement before being production-ready.
This commit is contained in:
1
.env.electron
Normal file
1
.env.electron
Normal file
@@ -0,0 +1 @@
|
||||
PLATFORM=electron
|
||||
1
.env.mobile
Normal file
1
.env.mobile
Normal file
@@ -0,0 +1 @@
|
||||
PLATFORM=mobile
|
||||
86
package-lock.json
generated
86
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
257
src/components/ProfileSection.vue
Normal file
257
src/components/ProfileSection.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
/** * @file ProfileSection.vue * @description Component for managing user
|
||||
profile information * @author Matthew Raymer * @version 1.0.0 */
|
||||
|
||||
<template>
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||
<div v-if="loading" class="text-center mb-2">
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="fa-spin text-slate-400"
|
||||
></font-awesome>
|
||||
Loading profile...
|
||||
</div>
|
||||
<div v-else class="flex items-center mb-2">
|
||||
<span class="font-bold">Public Profile</span>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
||||
@click="showProfileInfo"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="profileDesc"
|
||||
class="w-full h-32 p-2 border border-slate-300 rounded-md"
|
||||
placeholder="Write something about yourself for the public..."
|
||||
:readonly="loading || saving"
|
||||
:class="{ 'bg-slate-100': loading || saving }"
|
||||
></textarea>
|
||||
|
||||
<div class="flex items-center mb-4" @click="toggleLocation">
|
||||
<input v-model="includeLocation" type="checkbox" class="mr-2" />
|
||||
<label for="includeLocation">Include Location</label>
|
||||
</div>
|
||||
<div v-if="includeLocation" class="mb-4 aspect-video">
|
||||
<p class="text-sm mb-2 text-slate-500">
|
||||
For your security, choose a location nearby but not exactly at your
|
||||
place.
|
||||
</p>
|
||||
|
||||
<l-map
|
||||
ref="profileMap"
|
||||
class="!z-40 rounded-md"
|
||||
@click="handleMapClick"
|
||||
@ready="onMapReady"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
layer-type="base"
|
||||
name="OpenStreetMap"
|
||||
/>
|
||||
<l-marker
|
||||
v-if="latitude && longitude"
|
||||
:lat-lng="[latitude, longitude]"
|
||||
@click="confirmEraseLocation"
|
||||
/>
|
||||
</l-map>
|
||||
</div>
|
||||
<div v-if="!loading && !saving">
|
||||
<div class="flex justify-between items-center">
|
||||
<button
|
||||
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||
:disabled="loading || saving"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': loading || saving,
|
||||
}"
|
||||
@click="saveProfile"
|
||||
>
|
||||
Save Profile
|
||||
</button>
|
||||
<button
|
||||
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||
:disabled="loading || saving"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed':
|
||||
loading || saving || (!profileDesc && !includeLocation),
|
||||
}"
|
||||
@click="confirmDeleteProfile"
|
||||
>
|
||||
Delete Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="loading">Loading...</div>
|
||||
<div v-else>Saving...</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||
import { ProfileService } from "../services/ProfileService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
LMap,
|
||||
LMarker,
|
||||
LTileLayer,
|
||||
},
|
||||
})
|
||||
export default class ProfileSection extends Vue {
|
||||
@Prop({ required: true }) activeDid!: string;
|
||||
@Prop({ required: true }) partnerApiServer!: string;
|
||||
|
||||
@Emit("profile-updated") profileUpdated() {}
|
||||
|
||||
loading = true;
|
||||
saving = false;
|
||||
profileDesc = "";
|
||||
latitude = 0;
|
||||
longitude = 0;
|
||||
includeLocation = false;
|
||||
zoom = 2;
|
||||
|
||||
async mounted() {
|
||||
await this.loadProfile();
|
||||
}
|
||||
|
||||
async loadProfile() {
|
||||
try {
|
||||
const profile = await ProfileService.loadProfile(
|
||||
this.activeDid,
|
||||
this.partnerApiServer,
|
||||
);
|
||||
if (profile) {
|
||||
this.profileDesc = profile.description || "";
|
||||
this.latitude = profile.location?.lat || 0;
|
||||
this.longitude = profile.location?.lng || 0;
|
||||
this.includeLocation = !!(this.latitude && this.longitude);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error loading profile:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Loading Profile",
|
||||
text: "Your server profile is not available.",
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async saveProfile() {
|
||||
this.saving = true;
|
||||
try {
|
||||
await ProfileService.saveProfile(this.activeDid, this.partnerApiServer, {
|
||||
description: this.profileDesc,
|
||||
location: this.includeLocation
|
||||
? {
|
||||
lat: this.latitude,
|
||||
lng: this.longitude,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Profile Saved",
|
||||
text: "Your profile has been updated successfully.",
|
||||
});
|
||||
this.profileUpdated();
|
||||
} catch (error) {
|
||||
logger.error("Error saving profile:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Saving Profile",
|
||||
text: "There was an error saving your profile.",
|
||||
});
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
toggleLocation() {
|
||||
this.includeLocation = !this.includeLocation;
|
||||
if (!this.includeLocation) {
|
||||
this.latitude = 0;
|
||||
this.longitude = 0;
|
||||
this.zoom = 2;
|
||||
}
|
||||
}
|
||||
|
||||
handleMapClick(event: { latlng: { lat: number; lng: number } }) {
|
||||
this.latitude = event.latlng.lat;
|
||||
this.longitude = event.latlng.lng;
|
||||
}
|
||||
|
||||
onMapReady(map: L.Map) {
|
||||
const zoom = this.latitude && this.longitude ? 12 : 2;
|
||||
map.setView([this.latitude, this.longitude], zoom);
|
||||
}
|
||||
|
||||
confirmEraseLocation() {
|
||||
this.$notify({
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Erase Marker",
|
||||
text: "Are you sure you don't want to mark a location? This will erase the current location.",
|
||||
onYes: () => {
|
||||
this.latitude = 0;
|
||||
this.longitude = 0;
|
||||
this.zoom = 2;
|
||||
this.includeLocation = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async confirmDeleteProfile() {
|
||||
this.$notify({
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Delete Profile",
|
||||
text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.",
|
||||
onYes: this.deleteProfile,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteProfile() {
|
||||
this.saving = true;
|
||||
try {
|
||||
await ProfileService.deleteProfile(this.activeDid, this.partnerApiServer);
|
||||
this.profileDesc = "";
|
||||
this.latitude = 0;
|
||||
this.longitude = 0;
|
||||
this.includeLocation = false;
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Profile Deleted",
|
||||
text: "Your profile has been deleted successfully.",
|
||||
});
|
||||
this.profileUpdated();
|
||||
} catch (error) {
|
||||
logger.error("Error deleting profile:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Deleting Profile",
|
||||
text: "There was an error deleting your profile.",
|
||||
});
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
showProfileInfo() {
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Public Profile Information",
|
||||
text: "This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.",
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
22
src/interfaces/identifier.ts
Normal file
22
src/interfaces/identifier.ts
Normal file
@@ -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[];
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export * from "./limits";
|
||||
export * from "./records";
|
||||
export * from "./user";
|
||||
export * from "./deepLinks";
|
||||
export * from "./identifier";
|
||||
|
||||
@@ -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 });
|
||||
|
||||
26
src/services/DatabaseBackupService.ts
Normal file
26
src/services/DatabaseBackupService.ts
Normal file
@@ -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<void> {
|
||||
throw new Error(
|
||||
"handleBackup must be implemented by platform-specific service",
|
||||
);
|
||||
}
|
||||
|
||||
public static async createAndShareBackup(
|
||||
base64Data: string,
|
||||
arrayBuffer: ArrayBuffer,
|
||||
blob: Blob,
|
||||
): Promise<void> {
|
||||
const factory = PlatformServiceFactory.getInstance();
|
||||
const service = await factory.createDatabaseBackupService();
|
||||
await service.handleBackup(base64Data, arrayBuffer, blob);
|
||||
}
|
||||
}
|
||||
40
src/services/PlatformServiceFactory.ts
Normal file
40
src/services/PlatformServiceFactory.ts
Normal file
@@ -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<DatabaseBackupService> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/services/ProfileService.ts
Normal file
105
src/services/ProfileService.ts
Normal file
@@ -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<void>
|
||||
*/
|
||||
static async saveProfile(
|
||||
activeDid: string,
|
||||
partnerApiServer: string,
|
||||
profile: Partial<UserProfile>,
|
||||
): Promise<void> {
|
||||
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<void>
|
||||
*/
|
||||
static async deleteProfile(
|
||||
activeDid: string,
|
||||
partnerApiServer: string,
|
||||
): Promise<void> {
|
||||
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<UserProfile | null>
|
||||
*/
|
||||
static async loadProfile(
|
||||
activeDid: string,
|
||||
partnerApiServer: string,
|
||||
): Promise<UserProfile | null> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/services/RateLimitsService.ts
Normal file
88
src/services/RateLimitsService.ts
Normal file
@@ -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<EndorserRateLimits>
|
||||
*/
|
||||
static async fetchRateLimits(
|
||||
apiServer: string,
|
||||
activeDid: string,
|
||||
): Promise<EndorserRateLimits> {
|
||||
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<ImageRateLimits>
|
||||
*/
|
||||
static async fetchImageRateLimits(
|
||||
apiServer: string,
|
||||
activeDid: string,
|
||||
): Promise<ImageRateLimits> {
|
||||
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";
|
||||
}
|
||||
}
|
||||
18
src/services/platforms/electron/DatabaseBackupService.ts
Normal file
18
src/services/platforms/electron/DatabaseBackupService.ts
Normal file
@@ -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<void> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/services/platforms/mobile/DatabaseBackupService.ts
Normal file
31
src/services/platforms/mobile/DatabaseBackupService.ts
Normal file
@@ -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<void> {
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
22
src/services/platforms/web/DatabaseBackupService.ts
Normal file
22
src/services/platforms/web/DatabaseBackupService.ts
Normal file
@@ -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<void> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
64
src/types/capacitor.d.ts
vendored
Normal file
64
src/types/capacitor.d.ts
vendored
Normal file
@@ -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<void>;
|
||||
readFile(options: {
|
||||
path: string;
|
||||
directory?: string;
|
||||
}): Promise<FileReadResult>;
|
||||
deleteFile(options: FileDeleteOptions): Promise<void>;
|
||||
}
|
||||
|
||||
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<void>;
|
||||
}
|
||||
|
||||
export const Share: Share;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
96
src/types/interfaces.ts
Normal file
96
src/types/interfaces.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -57,12 +57,7 @@
|
||||
>
|
||||
<button
|
||||
class="inline-block text-md uppercase 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-4 py-2 rounded-md"
|
||||
@click="
|
||||
() =>
|
||||
($refs.userNameDialog as UserNameDialog).open(
|
||||
(name) => (givenName = name),
|
||||
)
|
||||
"
|
||||
@click="showNameDialog"
|
||||
>
|
||||
Set Your Name
|
||||
</button>
|
||||
@@ -83,7 +78,7 @@
|
||||
/>
|
||||
</span>
|
||||
<div v-else class="text-center">
|
||||
<div class @click="openImageDialog()">
|
||||
<div @click="openImageDialog()">
|
||||
<font-awesome
|
||||
icon="image-portrait"
|
||||
class="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-2 py-2 rounded-l"
|
||||
@@ -266,100 +261,12 @@
|
||||
</div>
|
||||
|
||||
<!-- User Profile -->
|
||||
<div
|
||||
<ProfileSection
|
||||
v-if="isRegistered"
|
||||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||
>
|
||||
<div v-if="loadingProfile" class="text-center mb-2">
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="fa-spin text-slate-400"
|
||||
></font-awesome>
|
||||
Loading profile...
|
||||
</div>
|
||||
<div v-else class="flex items-center mb-2">
|
||||
<span class="font-bold">Public Profile</span>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
||||
@click="showProfileInfo"
|
||||
/>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="userProfileDesc"
|
||||
class="w-full h-32 p-2 border border-slate-300 rounded-md"
|
||||
placeholder="Write something about yourself for the public..."
|
||||
:readonly="loadingProfile || savingProfile"
|
||||
:class="{ 'bg-slate-100': loadingProfile || savingProfile }"
|
||||
></textarea>
|
||||
|
||||
<div class="flex items-center mb-4" @click="toggleUserProfileLocation">
|
||||
<input
|
||||
v-model="includeUserProfileLocation"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
/>
|
||||
<label for="includeUserProfileLocation">Include Location</label>
|
||||
</div>
|
||||
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
|
||||
<p class="text-sm mb-2 text-slate-500">
|
||||
For your security, choose a location nearby but not exactly at your
|
||||
place.
|
||||
</p>
|
||||
|
||||
<l-map
|
||||
ref="profileMap"
|
||||
class="!z-40 rounded-md"
|
||||
@click="
|
||||
(event: LeafletMouseEvent) => {
|
||||
userProfileLatitude = event.latlng.lat;
|
||||
userProfileLongitude = event.latlng.lng;
|
||||
}
|
||||
"
|
||||
@ready="onMapReady"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
layer-type="base"
|
||||
name="OpenStreetMap"
|
||||
/>
|
||||
<l-marker
|
||||
v-if="userProfileLatitude && userProfileLongitude"
|
||||
:lat-lng="[userProfileLatitude, userProfileLongitude]"
|
||||
@click="confirmEraseLatLong()"
|
||||
/>
|
||||
</l-map>
|
||||
</div>
|
||||
<div v-if="!loadingProfile && !savingProfile">
|
||||
<div class="flex justify-between items-center">
|
||||
<button
|
||||
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||
:disabled="loadingProfile || savingProfile"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': loadingProfile || savingProfile,
|
||||
}"
|
||||
@click="saveProfile"
|
||||
>
|
||||
Save Profile
|
||||
</button>
|
||||
<button
|
||||
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||
:disabled="loadingProfile || savingProfile"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed':
|
||||
loadingProfile ||
|
||||
savingProfile ||
|
||||
(!userProfileDesc && !includeUserProfileLocation),
|
||||
}"
|
||||
@click="confirmDeleteProfile"
|
||||
>
|
||||
Delete Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="loadingProfile">Loading...</div>
|
||||
<div v-else>Saving...</div>
|
||||
</div>
|
||||
:active-did="activeDid"
|
||||
:partner-api-server="partnerApiServer"
|
||||
@profile-updated="handleProfileUpdate"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="activeDid"
|
||||
@@ -930,15 +837,20 @@ import { AxiosError } from "axios";
|
||||
import { Buffer } from "buffer/";
|
||||
import Dexie from "dexie";
|
||||
import "dexie-export-import";
|
||||
import { ImportProgress } from "dexie-export-import";
|
||||
import { LeafletMouseEvent } from "leaflet";
|
||||
import * as R from "ramda";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import type { IIdentifier, UserProfile } from "@/types/interfaces";
|
||||
import { ref } from "vue";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||
import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits";
|
||||
import {
|
||||
clearPasskeyToken,
|
||||
errorStringForLog,
|
||||
getHeaders,
|
||||
tokenExpiryTimeDescription,
|
||||
} from "../libs/endorserServer";
|
||||
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
||||
@@ -966,32 +878,51 @@ import {
|
||||
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
||||
MASTER_SETTINGS_KEY,
|
||||
} from "../db/tables/settings";
|
||||
import {
|
||||
clearPasskeyToken,
|
||||
EndorserRateLimits,
|
||||
ErrorResponse,
|
||||
errorStringForLog,
|
||||
fetchEndorserRateLimits,
|
||||
fetchImageRateLimits,
|
||||
getHeaders,
|
||||
ImageRateLimits,
|
||||
tokenExpiryTimeDescription,
|
||||
} from "../libs/endorserServer";
|
||||
import {
|
||||
DAILY_CHECK_TITLE,
|
||||
DIRECT_PUSH_TITLE,
|
||||
retrieveAccountMetadata,
|
||||
} from "../libs/util";
|
||||
import { UserProfile } from "@/libs/partnerServer";
|
||||
import { logger } from "../utils/logger";
|
||||
import { ExportProgress as DexieExportProgress } from "dexie-export-import";
|
||||
import { DatabaseBackupService } from "../services/DatabaseBackupService";
|
||||
import { ProfileService } from "../services/ProfileService";
|
||||
import { RateLimitsService } from "../services/RateLimitsService";
|
||||
import ProfileSection from "../components/ProfileSection.vue";
|
||||
|
||||
const inputImportFileNameRef = ref<Blob>();
|
||||
|
||||
// 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<Blob>();
|
||||
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.
|
||||
* @internal
|
||||
* @callGraph
|
||||
* - Called by: template click handler
|
||||
* - Calls: Filesystem.writeFile(), Share.share(), URL.createObjectURL()
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @requires
|
||||
* - db: Dexie database instance
|
||||
* - Filesystem API (for mobile)
|
||||
* - Share API (for mobile)
|
||||
* - fs module (for Electron)
|
||||
* - dialog module (for Electron)
|
||||
*
|
||||
* @modifies
|
||||
* - downloadLinkRef: Sets href and triggers download
|
||||
* - State: Updates UI feedback
|
||||
*/
|
||||
public async exportDatabase() {
|
||||
async exportDatabase() {
|
||||
try {
|
||||
// Generate the blob from the database
|
||||
const blob = await this.generateDatabaseBlob();
|
||||
// Generate database blob
|
||||
const blob = await (Dexie as any).export(db, {
|
||||
prettyJson: true,
|
||||
});
|
||||
|
||||
// Create a temporary URL for the blob
|
||||
this.downloadUrl = this.createBlobURL(blob);
|
||||
// Convert blob to base64 for mobile platforms
|
||||
const arrayBuffer = await blob.arrayBuffer();
|
||||
const base64Data = Buffer.from(arrayBuffer).toString("base64");
|
||||
|
||||
// Trigger the download
|
||||
this.downloadDatabaseBackup(this.downloadUrl);
|
||||
await DatabaseBackupService.createAndShareBackup(
|
||||
base64Data,
|
||||
arrayBuffer,
|
||||
blob,
|
||||
);
|
||||
|
||||
// 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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a blob object representing the database.
|
||||
*
|
||||
* @returns {Promise<Blob>} The generated blob object.
|
||||
*/
|
||||
private async generateDatabaseBlob(): Promise<Blob> {
|
||||
return await db.export({ prettyJson: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary URL for a blob object.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @param {string} url - The temporary URL for the blob.
|
||||
*/
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
public computedDownloadLinkClassNames() {
|
||||
return {
|
||||
hidden: !this.downloadUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
46
vite.config.base.ts
Normal file
46
vite.config.base.ts
Normal file
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
28
vite.config.mobile.ts
Normal file
28
vite.config.mobile.ts
Normal file
@@ -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'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
27
vite.config.web.ts
Normal file
27
vite.config.web.ts
Normal file
@@ -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'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user