fix: update Vue template syntax and improve Vite config
- Fix Vue template syntax in App.vue by using proper event handler format - Update Vite config to properly handle ESM imports and crypto modules - Add manual chunks for better code splitting - Improve environment variable handling in vite-env.d.ts - Fix TypeScript linting errors in App.vue
This commit is contained in:
32
.eslintrc.js
32
.eslintrc.js
@@ -1,32 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
es2022: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
"plugin:vue/vue3-recommended",
|
|
||||||
"eslint:recommended",
|
|
||||||
"@vue/typescript/recommended",
|
|
||||||
"plugin:prettier/recommended"
|
|
||||||
],
|
|
||||||
// parserOptions: {
|
|
||||||
// ecmaVersion: 2020,
|
|
||||||
// },
|
|
||||||
rules: {
|
|
||||||
"max-len": ["warn", {
|
|
||||||
code: 100,
|
|
||||||
ignoreComments: true,
|
|
||||||
ignorePattern: '^\\s*class="[^"]*"$',
|
|
||||||
ignoreStrings: true,
|
|
||||||
ignoreTemplateLiterals: true,
|
|
||||||
ignoreUrls: true,
|
|
||||||
}],
|
|
||||||
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
|
|
||||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
|
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
"@typescript-eslint/no-unnecessary-type-constraint": "off",
|
|
||||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
|
||||||
},
|
|
||||||
};
|
|
||||||
57
.eslintrc.json
Normal file
57
.eslintrc.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"browser": true,
|
||||||
|
"es2022": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/vue3-recommended",
|
||||||
|
"eslint:recommended",
|
||||||
|
"@vue/typescript/recommended",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
|
],
|
||||||
|
"parser": "vue-eslint-parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"ecmaVersion": 2022,
|
||||||
|
"sourceType": "module",
|
||||||
|
"extraFileExtensions": [".vue"],
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"vue",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-console": "warn",
|
||||||
|
"no-debugger": "warn",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"vue/multi-word-component-names": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||||
|
"@typescript-eslint/no-unnecessary-type-constraint": "off",
|
||||||
|
"vue/no-parsing-error": ["error", {
|
||||||
|
"x-invalid-end-tag": false,
|
||||||
|
"invalid-first-character-of-tag-name": false
|
||||||
|
}],
|
||||||
|
"vue/no-v-html": "warn",
|
||||||
|
"prettier/prettier": ["error", {
|
||||||
|
"singleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.mts"],
|
||||||
|
"parser": "@typescript-eslint/parser"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx", "*.mjs"],
|
||||||
|
"parser": "@typescript-eslint/parser"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
55
.eslintrc.mjs
Normal file
55
.eslintrc.mjs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
export default {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
browser: true,
|
||||||
|
es2022: true
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/vue3-recommended',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/typescript/recommended'
|
||||||
|
],
|
||||||
|
parser: 'vue-eslint-parser',
|
||||||
|
parserOptions: {
|
||||||
|
parser: {
|
||||||
|
'ts': '@typescript-eslint/parser',
|
||||||
|
'js': '@typescript-eslint/parser',
|
||||||
|
'<template>': 'espree'
|
||||||
|
},
|
||||||
|
ecmaVersion: 2022,
|
||||||
|
sourceType: 'module',
|
||||||
|
extraFileExtensions: ['.vue'],
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
'@typescript-eslint',
|
||||||
|
'vue'
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }]
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.ts', '*.tsx', '*.mts'],
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['*.js', '*.jsx', '*.mjs'],
|
||||||
|
parser: '@typescript-eslint/parser'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
node: {
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue', '.mjs', '.mts']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
672
package-lock.json
generated
672
package-lock.json
generated
@@ -45,8 +45,10 @@
|
|||||||
"@zxing/text-encoding": "^0.9.0",
|
"@zxing/text-encoding": "^0.9.0",
|
||||||
"asn1-ber": "^1.2.2",
|
"asn1-ber": "^1.2.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"cbor-x": "^1.5.9",
|
"cbor-x": "^1.5.9",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
|
"crypto-browserify": "^3.12.1",
|
||||||
"dexie": "^3.2.7",
|
"dexie": "^3.2.7",
|
||||||
"dexie-export-import": "^4.1.4",
|
"dexie-export-import": "^4.1.4",
|
||||||
"did-jwt": "^7.4.7",
|
"did-jwt": "^7.4.7",
|
||||||
@@ -63,7 +65,7 @@
|
|||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^10.2.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"merkletreejs": "^0.3.11",
|
"merkletreejs": "^0.3.11",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.12.0",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
@@ -105,21 +107,23 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"electron": "^33.2.1",
|
"electron": "^33.2.1",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.54.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-import-resolver-node": "^0.3.9",
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
"eslint-plugin-prettier": "^5.2.6",
|
||||||
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
|
"espree": "^10.3.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"markdownlint": "^0.37.4",
|
"markdownlint": "^0.37.4",
|
||||||
"markdownlint-cli": "^0.44.0",
|
"markdownlint-cli": "^0.44.0",
|
||||||
"npm-check-updates": "^17.1.13",
|
"npm-check-updates": "^17.1.13",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.5.3",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
@@ -4625,6 +4629,24 @@
|
|||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@eslint/eslintrc/node_modules/espree": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.9.0",
|
||||||
|
"acorn-jsx": "^5.3.2",
|
||||||
|
"eslint-visitor-keys": "^3.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
"node_modules/@eslint/eslintrc/node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -10745,15 +10767,15 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/eslint-config-typescript": {
|
"node_modules/@vue/eslint-config-typescript": {
|
||||||
"version": "11.0.3",
|
"version": "12.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz",
|
||||||
"integrity": "sha512-dkt6W0PX6H/4Xuxg/BlFj5xHvksjpSlVjtkQCpaYJBIEuKj2hOVU7r+TIe+ysCwRYFz/lGqvklntRkCAibsbPw==",
|
"integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||||
"@typescript-eslint/parser": "^5.59.1",
|
"@typescript-eslint/parser": "^6.7.0",
|
||||||
"vue-eslint-parser": "^9.1.1"
|
"vue-eslint-parser": "^9.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^14.17.0 || >=16.0.0"
|
"node": "^14.17.0 || >=16.0.0"
|
||||||
@@ -10769,226 +10791,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/eslint-plugin": {
|
|
||||||
"version": "5.62.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
|
||||||
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@eslint-community/regexpp": "^4.4.0",
|
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
|
||||||
"@typescript-eslint/type-utils": "5.62.0",
|
|
||||||
"@typescript-eslint/utils": "5.62.0",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"graphemer": "^1.4.0",
|
|
||||||
"ignore": "^5.2.0",
|
|
||||||
"natural-compare-lite": "^1.4.0",
|
|
||||||
"semver": "^7.3.7",
|
|
||||||
"tsutils": "^3.21.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
|
||||||
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/parser": {
|
|
||||||
"version": "5.62.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
|
|
||||||
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
|
||||||
"@typescript-eslint/types": "5.62.0",
|
|
||||||
"@typescript-eslint/typescript-estree": "5.62.0",
|
|
||||||
"debug": "^4.3.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/scope-manager": {
|
|
||||||
"version": "5.62.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
|
|
||||||
"integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@typescript-eslint/types": "5.62.0",
|
|
||||||
"@typescript-eslint/visitor-keys": "5.62.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/type-utils": {
|
|
||||||
"version": "5.62.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
|
|
||||||
"integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@typescript-eslint/typescript-estree": "5.62.0",
|
|
||||||
"@typescript-eslint/utils": "5.62.0",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"tsutils": "^3.21.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": "*"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/types": {
|
|
||||||
"version": "5.62.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
|
|
||||||
"integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/typescript-estree": {
|
|
||||||
"version": "5.62.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
|
|
||||||
"integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@typescript-eslint/types": "5.62.0",
|
|
||||||
"@typescript-eslint/visitor-keys": "5.62.0",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"globby": "^11.1.0",
|
|
||||||
"is-glob": "^4.0.3",
|
|
||||||
"semver": "^7.3.7",
|
|
||||||
"tsutils": "^3.21.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/utils": {
|
|
||||||
"version": "5.62.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
|
|
||||||
"integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
|
||||||
"@types/json-schema": "^7.0.9",
|
|
||||||
"@types/semver": "^7.3.12",
|
|
||||||
"@typescript-eslint/scope-manager": "5.62.0",
|
|
||||||
"@typescript-eslint/types": "5.62.0",
|
|
||||||
"@typescript-eslint/typescript-estree": "5.62.0",
|
|
||||||
"eslint-scope": "^5.1.1",
|
|
||||||
"semver": "^7.3.7"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/visitor-keys": {
|
|
||||||
"version": "5.62.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
|
|
||||||
"integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@typescript-eslint/types": "5.62.0",
|
|
||||||
"eslint-visitor-keys": "^3.3.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/eslint-config-typescript/node_modules/eslint-scope": {
|
|
||||||
"version": "5.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
|
||||||
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"esrecurse": "^4.3.0",
|
|
||||||
"estraverse": "^4.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/eslint-config-typescript/node_modules/estraverse": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
||||||
@@ -12558,6 +12360,119 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/browserify-cipher": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"browserify-aes": "^1.0.4",
|
||||||
|
"browserify-des": "^1.0.0",
|
||||||
|
"evp_bytestokey": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserify-des": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cipher-base": "^1.0.1",
|
||||||
|
"des.js": "^1.0.0",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserify-rsa": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bn.js": "^5.2.1",
|
||||||
|
"randombytes": "^2.1.0",
|
||||||
|
"safe-buffer": "^5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserify-sign": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"bn.js": "^5.2.1",
|
||||||
|
"browserify-rsa": "^4.1.0",
|
||||||
|
"create-hash": "^1.2.0",
|
||||||
|
"create-hmac": "^1.1.7",
|
||||||
|
"elliptic": "^6.5.5",
|
||||||
|
"hash-base": "~3.0",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"parse-asn1": "^5.1.7",
|
||||||
|
"readable-stream": "^2.3.8",
|
||||||
|
"safe-buffer": "^5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserify-sign/node_modules/hash-base": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"safe-buffer": "^5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserify-sign/node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/browserify-sign/node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/browserify-sign/node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.4",
|
"version": "4.24.4",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
||||||
@@ -14091,7 +14006,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/cosmiconfig": {
|
"node_modules/cosmiconfig": {
|
||||||
@@ -14253,6 +14167,22 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/create-ecdh": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bn.js": "^4.1.0",
|
||||||
|
"elliptic": "^6.5.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/create-ecdh/node_modules/bn.js": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/create-hash": {
|
"node_modules/create-hash": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||||
@@ -14379,6 +14309,45 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crypto-browserify": {
|
||||||
|
"version": "3.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz",
|
||||||
|
"integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"browserify-cipher": "^1.0.1",
|
||||||
|
"browserify-sign": "^4.2.3",
|
||||||
|
"create-ecdh": "^4.0.4",
|
||||||
|
"create-hash": "^1.2.0",
|
||||||
|
"create-hmac": "^1.1.7",
|
||||||
|
"diffie-hellman": "^5.0.3",
|
||||||
|
"hash-base": "~3.0.4",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"pbkdf2": "^3.1.2",
|
||||||
|
"public-encrypt": "^4.0.3",
|
||||||
|
"randombytes": "^2.1.0",
|
||||||
|
"randomfill": "^1.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/crypto-browserify/node_modules/hash-base": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"safe-buffer": "^5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/crypto-js": {
|
"node_modules/crypto-js": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||||
@@ -14841,6 +14810,16 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/des.js": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"minimalistic-assert": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/destroy": {
|
"node_modules/destroy": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||||
@@ -14981,6 +14960,23 @@
|
|||||||
"node": ">=0.3.1"
|
"node": ">=0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/diffie-hellman": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bn.js": "^4.1.0",
|
||||||
|
"miller-rabin": "^4.0.0",
|
||||||
|
"randombytes": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/diffie-hellman/node_modules/bn.js": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dijkstrajs": {
|
"node_modules/dijkstrajs": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
@@ -15832,6 +15828,28 @@
|
|||||||
"eslint": ">=7.0.0"
|
"eslint": ">=7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint-import-resolver-node": {
|
||||||
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^3.2.7",
|
||||||
|
"is-core-module": "^2.13.0",
|
||||||
|
"resolve": "^1.22.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eslint-import-resolver-node/node_modules/debug": {
|
||||||
|
"version": "3.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||||
|
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint-plugin-prettier": {
|
"node_modules/eslint-plugin-prettier": {
|
||||||
"version": "5.2.6",
|
"version": "5.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
|
||||||
@@ -15927,6 +15945,24 @@
|
|||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eslint/node_modules/espree": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.9.0",
|
||||||
|
"acorn-jsx": "^5.3.2",
|
||||||
|
"eslint-visitor-keys": "^3.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/eslint/node_modules/minimatch": {
|
"node_modules/eslint/node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@@ -15951,18 +15987,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/espree": {
|
"node_modules/espree": {
|
||||||
"version": "9.6.1",
|
"version": "10.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
||||||
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
|
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.9.0",
|
"acorn": "^8.14.0",
|
||||||
"acorn-jsx": "^5.3.2",
|
"acorn-jsx": "^5.3.2",
|
||||||
"eslint-visitor-keys": "^3.4.1"
|
"eslint-visitor-keys": "^4.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/espree/node_modules/eslint-visitor-keys": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://opencollective.com/eslint"
|
"url": "https://opencollective.com/eslint"
|
||||||
@@ -22576,6 +22625,25 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/miller-rabin": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bn.js": "^4.0.0",
|
||||||
|
"brorand": "^1.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"miller-rabin": "bin/miller-rabin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/miller-rabin/node_modules/bn.js": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mime": {
|
"node_modules/mime": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||||
@@ -23019,13 +23087,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/natural-compare-lite": {
|
|
||||||
"version": "1.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
|
|
||||||
"integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "0.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
@@ -23924,6 +23985,53 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-asn1": {
|
||||||
|
"version": "5.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz",
|
||||||
|
"integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"asn1.js": "^4.10.1",
|
||||||
|
"browserify-aes": "^1.2.0",
|
||||||
|
"evp_bytestokey": "^1.0.3",
|
||||||
|
"hash-base": "~3.0",
|
||||||
|
"pbkdf2": "^3.1.2",
|
||||||
|
"safe-buffer": "^5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse-asn1/node_modules/asn1.js": {
|
||||||
|
"version": "4.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
||||||
|
"integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bn.js": "^4.0.0",
|
||||||
|
"inherits": "^2.0.1",
|
||||||
|
"minimalistic-assert": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parse-asn1/node_modules/bn.js": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/parse-asn1/node_modules/hash-base": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"safe-buffer": "^5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parse-entities": {
|
"node_modules/parse-entities": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
|
||||||
@@ -24619,7 +24727,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/progress": {
|
"node_modules/progress": {
|
||||||
@@ -24718,6 +24825,26 @@
|
|||||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/public-encrypt": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bn.js": "^4.1.0",
|
||||||
|
"browserify-rsa": "^4.0.0",
|
||||||
|
"create-hash": "^1.1.0",
|
||||||
|
"parse-asn1": "^5.0.0",
|
||||||
|
"randombytes": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/public-encrypt/node_modules/bn.js": {
|
||||||
|
"version": "4.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz",
|
||||||
|
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pump": {
|
"node_modules/pump": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||||
@@ -25073,6 +25200,16 @@
|
|||||||
"safe-buffer": "^5.1.0"
|
"safe-buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/randomfill": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"randombytes": "^2.0.5",
|
||||||
|
"safe-buffer": "^5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/range-parser": {
|
"node_modules/range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
@@ -29109,29 +29246,6 @@
|
|||||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tsutils": {
|
|
||||||
"version": "3.21.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
|
|
||||||
"integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^1.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 6"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsutils/node_modules/tslib": {
|
|
||||||
"version": "1.14.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD"
|
|
||||||
},
|
|
||||||
"node_modules/tunnel-agent": {
|
"node_modules/tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
@@ -30096,6 +30210,24 @@
|
|||||||
"eslint": ">=6.0.0"
|
"eslint": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-eslint-parser/node_modules/espree": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.9.0",
|
||||||
|
"acorn-jsx": "^5.3.2",
|
||||||
|
"eslint-visitor-keys": "^3.4.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/eslint"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-facing-decorator": {
|
"node_modules/vue-facing-decorator": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/vue-facing-decorator/-/vue-facing-decorator-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/vue-facing-decorator/-/vue-facing-decorator-3.0.4.tgz",
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -2,18 +2,20 @@
|
|||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
|
"type": "module",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
"dev:web": "vite --config vite.config.web.mts",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:mobile": "VITE_PLATFORM=capacitor vite build",
|
"build:mobile": "VITE_PLATFORM=capacitor vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"lint": "NODE_OPTIONS='--experimental-vm-modules --experimental-specifier-resolution=node' eslint --ext .vue,.js,.jsx,.mjs,.ts,.tsx,.mts --fix --ignore-path .gitignore",
|
||||||
"format": "prettier --write src/",
|
"format": "prettier --write src/",
|
||||||
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
"lint-fix": "NODE_OPTIONS='--experimental-vm-modules --experimental-specifier-resolution=node' eslint \"src/**/*.{vue,js,jsx,ts,tsx,mjs,mts}\" --fix",
|
||||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js",
|
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js",
|
||||||
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
|
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
|
||||||
"test:prerequisites": "node scripts/check-prerequisites.js",
|
"test:prerequisites": "node scripts/check-prerequisites.js",
|
||||||
@@ -84,8 +86,10 @@
|
|||||||
"@zxing/text-encoding": "^0.9.0",
|
"@zxing/text-encoding": "^0.9.0",
|
||||||
"asn1-ber": "^1.2.2",
|
"asn1-ber": "^1.2.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"cbor-x": "^1.5.9",
|
"cbor-x": "^1.5.9",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
|
"crypto-browserify": "^3.12.1",
|
||||||
"dexie": "^3.2.7",
|
"dexie": "^3.2.7",
|
||||||
"dexie-export-import": "^4.1.4",
|
"dexie-export-import": "^4.1.4",
|
||||||
"did-jwt": "^7.4.7",
|
"did-jwt": "^7.4.7",
|
||||||
@@ -102,7 +106,7 @@
|
|||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^10.2.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"merkletreejs": "^0.3.11",
|
"merkletreejs": "^0.3.11",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.12.0",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
@@ -144,21 +148,23 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"electron": "^33.2.1",
|
"electron": "^33.2.1",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.54.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-import-resolver-node": "^0.3.9",
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
"eslint-plugin-prettier": "^5.2.6",
|
||||||
|
"eslint-plugin-vue": "^9.33.0",
|
||||||
|
"espree": "^10.3.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
"markdownlint": "^0.37.4",
|
"markdownlint": "^0.37.4",
|
||||||
"markdownlint-cli": "^0.44.0",
|
"markdownlint-cli": "^0.44.0",
|
||||||
"npm-check-updates": "^17.1.13",
|
"npm-check-updates": "^17.1.13",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.5.3",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
|
|||||||
266
src/App.vue
266
src/App.vue
@@ -29,7 +29,9 @@
|
|||||||
>
|
>
|
||||||
<div class="w-full px-4 py-3">
|
<div class="w-full px-4 py-3">
|
||||||
<span class="font-semibold">{{ notification.title }}</span>
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
<p class="text-sm">{{ notification.text }}</p>
|
<p class="text-sm">
|
||||||
|
{{ notification.text }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -40,21 +42,20 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
|
class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome icon="circle-info" class="fa-fw fa-xl" />
|
||||||
icon="circle-info"
|
|
||||||
class="fa-fw fa-xl"
|
|
||||||
></font-awesome>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
|
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
|
||||||
<span class="font-semibold">{{ notification.title }}</span>
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
<p class="text-sm">
|
||||||
|
{{ truncateLongWords(notification.text) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
|
||||||
@click="close(notification.id)"
|
@click="close(notification.id)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
<font-awesome icon="xmark" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,21 +67,20 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
|
class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome icon="circle-info" class="fa-fw fa-xl" />
|
||||||
icon="circle-info"
|
|
||||||
class="fa-fw fa-xl"
|
|
||||||
></font-awesome>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
|
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
|
||||||
<span class="font-semibold">{{ notification.title }}</span>
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
<p class="text-sm">
|
||||||
|
{{ truncateLongWords(notification.text) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
|
||||||
@click="close(notification.id)"
|
@click="close(notification.id)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
<font-awesome icon="xmark" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,21 +92,20 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
|
class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome icon="triangle-exclamation" class="fa-fw fa-xl" />
|
||||||
icon="triangle-exclamation"
|
|
||||||
class="fa-fw fa-xl"
|
|
||||||
></font-awesome>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
|
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
|
||||||
<span class="font-semibold">{{ notification.title }}</span>
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
<p class="text-sm">
|
||||||
|
{{ truncateLongWords(notification.text) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
|
||||||
@click="close(notification.id)"
|
@click="close(notification.id)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
<font-awesome icon="xmark" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,21 +117,20 @@
|
|||||||
<div
|
<div
|
||||||
class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
|
class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome icon="triangle-exclamation" class="fa-fw fa-xl" />
|
||||||
icon="triangle-exclamation"
|
|
||||||
class="fa-fw fa-xl"
|
|
||||||
></font-awesome>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
|
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
|
||||||
<span class="font-semibold">{{ notification.title }}</span>
|
<span class="font-semibold">{{ notification.title }}</span>
|
||||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
<p class="text-sm">
|
||||||
|
{{ truncateLongWords(notification.text) }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
|
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
|
||||||
@click="close(notification.id)"
|
@click="close(notification.id)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
<font-awesome icon="xmark" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,31 +181,33 @@
|
|||||||
<span class="font-semibold text-lg">
|
<span class="font-semibold text-lg">
|
||||||
{{ notification.title }}
|
{{ notification.title }}
|
||||||
</span>
|
</span>
|
||||||
<p class="text-sm mb-2">{{ notification.text }}</p>
|
<p class="text-sm mb-2">
|
||||||
|
{{ notification.text }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="notification.onYes"
|
v-if="notification.onYes"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
@click="
|
@click="{
|
||||||
notification.onYes();
|
notification.onYes();
|
||||||
close(notification.id);
|
close(notification.id);
|
||||||
"
|
}"
|
||||||
>
|
>
|
||||||
Yes{{
|
Yes{{
|
||||||
notification.yesText ? ", " + notification.yesText : ""
|
notification.yesText ? ', ' + notification.yesText : ''
|
||||||
}}
|
}}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="notification.onNo"
|
v-if="notification.onNo"
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
@click="
|
@click="{
|
||||||
notification.onNo(stopAsking);
|
notification.onNo(stopAsking);
|
||||||
close(notification.id);
|
close(notification.id);
|
||||||
stopAsking = false; // reset value
|
stopAsking = false; // reset value
|
||||||
"
|
}"
|
||||||
>
|
>
|
||||||
No{{ notification.noText ? ", " + notification.noText : "" }}
|
No{{ notification.noText ? ', ' + notification.noText : '' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
@@ -228,25 +228,25 @@
|
|||||||
class="sr-only"
|
class="sr-only"
|
||||||
/>
|
/>
|
||||||
<!-- line -->
|
<!-- line -->
|
||||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
|
||||||
<!-- dot -->
|
<!-- dot -->
|
||||||
<div
|
<div
|
||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||||
@click="
|
@click="{
|
||||||
notification.onCancel
|
notification.onCancel
|
||||||
? notification.onCancel(stopAsking)
|
? notification.onCancel(stopAsking)
|
||||||
: null;
|
: null;
|
||||||
close(notification.id);
|
close(notification.id);
|
||||||
stopAsking = false; // reset value for next time they open this modal
|
stopAsking = false; // reset value for next time they open this modal
|
||||||
"
|
}"
|
||||||
>
|
>
|
||||||
{{ notification.onYes ? "Cancel" : "Close" }}
|
{{ notification.onYes ? 'Cancel' : 'Close' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,10 +306,10 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
|
||||||
@click="
|
@click="{
|
||||||
close(notification.id);
|
close(notification.id);
|
||||||
turnOffNotifications(notification);
|
turnOffNotifications(notification);
|
||||||
"
|
}"
|
||||||
>
|
>
|
||||||
Turn Off Notification
|
Turn Off Notification
|
||||||
</button>
|
</button>
|
||||||
@@ -329,21 +329,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component } from "vue-facing-decorator";
|
import { Vue, Component } from 'vue-facing-decorator'
|
||||||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
|
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from './db/index'
|
||||||
import { NotificationIface } from "./constants/app";
|
import { NotificationIface } from './constants/app'
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from './utils/logger'
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
notifyingNewActivityTime?: string;
|
notifyingNewActivityTime?: string
|
||||||
notifyingReminderTime?: string;
|
notifyingReminderTime?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class App extends Vue {
|
export default class App extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
stopAsking = false;
|
stopAsking = false
|
||||||
|
|
||||||
// created() {
|
// created() {
|
||||||
// logger.log(
|
// logger.log(
|
||||||
@@ -382,158 +382,158 @@ export default class App extends Vue {
|
|||||||
|
|
||||||
truncateLongWords(sentence: string) {
|
truncateLongWords(sentence: string) {
|
||||||
return sentence
|
return sentence
|
||||||
.split(" ")
|
.split(' ')
|
||||||
.map((word) => (word.length > 30 ? word.slice(0, 30) + "..." : word))
|
.map((word) => (word.length > 30 ? word.slice(0, 30) + '...' : word))
|
||||||
.join(" ");
|
.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
async turnOffNotifications(
|
async turnOffNotifications(
|
||||||
notification: NotificationIface,
|
notification: NotificationIface
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
logger.log("Starting turnOffNotifications...");
|
logger.log('Starting turnOffNotifications...')
|
||||||
let subscription: PushSubscriptionJSON | null = null;
|
let subscription: PushSubscriptionJSON | null = null
|
||||||
let allGoingOff = false;
|
let allGoingOff = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.log("Retrieving settings for the active account...");
|
logger.log('Retrieving settings for the active account...')
|
||||||
const settings: Settings = await retrieveSettingsForActiveAccount();
|
const settings: Settings = await retrieveSettingsForActiveAccount()
|
||||||
logger.log("Retrieved settings:", settings);
|
logger.log('Retrieved settings:', settings)
|
||||||
|
|
||||||
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
const notifyingNewActivity = !!settings?.notifyingNewActivityTime
|
||||||
const notifyingReminder = !!settings?.notifyingReminderTime;
|
const notifyingReminder = !!settings?.notifyingReminderTime
|
||||||
|
|
||||||
if (!notifyingNewActivity || !notifyingReminder) {
|
if (!notifyingNewActivity || !notifyingReminder) {
|
||||||
allGoingOff = true;
|
allGoingOff = true
|
||||||
logger.log("Both notifications are being turned off.");
|
logger.log('Both notifications are being turned off.')
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("Checking service worker readiness...");
|
logger.log('Checking service worker readiness...')
|
||||||
await navigator.serviceWorker?.ready
|
await navigator.serviceWorker?.ready
|
||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
logger.log("Service worker is ready. Fetching subscription...");
|
logger.log('Service worker is ready. Fetching subscription...')
|
||||||
return registration.pushManager.getSubscription();
|
return registration.pushManager.getSubscription()
|
||||||
})
|
})
|
||||||
.then(async (subscript: PushSubscription | null) => {
|
.then(async (subscript: PushSubscription | null) => {
|
||||||
if (subscript) {
|
if (subscript) {
|
||||||
subscription = subscript.toJSON();
|
subscription = subscript.toJSON()
|
||||||
logger.log("PushSubscription retrieved:", subscription);
|
logger.log('PushSubscription retrieved:', subscription)
|
||||||
|
|
||||||
if (allGoingOff) {
|
if (allGoingOff) {
|
||||||
logger.log("Unsubscribing from push notifications...");
|
logger.log('Unsubscribing from push notifications...')
|
||||||
await subscript.unsubscribe();
|
await subscript.unsubscribe()
|
||||||
logger.log("Successfully unsubscribed.");
|
logger.log('Successfully unsubscribed.')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logConsoleAndDb("Subscription object is not available.");
|
logConsoleAndDb('Subscription object is not available.')
|
||||||
logger.log("No subscription found.");
|
logger.log('No subscription found.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Push provider server communication failed: " +
|
'Push provider server communication failed: ' +
|
||||||
JSON.stringify(error),
|
JSON.stringify(error),
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
logger.error("Error during subscription fetch:", error);
|
logger.error('Error during subscription fetch:', error)
|
||||||
});
|
})
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
logger.log("No subscription available. Notifying user...");
|
logger.log('No subscription available. Notifying user...')
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Finished",
|
title: 'Finished',
|
||||||
text: "Notifications are off.",
|
text: 'Notifications are off.'
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
logger.log("Exiting as there is no subscription to process.");
|
logger.log('Exiting as there is no subscription to process.')
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverSubscription = {
|
const serverSubscription = {
|
||||||
...subscription,
|
...subscription
|
||||||
};
|
}
|
||||||
if (!allGoingOff) {
|
if (!allGoingOff) {
|
||||||
serverSubscription["notifyType"] = notification.title;
|
serverSubscription['notifyType'] = notification.title
|
||||||
logger.log(
|
logger.log(
|
||||||
`Server subscription updated with notifyType: ${notification.title}`,
|
`Server subscription updated with notifyType: ${notification.title}`
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("Sending unsubscribe request to the server...");
|
logger.log('Sending unsubscribe request to the server...')
|
||||||
const pushServerSuccess = await fetch("/web-push/unsubscribe", {
|
const pushServerSuccess = await fetch('/web-push/unsubscribe', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(serverSubscription),
|
body: JSON.stringify(serverSubscription)
|
||||||
})
|
})
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorBody = await response.text();
|
const errorBody = await response.text()
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`Push server failed: ${response.status} ${errorBody}`,
|
`Push server failed: ${response.status} ${errorBody}`,
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
logger.error("Push server error response:", errorBody);
|
logger.error('Push server error response:', errorBody)
|
||||||
}
|
}
|
||||||
logger.log(`Server response status: ${response.status}`);
|
logger.log(`Server response status: ${response.status}`)
|
||||||
return response.ok;
|
return response.ok
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Push server communication failed: " + JSON.stringify(error),
|
'Push server communication failed: ' + JSON.stringify(error),
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
logger.error("Error during server communication:", error);
|
logger.error('Error during server communication:', error)
|
||||||
return false;
|
return false
|
||||||
});
|
})
|
||||||
|
|
||||||
const message = pushServerSuccess
|
const message = pushServerSuccess
|
||||||
? "Notification is off."
|
? 'Notification is off.'
|
||||||
: "Notification is still on. Try to turn it off again.";
|
: 'Notification is still on. Try to turn it off again.'
|
||||||
logger.log("Server response processed. Message:", message);
|
logger.log('Server response processed. Message:', message)
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Finished",
|
title: 'Finished',
|
||||||
text: message,
|
text: message
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
|
|
||||||
if (notification.callback) {
|
if (notification.callback) {
|
||||||
logger.log("Executing notification callback...");
|
logger.log('Executing notification callback...')
|
||||||
notification.callback(pushServerSuccess);
|
notification.callback(pushServerSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
"Completed turnOffNotifications with success:",
|
'Completed turnOffNotifications with success:',
|
||||||
pushServerSuccess,
|
pushServerSuccess
|
||||||
);
|
)
|
||||||
return pushServerSuccess;
|
return pushServerSuccess
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Error turning off notifications: " + JSON.stringify(error),
|
'Error turning off notifications: ' + JSON.stringify(error),
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
logger.error("Critical error in turnOffNotifications:", error);
|
logger.error('Critical error in turnOffNotifications:', error)
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "error",
|
type: 'error',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: "Failed to turn off notifications. Please try again.",
|
text: 'Failed to turn off notifications. Please try again.'
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
|
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-semibold">
|
<h3 class="font-semibold">
|
||||||
{{ record.issuer.known ? record.issuer.displayName : "" }}
|
{{ record.issuer.known ? record.issuer.displayName : '' }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="ms-auto text-xs text-slate-500 italic">
|
<p class="ms-auto text-xs text-slate-500 italic">
|
||||||
{{ friendlyDate }}
|
{{ friendlyDate }}
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="shrink-0 w-0 h-0 border border-slate-300 border-t-[20px] sm:border-t-[25px] border-t-transparent border-b-[20px] sm:border-b-[25px] border-b-transparent border-s-[27px] sm:border-s-[34px] border-e-0"
|
class="shrink-0 w-0 h-0 border border-slate-300 border-t-[20px] sm:border-t-[25px] border-t-transparent border-b-[20px] sm:border-b-[25px] border-b-transparent border-s-[27px] sm:border-s-[34px] border-e-0"
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,59 +182,59 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
import { Component, Prop, Vue } from 'vue-facing-decorator'
|
||||||
import { GiveRecordWithContactInfo } from "../types";
|
import { GiveRecordWithContactInfo } from '../types'
|
||||||
import EntityIcon from "./EntityIcon.vue";
|
import EntityIcon from './EntityIcon.vue'
|
||||||
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
|
import { isGiveClaimType, notifyWhyCannotConfirm } from '../libs/util'
|
||||||
import { containsHiddenDid } from "../libs/endorserServer";
|
import { containsHiddenDid } from '../libs/endorserServer'
|
||||||
import ProjectIcon from "./ProjectIcon.vue";
|
import ProjectIcon from './ProjectIcon.vue'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
ProjectIcon,
|
ProjectIcon
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
export default class ActivityListItem extends Vue {
|
export default class ActivityListItem extends Vue {
|
||||||
@Prop() record!: GiveRecordWithContactInfo;
|
@Prop() record!: GiveRecordWithContactInfo
|
||||||
@Prop() lastViewedClaimId?: string;
|
@Prop() lastViewedClaimId?: string
|
||||||
@Prop() isRegistered!: boolean;
|
@Prop() isRegistered!: boolean
|
||||||
@Prop() activeDid!: string;
|
@Prop() activeDid!: string
|
||||||
@Prop() confirmerIdList?: string[];
|
@Prop() confirmerIdList?: string[]
|
||||||
|
|
||||||
get fetchAmount(): string {
|
get fetchAmount(): string {
|
||||||
const claim =
|
const claim =
|
||||||
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
|
(this.record.fullClaim as unknown).claim || this.record.fullClaim
|
||||||
|
|
||||||
const amount = claim.object?.amountOfThisGood
|
const amount = claim.object?.amountOfThisGood
|
||||||
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||||
: "";
|
: ''
|
||||||
|
|
||||||
return amount;
|
return amount
|
||||||
}
|
}
|
||||||
|
|
||||||
get description(): string {
|
get description(): string {
|
||||||
const claim =
|
const claim =
|
||||||
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
|
(this.record.fullClaim as unknown).claim || this.record.fullClaim
|
||||||
|
|
||||||
return `${claim.description}`;
|
return `${claim.description}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private displayAmount(code: string, amt: number) {
|
private displayAmount(code: string, amt: number) {
|
||||||
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
|
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private currencyShortWordForCode(unitCode: string, single: boolean) {
|
private currencyShortWordForCode(unitCode: string, single: boolean) {
|
||||||
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
return unitCode === 'HUR' ? (single ? 'hour' : 'hours') : unitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
get canConfirm(): boolean {
|
get canConfirm(): boolean {
|
||||||
if (!this.isRegistered) return false;
|
if (!this.isRegistered) return false
|
||||||
if (!isGiveClaimType(this.record.fullClaim?.["@type"])) return false;
|
if (!isGiveClaimType(this.record.fullClaim?.['@type'])) return false
|
||||||
if (this.confirmerIdList?.includes(this.activeDid)) return false;
|
if (this.confirmerIdList?.includes(this.activeDid)) return false
|
||||||
if (this.record.issuerDid === this.activeDid) return false;
|
if (this.record.issuerDid === this.activeDid) return false
|
||||||
if (containsHiddenDid(this.record.fullClaim)) return false;
|
if (containsHiddenDid(this.record.fullClaim)) return false
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConfirmClick() {
|
handleConfirmClick() {
|
||||||
@@ -242,24 +242,24 @@ export default class ActivityListItem extends Vue {
|
|||||||
notifyWhyCannotConfirm(
|
notifyWhyCannotConfirm(
|
||||||
this.$notify,
|
this.$notify,
|
||||||
this.isRegistered,
|
this.isRegistered,
|
||||||
this.record.fullClaim?.["@type"],
|
this.record.fullClaim?.['@type'],
|
||||||
this.record,
|
this.record,
|
||||||
this.activeDid,
|
this.activeDid,
|
||||||
this.confirmerIdList,
|
this.confirmerIdList
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$emit("confirmClaim", this.record);
|
this.$emit('confirmClaim', this.record)
|
||||||
}
|
}
|
||||||
|
|
||||||
get friendlyDate(): string {
|
get friendlyDate(): string {
|
||||||
const date = new Date(this.record.issuedAt);
|
const date = new Date(this.record.issuedAt)
|
||||||
return date.toLocaleDateString(undefined, {
|
return date.toLocaleDateString(undefined, {
|
||||||
year: "numeric",
|
year: 'numeric',
|
||||||
month: "short",
|
month: 'short',
|
||||||
day: "numeric",
|
day: 'numeric'
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -26,7 +26,9 @@
|
|||||||
>
|
>
|
||||||
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||||
<span class="font-semibold text-lg">{{ title }}</span>
|
<span class="font-semibold text-lg">{{ title }}</span>
|
||||||
<p class="text-sm mb-2">{{ text }}</p>
|
<p class="text-sm mb-2">
|
||||||
|
{{ text }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="block w-full text-center text-md font-bold capitalize bg-blue-800 text-white px-2 py-2 rounded-md mb-2"
|
class="block w-full text-center text-md font-bold capitalize bg-blue-800 text-white px-2 py-2 rounded-md mb-2"
|
||||||
@@ -65,48 +67,48 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from 'vue-facing-decorator'
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../constants/app'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class PromptDialog extends Vue {
|
export default class PromptDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
title = "";
|
title = ''
|
||||||
text = "";
|
text = ''
|
||||||
option1Text = "";
|
option1Text = ''
|
||||||
option2Text = "";
|
option2Text = ''
|
||||||
option3Text = "";
|
option3Text = ''
|
||||||
onOption1?: () => void;
|
onOption1?: () => void
|
||||||
onOption2?: () => void;
|
onOption2?: () => void
|
||||||
onOption3?: () => void;
|
onOption3?: () => void
|
||||||
onCancel?: () => Promise<void>;
|
onCancel?: () => Promise<void>
|
||||||
|
|
||||||
open(options: {
|
open(options: {
|
||||||
title: string;
|
title: string
|
||||||
text: string;
|
text: string
|
||||||
option1Text?: string;
|
option1Text?: string
|
||||||
option2Text?: string;
|
option2Text?: string
|
||||||
option3Text?: string;
|
option3Text?: string
|
||||||
onOption1?: () => void;
|
onOption1?: () => void
|
||||||
onOption2?: () => void;
|
onOption2?: () => void
|
||||||
onOption3?: () => void;
|
onOption3?: () => void
|
||||||
onCancel?: () => Promise<void>;
|
onCancel?: () => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
this.title = options.title;
|
this.title = options.title
|
||||||
this.text = options.text;
|
this.text = options.text
|
||||||
this.option1Text = options.option1Text || "";
|
this.option1Text = options.option1Text || ''
|
||||||
this.option2Text = options.option2Text || "";
|
this.option2Text = options.option2Text || ''
|
||||||
this.option3Text = options.option3Text || "";
|
this.option3Text = options.option3Text || ''
|
||||||
this.onOption1 = options.onOption1;
|
this.onOption1 = options.onOption1
|
||||||
this.onOption2 = options.onOption2;
|
this.onOption2 = options.onOption2
|
||||||
this.onOption3 = options.onOption3;
|
this.onOption3 = options.onOption3
|
||||||
this.onCancel = options.onCancel;
|
this.onCancel = options.onCancel
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "customModal",
|
group: 'customModal',
|
||||||
type: "confirm",
|
type: 'confirm',
|
||||||
title: this.title,
|
title: this.title,
|
||||||
text: this.text,
|
text: this.text,
|
||||||
option1Text: this.option1Text,
|
option1Text: this.option1Text,
|
||||||
@@ -115,38 +117,38 @@ export default class PromptDialog extends Vue {
|
|||||||
onOption1: this.onOption1,
|
onOption1: this.onOption1,
|
||||||
onOption2: this.onOption2,
|
onOption2: this.onOption2,
|
||||||
onOption3: this.onOption3,
|
onOption3: this.onOption3,
|
||||||
onCancel: this.onCancel,
|
onCancel: this.onCancel
|
||||||
} as NotificationIface,
|
} as NotificationIface,
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOption1(close: (id: string) => void) {
|
handleOption1(close: (id: string) => void) {
|
||||||
if (this.onOption1) {
|
if (this.onOption1) {
|
||||||
this.onOption1();
|
this.onOption1()
|
||||||
}
|
}
|
||||||
close("string that does not matter");
|
close('string that does not matter')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOption2(close: (id: string) => void) {
|
handleOption2(close: (id: string) => void) {
|
||||||
if (this.onOption2) {
|
if (this.onOption2) {
|
||||||
this.onOption2();
|
this.onOption2()
|
||||||
}
|
}
|
||||||
close("string that does not matter");
|
close('string that does not matter')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOption3(close: (id: string) => void) {
|
handleOption3(close: (id: string) => void) {
|
||||||
if (this.onOption3) {
|
if (this.onOption3) {
|
||||||
this.onOption3();
|
this.onOption3()
|
||||||
}
|
}
|
||||||
close("string that does not matter");
|
close('string that does not matter')
|
||||||
}
|
}
|
||||||
|
|
||||||
handleCancel(close: (id: string) => void) {
|
handleCancel(close: (id: string) => void) {
|
||||||
if (this.onCancel) {
|
if (this.onCancel) {
|
||||||
this.onCancel();
|
this.onCancel()
|
||||||
}
|
}
|
||||||
close("string that does not matter");
|
close('string that does not matter')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="visible" class="dialog-overlay">
|
<div v-if="visible" class="dialog-overlay">
|
||||||
<div class="dialog">
|
<div class="dialog">
|
||||||
<h1 class="text-xl font-bold text-center mb-4">{{ title }}</h1>
|
<h1 class="text-xl font-bold text-center mb-4">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
Note that their name is only stored on this device.
|
Note that their name is only stored on this device.
|
||||||
<input
|
<input
|
||||||
@@ -35,43 +37,43 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component } from "vue-facing-decorator";
|
import { Vue, Component } from 'vue-facing-decorator'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class ContactNameDialog extends Vue {
|
export default class ContactNameDialog extends Vue {
|
||||||
cancelCallback: () => void = () => {};
|
cancelCallback: () => void = () => {}
|
||||||
saveCallback: (name?: string) => void = () => {};
|
saveCallback: (name?: string) => void = () => {}
|
||||||
message = "";
|
message = ''
|
||||||
newText = "";
|
newText = ''
|
||||||
title = "Contact Name";
|
title = 'Contact Name'
|
||||||
visible = false;
|
visible = false
|
||||||
|
|
||||||
async open(
|
async open(
|
||||||
title?: string,
|
title?: string,
|
||||||
message?: string,
|
message?: string,
|
||||||
saveCallback?: (name?: string) => void,
|
saveCallback?: (name?: string) => void,
|
||||||
cancelCallback?: () => void,
|
cancelCallback?: () => void,
|
||||||
defaultName?: string,
|
defaultName?: string
|
||||||
) {
|
) {
|
||||||
this.cancelCallback = cancelCallback || this.cancelCallback;
|
this.cancelCallback = cancelCallback || this.cancelCallback
|
||||||
this.saveCallback = saveCallback || this.saveCallback;
|
this.saveCallback = saveCallback || this.saveCallback
|
||||||
this.message = message ?? this.message;
|
this.message = message ?? this.message
|
||||||
this.newText = defaultName ?? "";
|
this.newText = defaultName ?? ''
|
||||||
this.title = title ?? this.title;
|
this.title = title ?? this.title
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickSaveChanges() {
|
async onClickSaveChanges() {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
if (this.saveCallback) {
|
if (this.saveCallback) {
|
||||||
this.saveCallback(this.newText);
|
this.saveCallback(this.newText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickCancel() {
|
onClickCancel() {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
if (this.cancelCallback) {
|
if (this.cancelCallback) {
|
||||||
this.cancelCallback();
|
this.cancelCallback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,15 +61,15 @@ backup and database export, with platform-specific download instructions. * *
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
import { Component, Prop, Vue } from 'vue-facing-decorator'
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../constants/app'
|
||||||
import { db } from "../db/index";
|
import { db } from '../db/index'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../utils/logger'
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from '../services/PlatformServiceFactory'
|
||||||
import {
|
import {
|
||||||
PlatformService,
|
PlatformService,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities
|
||||||
} from "../services/PlatformService";
|
} from '../services/PlatformService'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @vue-component
|
* @vue-component
|
||||||
@@ -82,33 +82,33 @@ export default class DataExportSection extends Vue {
|
|||||||
* Notification function injected by Vue
|
* Notification function injected by Vue
|
||||||
* Used to show success/error messages to the user
|
* Used to show success/error messages to the user
|
||||||
*/
|
*/
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Active DID (Decentralized Identifier) of the user
|
* Active DID (Decentralized Identifier) of the user
|
||||||
* Controls visibility of seed backup option
|
* Controls visibility of seed backup option
|
||||||
* @required
|
* @required
|
||||||
*/
|
*/
|
||||||
@Prop({ required: true }) readonly activeDid!: string;
|
@Prop({ required: true }) readonly activeDid!: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL for the database export download
|
* URL for the database export download
|
||||||
* Created and revoked dynamically during export process
|
* Created and revoked dynamically during export process
|
||||||
* Only used in web platform
|
* Only used in web platform
|
||||||
*/
|
*/
|
||||||
downloadUrl = "";
|
downloadUrl = ''
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service instance for platform-specific operations
|
* Platform service instance for platform-specific operations
|
||||||
*/
|
*/
|
||||||
private platformService: PlatformService =
|
private platformService: PlatformService =
|
||||||
PlatformServiceFactory.getInstance();
|
PlatformServiceFactory.getInstance()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform capabilities for the current platform
|
* Platform capabilities for the current platform
|
||||||
*/
|
*/
|
||||||
private get platformCapabilities(): PlatformCapabilities {
|
private get platformCapabilities(): PlatformCapabilities {
|
||||||
return this.platformService.getCapabilities();
|
return this.platformService.getCapabilities()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,7 +117,7 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
if (this.downloadUrl && this.platformCapabilities.hasFileDownload) {
|
if (this.downloadUrl && this.platformCapabilities.hasFileDownload) {
|
||||||
URL.revokeObjectURL(this.downloadUrl);
|
URL.revokeObjectURL(this.downloadUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,45 +131,45 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
public async exportDatabase() {
|
public async exportDatabase() {
|
||||||
try {
|
try {
|
||||||
const blob = await db.export({ prettyJson: true });
|
const blob = await db.export({ prettyJson: true })
|
||||||
const fileName = `${db.name}-backup.json`;
|
const fileName = `${db.name}-backup.json`
|
||||||
|
|
||||||
if (this.platformCapabilities.hasFileDownload) {
|
if (this.platformCapabilities.hasFileDownload) {
|
||||||
// Web platform: Use download link
|
// Web platform: Use download link
|
||||||
this.downloadUrl = URL.createObjectURL(blob);
|
this.downloadUrl = URL.createObjectURL(blob)
|
||||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement
|
||||||
downloadAnchor.href = this.downloadUrl;
|
downloadAnchor.href = this.downloadUrl
|
||||||
downloadAnchor.download = fileName;
|
downloadAnchor.download = fileName
|
||||||
downloadAnchor.click();
|
downloadAnchor.click()
|
||||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000)
|
||||||
} else if (this.platformCapabilities.hasFileSystem) {
|
} else if (this.platformCapabilities.hasFileSystem) {
|
||||||
// Native platform: Write to app directory
|
// Native platform: Write to app directory
|
||||||
const content = await blob.text();
|
const content = await blob.text()
|
||||||
await this.platformService.writeAndShareFile(fileName, content);
|
await this.platformService.writeAndShareFile(fileName, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "success",
|
type: 'success',
|
||||||
title: "Export Successful",
|
title: 'Export Successful',
|
||||||
text: this.platformCapabilities.hasFileDownload
|
text: this.platformCapabilities.hasFileDownload
|
||||||
? "See your downloads directory for the backup. It is in the Dexie format."
|
? 'See your downloads directory for the backup. It is in the Dexie format.'
|
||||||
: "Please choose a location to save your backup file.",
|
: 'Please choose a location to save your backup file.'
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Export Error:", error);
|
logger.error('Export Error:', error)
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Export Error",
|
title: 'Export Error',
|
||||||
text: "There was an error exporting the data.",
|
text: 'There was an error exporting the data.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,8 +179,8 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
public computedStartDownloadLinkClassNames() {
|
public computedStartDownloadLinkClassNames() {
|
||||||
return {
|
return {
|
||||||
hidden: this.downloadUrl && this.platformCapabilities.hasFileDownload,
|
hidden: this.downloadUrl && this.platformCapabilities.hasFileDownload
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,8 +189,8 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
public computedDownloadLinkClassNames() {
|
public computedDownloadLinkClassNames() {
|
||||||
return {
|
return {
|
||||||
hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload,
|
hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<div class="w-fit" v-html="generateIcon()"></div>
|
<div class="w-fit" v-html="generateIcon()" />
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createAvatar, StyleOptions } from "@dicebear/core";
|
import { createAvatar, StyleOptions } from '@dicebear/core'
|
||||||
import { avataaars } from "@dicebear/collection";
|
import { avataaars } from '@dicebear/collection'
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from 'vue-facing-decorator'
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from '../db/tables/contacts'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class EntityIcon extends Vue {
|
export default class EntityIcon extends Vue {
|
||||||
@Prop contact: Contact;
|
@Prop contact: Contact
|
||||||
@Prop entityId = ""; // overridden by contact.did or profileImageUrl
|
@Prop entityId = '' // overridden by contact.did or profileImageUrl
|
||||||
@Prop iconSize = 0;
|
@Prop iconSize = 0
|
||||||
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
|
@Prop profileImageUrl = '' // overridden by contact.profileImageUrl
|
||||||
|
|
||||||
generateIcon() {
|
generateIcon() {
|
||||||
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
|
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl
|
||||||
if (imageUrl) {
|
if (imageUrl) {
|
||||||
return `<img src="${imageUrl}" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
|
return `<img src="${imageUrl}" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`
|
||||||
} else {
|
} else {
|
||||||
const identifier = this.contact?.did || this.entityId;
|
const identifier = this.contact?.did || this.entityId
|
||||||
if (!identifier) {
|
if (!identifier) {
|
||||||
return `<img src="../src/assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
|
return `<img src="../src/assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`
|
||||||
}
|
}
|
||||||
// https://api.dicebear.com/8.x/avataaars/svg?seed=
|
// https://api.dicebear.com/8.x/avataaars/svg?seed=
|
||||||
// ... does not render things with the same seed as this library.
|
// ... does not render things with the same seed as this library.
|
||||||
// "did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b" yields a girl with flowers in her hair and a lightning earring
|
// "did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b" yields a girl with flowers in her hair and a lightning earring
|
||||||
// ... which looks similar to '' at the dicebear site but which is different.
|
// ... which looks similar to '' at the dicebear site but which is different.
|
||||||
const options: StyleOptions<object> = {
|
const options: StyleOptions<object> = {
|
||||||
seed: (identifier as string) || "",
|
seed: (identifier as string) || '',
|
||||||
size: this.iconSize,
|
size: this.iconSize
|
||||||
};
|
}
|
||||||
const avatar = createAvatar(avataaars, options);
|
const avatar = createAvatar(avataaars, options)
|
||||||
const svgString = avatar.toString();
|
const svgString = avatar.toString()
|
||||||
return svgString;
|
return svgString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,11 @@
|
|||||||
class="sr-only"
|
class="sr-only"
|
||||||
/>
|
/>
|
||||||
<!-- line -->
|
<!-- line -->
|
||||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
|
||||||
<!-- dot -->
|
<!-- dot -->
|
||||||
<div
|
<div
|
||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -52,11 +52,11 @@
|
|||||||
class="sr-only"
|
class="sr-only"
|
||||||
/>
|
/>
|
||||||
<!-- line -->
|
<!-- line -->
|
||||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
|
||||||
<!-- dot -->
|
<!-- dot -->
|
||||||
<div
|
<div
|
||||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="relative ml-2">
|
<div v-else class="relative ml-2">
|
||||||
<button class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500">
|
<button class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500">
|
||||||
@@ -91,101 +91,96 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component } from "vue-facing-decorator";
|
import { Vue, Component } from 'vue-facing-decorator'
|
||||||
import {
|
import { LMap, LMarker, LRectangle, LTileLayer } from '@vue-leaflet/vue-leaflet'
|
||||||
LMap,
|
import { Router } from 'vue-router'
|
||||||
LMarker,
|
import { MASTER_SETTINGS_KEY } from '../db/tables/settings'
|
||||||
LRectangle,
|
import { db, retrieveSettingsForActiveAccount } from '../db/index'
|
||||||
LTileLayer,
|
|
||||||
} from "@vue-leaflet/vue-leaflet";
|
|
||||||
import { Router } from "vue-router";
|
|
||||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
LRectangle,
|
LRectangle,
|
||||||
LMap,
|
LMap,
|
||||||
LMarker,
|
LMarker,
|
||||||
LTileLayer,
|
LTileLayer
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
export default class FeedFilters extends Vue {
|
export default class FeedFilters extends Vue {
|
||||||
$router!: Router;
|
$router!: Router
|
||||||
onCloseIfChanged = () => {};
|
onCloseIfChanged = () => {}
|
||||||
hasSearchBox = false;
|
hasSearchBox = false
|
||||||
hasVisibleDid = false;
|
hasVisibleDid = false
|
||||||
isNearby = false;
|
isNearby = false
|
||||||
settingChanged = false;
|
settingChanged = false
|
||||||
visible = false;
|
visible = false
|
||||||
|
|
||||||
async open(onCloseIfChanged: () => void) {
|
async open(onCloseIfChanged: () => void) {
|
||||||
this.onCloseIfChanged = onCloseIfChanged;
|
this.onCloseIfChanged = onCloseIfChanged
|
||||||
|
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
this.hasVisibleDid = !!settings.filterFeedByVisible;
|
this.hasVisibleDid = !!settings.filterFeedByVisible
|
||||||
this.isNearby = !!settings.filterFeedByNearby;
|
this.isNearby = !!settings.filterFeedByNearby
|
||||||
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
||||||
this.hasSearchBox = true;
|
this.hasSearchBox = true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.settingChanged = false;
|
this.settingChanged = false
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleHasVisibleDid() {
|
async toggleHasVisibleDid() {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true
|
||||||
this.hasVisibleDid = !this.hasVisibleDid;
|
this.hasVisibleDid = !this.hasVisibleDid
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
filterFeedByVisible: this.hasVisibleDid,
|
filterFeedByVisible: this.hasVisibleDid
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleNearby() {
|
async toggleNearby() {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true
|
||||||
this.isNearby = !this.isNearby;
|
this.isNearby = !this.isNearby
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
filterFeedByNearby: this.isNearby,
|
filterFeedByNearby: this.isNearby
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearAll() {
|
async clearAll() {
|
||||||
if (this.hasVisibleDid || this.isNearby) {
|
if (this.hasVisibleDid || this.isNearby) {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
filterFeedByNearby: false,
|
filterFeedByNearby: false,
|
||||||
filterFeedByVisible: false,
|
filterFeedByVisible: false
|
||||||
});
|
})
|
||||||
|
|
||||||
this.hasVisibleDid = false;
|
this.hasVisibleDid = false
|
||||||
this.isNearby = false;
|
this.isNearby = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAll() {
|
async setAll() {
|
||||||
if (!this.hasVisibleDid || !this.isNearby) {
|
if (!this.hasVisibleDid || !this.isNearby) {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
filterFeedByNearby: true,
|
filterFeedByNearby: true,
|
||||||
filterFeedByVisible: true,
|
filterFeedByVisible: true
|
||||||
});
|
})
|
||||||
|
|
||||||
this.hasVisibleDid = true;
|
this.hasVisibleDid = true
|
||||||
this.isNearby = true;
|
this.isNearby = true
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
if (this.settingChanged) {
|
if (this.settingChanged) {
|
||||||
this.onCloseIfChanged();
|
this.onCloseIfChanged()
|
||||||
}
|
}
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
done() {
|
done() {
|
||||||
this.close();
|
this.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -51,8 +51,8 @@
|
|||||||
providerProjectId: fromProjectId,
|
providerProjectId: fromProjectId,
|
||||||
recipientDid: receiver?.did,
|
recipientDid: receiver?.did,
|
||||||
recipientName: receiver?.name,
|
recipientName: receiver?.name,
|
||||||
unitCode,
|
unitCode
|
||||||
},
|
}
|
||||||
}"
|
}"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
@@ -87,44 +87,44 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from 'vue-facing-decorator'
|
||||||
|
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../constants/app'
|
||||||
import {
|
import {
|
||||||
createAndSubmitGive,
|
createAndSubmitGive,
|
||||||
didInfo,
|
didInfo,
|
||||||
serverMessageForUser,
|
serverMessageForUser
|
||||||
} from "../libs/endorserServer";
|
} from '../libs/endorserServer'
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from '../libs/util'
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from '../db/index'
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from '../db/tables/contacts'
|
||||||
import { retrieveAccountDids } from "../libs/util";
|
import { retrieveAccountDids } from '../libs/util'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class GiftedDialog extends Vue {
|
export default class GiftedDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
@Prop() fromProjectId = "";
|
@Prop() fromProjectId = ''
|
||||||
@Prop() toProjectId = "";
|
@Prop() toProjectId = ''
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = ''
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = []
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = []
|
||||||
apiServer = "";
|
apiServer = ''
|
||||||
|
|
||||||
amountInput = "0";
|
amountInput = '0'
|
||||||
callbackOnSuccess?: (amount: number) => void = () => {};
|
callbackOnSuccess?: (amount: number) => void = () => {}
|
||||||
customTitle?: string;
|
customTitle?: string
|
||||||
description = "";
|
description = ''
|
||||||
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
|
giver?: libsUtil.GiverReceiverInputInfo // undefined means no identified giver agent
|
||||||
isTrade = false;
|
isTrade = false
|
||||||
offerId = "";
|
offerId = ''
|
||||||
prompt = "";
|
prompt = ''
|
||||||
receiver?: libsUtil.GiverReceiverInputInfo;
|
receiver?: libsUtil.GiverReceiverInputInfo
|
||||||
unitCode = "HUR";
|
unitCode = 'HUR'
|
||||||
visible = false;
|
visible = false
|
||||||
|
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil
|
||||||
|
|
||||||
async open(
|
async open(
|
||||||
giver?: libsUtil.GiverReceiverInputInfo,
|
giver?: libsUtil.GiverReceiverInputInfo,
|
||||||
@@ -132,146 +132,143 @@ export default class GiftedDialog extends Vue {
|
|||||||
offerId?: string,
|
offerId?: string,
|
||||||
customTitle?: string,
|
customTitle?: string,
|
||||||
prompt?: string,
|
prompt?: string,
|
||||||
callbackOnSuccess: (amount: number) => void = () => {},
|
callbackOnSuccess: (amount: number) => void = () => {}
|
||||||
) {
|
) {
|
||||||
this.customTitle = customTitle;
|
this.customTitle = customTitle
|
||||||
this.giver = giver;
|
this.giver = giver
|
||||||
this.prompt = prompt || "";
|
this.prompt = prompt || ''
|
||||||
this.receiver = receiver;
|
this.receiver = receiver
|
||||||
// if we show "given to user" selection, default checkbox to true
|
// if we show "given to user" selection, default checkbox to true
|
||||||
this.amountInput = "0";
|
this.amountInput = '0'
|
||||||
this.callbackOnSuccess = callbackOnSuccess;
|
this.callbackOnSuccess = callbackOnSuccess
|
||||||
this.offerId = offerId || "";
|
this.offerId = offerId || ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || ''
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || ''
|
||||||
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray()
|
||||||
|
|
||||||
this.allMyDids = await retrieveAccountDids();
|
this.allMyDids = await retrieveAccountDids()
|
||||||
|
|
||||||
if (this.giver && !this.giver.name) {
|
if (this.giver && !this.giver.name) {
|
||||||
this.giver.name = didInfo(
|
this.giver.name = didInfo(
|
||||||
this.giver.did,
|
this.giver.did,
|
||||||
this.activeDid,
|
this.activeDid,
|
||||||
this.allMyDids,
|
this.allMyDids,
|
||||||
this.allContacts,
|
this.allContacts
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error("Error retrieving settings from database:", err);
|
logger.error('Error retrieving settings from database:', err)
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: err.message || "There was an error retrieving your settings.",
|
text: err.message || 'There was an error retrieving your settings.'
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
// close the dialog but don't change values (since it might be submitting info)
|
// close the dialog but don't change values (since it might be submitting info)
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
changeUnitCode() {
|
changeUnitCode() {
|
||||||
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
const units = Object.keys(this.libsUtil.UNIT_SHORT)
|
||||||
const index = units.indexOf(this.unitCode);
|
const index = units.indexOf(this.unitCode)
|
||||||
this.unitCode = units[(index + 1) % units.length];
|
this.unitCode = units[(index + 1) % units.length]
|
||||||
}
|
}
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`
|
||||||
}
|
}
|
||||||
|
|
||||||
decrement() {
|
decrement() {
|
||||||
this.amountInput = `${Math.max(
|
this.amountInput = `${Math.max(0, (parseFloat(this.amountInput) || 1) - 1)}`
|
||||||
0,
|
|
||||||
(parseFloat(this.amountInput) || 1) - 1,
|
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.close();
|
this.close()
|
||||||
this.eraseValues();
|
this.eraseValues()
|
||||||
}
|
}
|
||||||
|
|
||||||
eraseValues() {
|
eraseValues() {
|
||||||
this.description = "";
|
this.description = ''
|
||||||
this.giver = undefined;
|
this.giver = undefined
|
||||||
this.amountInput = "0";
|
this.amountInput = '0'
|
||||||
this.prompt = "";
|
this.prompt = ''
|
||||||
this.unitCode = "HUR";
|
this.unitCode = 'HUR'
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm() {
|
async confirm() {
|
||||||
if (!this.activeDid) {
|
if (!this.activeDid) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: "You must select an identifier before you can record a give.",
|
text: 'You must select an identifier before you can record a give.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (parseFloat(this.amountInput) < 0) {
|
if (parseFloat(this.amountInput) < 0) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
text: "You may not send a negative number.",
|
text: 'You may not send a negative number.',
|
||||||
title: "",
|
title: ''
|
||||||
},
|
},
|
||||||
2000,
|
2000
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (!this.description && !parseFloat(this.amountInput)) {
|
if (!this.description && !parseFloat(this.amountInput)) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: `You must enter a description or some number of ${
|
text: `You must enter a description or some number of ${
|
||||||
this.libsUtil.UNIT_LONG[this.unitCode]
|
this.libsUtil.UNIT_LONG[this.unitCode]
|
||||||
}.`,
|
}.`
|
||||||
},
|
},
|
||||||
2000,
|
2000
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.close();
|
this.close()
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "toast",
|
type: 'toast',
|
||||||
text: "Recording the give...",
|
text: 'Recording the give...',
|
||||||
title: "",
|
title: ''
|
||||||
},
|
},
|
||||||
1000,
|
1000
|
||||||
);
|
)
|
||||||
// this is asynchronous, but we don't need to wait for it to complete
|
// this is asynchronous, but we don't need to wait for it to complete
|
||||||
await this.recordGive(
|
await this.recordGive(
|
||||||
(this.giver?.did as string) || null,
|
(this.giver?.did as string) || null,
|
||||||
(this.receiver?.did as string) || null,
|
(this.receiver?.did as string) || null,
|
||||||
this.description,
|
this.description,
|
||||||
parseFloat(this.amountInput),
|
parseFloat(this.amountInput),
|
||||||
this.unitCode,
|
this.unitCode
|
||||||
).then(() => {
|
).then(() => {
|
||||||
this.eraseValues();
|
this.eraseValues()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -287,7 +284,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
recipientDid: string | null,
|
recipientDid: string | null,
|
||||||
description: string,
|
description: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
unitCode: string = "HUR",
|
unitCode: string = 'HUR'
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const result = await createAndSubmitGive(
|
const result = await createAndSubmitGive(
|
||||||
@@ -303,54 +300,54 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.offerId,
|
this.offerId,
|
||||||
this.isTrade,
|
this.isTrade,
|
||||||
undefined,
|
undefined,
|
||||||
this.fromProjectId,
|
this.fromProjectId
|
||||||
);
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
result.type === "error" ||
|
result.type === 'error' ||
|
||||||
this.isGiveCreationError(result.response)
|
this.isGiveCreationError(result.response)
|
||||||
) {
|
) {
|
||||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
const errorMessage = this.getGiveCreationErrorMessage(result)
|
||||||
logger.error("Error with give creation result:", result);
|
logger.error('Error with give creation result:', result)
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: errorMessage || "There was an error creating the give.",
|
text: errorMessage || 'There was an error creating the give.'
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "success",
|
type: 'success',
|
||||||
title: "Success",
|
title: 'Success',
|
||||||
text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`,
|
text: `That ${this.isTrade ? 'trade' : 'gift'} was recorded.`
|
||||||
},
|
},
|
||||||
7000,
|
7000
|
||||||
);
|
)
|
||||||
if (this.callbackOnSuccess) {
|
if (this.callbackOnSuccess) {
|
||||||
this.callbackOnSuccess(amount);
|
this.callbackOnSuccess(amount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("Error with give recordation caught:", error);
|
logger.error('Error with give recordation caught:', error)
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error.userMessage ||
|
error.userMessage ||
|
||||||
serverMessageForUser(error) ||
|
serverMessageForUser(error) ||
|
||||||
"There was an error recording the give.";
|
'There was an error recording the give.'
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: errorMessage,
|
text: errorMessage
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +359,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
isGiveCreationError(result: any) {
|
isGiveCreationError(result: any) {
|
||||||
return result.status !== 201 || result.data?.error;
|
return result.status !== 201 || result.data?.error
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -375,19 +372,19 @@ export default class GiftedDialog extends Vue {
|
|||||||
result.error?.userMessage ||
|
result.error?.userMessage ||
|
||||||
result.error?.error ||
|
result.error?.error ||
|
||||||
result.response?.data?.error?.message
|
result.response?.data?.error?.message
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
explainData() {
|
explainData() {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "success",
|
type: 'success',
|
||||||
title: "Data Sharing",
|
title: 'Data Sharing',
|
||||||
text: libsUtil.PRIVACY_MESSAGE,
|
text: libsUtil.PRIVACY_MESSAGE
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
|
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
|
||||||
@click="cancel"
|
@click="cancel"
|
||||||
>
|
>
|
||||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
<font-awesome icon="xmark" class="w-[1em]" />
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
<span class="mt-2 flex justify-between">
|
<span class="mt-2 flex justify-between">
|
||||||
@@ -71,92 +71,92 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component } from "vue-facing-decorator";
|
import { Vue, Component } from 'vue-facing-decorator'
|
||||||
import { Router } from "vue-router";
|
import { Router } from 'vue-router'
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface } from '../constants/app'
|
||||||
import { db } from "../db/index";
|
import { db } from '../db/index'
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from '../db/tables/contacts'
|
||||||
import { GiverReceiverInputInfo } from "../libs/util";
|
import { GiverReceiverInputInfo } from '../libs/util'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class GivenPrompts extends Vue {
|
export default class GivenPrompts extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
$router!: Router;
|
$router!: Router
|
||||||
|
|
||||||
CATEGORY_CONTACTS = 1;
|
CATEGORY_CONTACTS = 1
|
||||||
CATEGORY_IDEAS = 0;
|
CATEGORY_IDEAS = 0
|
||||||
IDEAS = [
|
IDEAS = [
|
||||||
"What food did someone make? (How did it free up your time for something? Was something doable because it eased your stress?)",
|
'What food did someone make? (How did it free up your time for something? Was something doable because it eased your stress?)',
|
||||||
"What did a family member do? (How did you take better action because it made you feel loved?)",
|
'What did a family member do? (How did you take better action because it made you feel loved?)',
|
||||||
"What compliment did someone give you? (What task could you tackle because it boosted your confidence?)",
|
'What compliment did someone give you? (What task could you tackle because it boosted your confidence?)',
|
||||||
"Who is someone you can always rely on, and how did they demonstrate that? (What project tasks were enabled because you could depend on them?)",
|
'Who is someone you can always rely on, and how did they demonstrate that? (What project tasks were enabled because you could depend on them?)',
|
||||||
"What did you see someone give to someone else? (What is the effect of the positivity you gained from seeing that?)",
|
'What did you see someone give to someone else? (What is the effect of the positivity you gained from seeing that?)',
|
||||||
"What is a way that someone helped you even though you have never met? (What different action did you take due to that newfound perspective or inspiration?)",
|
'What is a way that someone helped you even though you have never met? (What different action did you take due to that newfound perspective or inspiration?)',
|
||||||
"How did a musician or author or artist inspire you? (What were you motivated to do more creatively because of that?)",
|
'How did a musician or author or artist inspire you? (What were you motivated to do more creatively because of that?)',
|
||||||
"What inspiration did you get from someone who handled tragedy well? (What could you accomplish with better grace or resilience after seeing their example?)",
|
'What inspiration did you get from someone who handled tragedy well? (What could you accomplish with better grace or resilience after seeing their example?)',
|
||||||
"What is something worth respect that an organization gave you? (How did their contribution improve the situation or enable new activities?)",
|
'What is something worth respect that an organization gave you? (How did their contribution improve the situation or enable new activities?)',
|
||||||
"Who last gave you a good laugh? (What kind of bond or revitalization did that bring to a situation?)",
|
'Who last gave you a good laugh? (What kind of bond or revitalization did that bring to a situation?)',
|
||||||
"What do you recall someone giving you while you were young? (How did it bring excitement or teach a skill or ignite a passion that resulted in improvements in your life?)",
|
'What do you recall someone giving you while you were young? (How did it bring excitement or teach a skill or ignite a passion that resulted in improvements in your life?)',
|
||||||
"Who forgave you or overlooked a mistake? (How did that free you or build trust that enabled better relationships?)",
|
'Who forgave you or overlooked a mistake? (How did that free you or build trust that enabled better relationships?)',
|
||||||
"What is a way an ancestor contributed to your life? (What in your life is now possible because of their efforts? What challenges are you undertaking knowing of their lives?)",
|
'What is a way an ancestor contributed to your life? (What in your life is now possible because of their efforts? What challenges are you undertaking knowing of their lives?)',
|
||||||
"What kind of help did someone at work give you? (How did that help with team progress? How did that lift your professional growth?)",
|
'What kind of help did someone at work give you? (How did that help with team progress? How did that lift your professional growth?)',
|
||||||
"How did a teacher or mentor or great example help you? (How did their guidance enhance your attitude or actions?)",
|
'How did a teacher or mentor or great example help you? (How did their guidance enhance your attitude or actions?)',
|
||||||
"What is a surprise gift you received? (What extra possibilities did it give you?)",
|
'What is a surprise gift you received? (What extra possibilities did it give you?)'
|
||||||
];
|
]
|
||||||
|
|
||||||
callbackOnFullGiftInfo?: (
|
callbackOnFullGiftInfo?: (
|
||||||
contactInfo?: GiverReceiverInputInfo,
|
contactInfo?: GiverReceiverInputInfo,
|
||||||
description?: string,
|
description?: string
|
||||||
) => void;
|
) => void
|
||||||
currentCategory = this.CATEGORY_IDEAS; // 0 = IDEAS, 1 = CONTACTS
|
currentCategory = this.CATEGORY_IDEAS // 0 = IDEAS, 1 = CONTACTS
|
||||||
currentContact: Contact | undefined = undefined;
|
currentContact: Contact | undefined = undefined
|
||||||
currentIdeaIndex = 0;
|
currentIdeaIndex = 0
|
||||||
numContacts = 0;
|
numContacts = 0
|
||||||
shownContactDbIndices: Array<boolean> = [];
|
shownContactDbIndices: Array<boolean> = []
|
||||||
visible = false;
|
visible = false
|
||||||
|
|
||||||
AppString = AppString;
|
AppString = AppString
|
||||||
|
|
||||||
async open(
|
async open(
|
||||||
callbackOnFullGiftInfo?: (
|
callbackOnFullGiftInfo?: (
|
||||||
contactInfo?: GiverReceiverInputInfo,
|
contactInfo?: GiverReceiverInputInfo,
|
||||||
description?: string,
|
description?: string
|
||||||
) => void,
|
) => void
|
||||||
) {
|
) {
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
|
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo
|
||||||
|
|
||||||
await db.open();
|
await db.open()
|
||||||
this.numContacts = await db.contacts.count();
|
this.numContacts = await db.contacts.count()
|
||||||
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
|
this.shownContactDbIndices = new Array<boolean>(this.numContacts) // all undefined to start
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.currentCategory = this.CATEGORY_IDEAS;
|
this.currentCategory = this.CATEGORY_IDEAS
|
||||||
this.currentContact = undefined;
|
this.currentContact = undefined
|
||||||
this.currentIdeaIndex = 0;
|
this.currentIdeaIndex = 0
|
||||||
this.numContacts = 0;
|
this.numContacts = 0
|
||||||
this.shownContactDbIndices = [];
|
this.shownContactDbIndices = []
|
||||||
|
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
proceed() {
|
proceed() {
|
||||||
// proceed with logic but don't change values (just in case some actions are added later)
|
// proceed with logic but don't change values (just in case some actions are added later)
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
name: "contact-gift",
|
name: 'contact-gift',
|
||||||
query: {
|
query: {
|
||||||
prompt: this.IDEAS[this.currentIdeaIndex],
|
prompt: this.IDEAS[this.currentIdeaIndex]
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// must be this.CATEGORY_CONTACTS
|
// must be this.CATEGORY_CONTACTS
|
||||||
this.callbackOnFullGiftInfo?.(
|
this.callbackOnFullGiftInfo?.(
|
||||||
this.currentContact as GiverReceiverInputInfo,
|
this.currentContact as GiverReceiverInputInfo
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,14 +167,14 @@ export default class GivenPrompts extends Vue {
|
|||||||
async nextIdea() {
|
async nextIdea() {
|
||||||
// check if the next one is an idea or a contact
|
// check if the next one is an idea or a contact
|
||||||
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
||||||
this.currentIdeaIndex++;
|
this.currentIdeaIndex++
|
||||||
if (this.currentIdeaIndex === this.IDEAS.length) {
|
if (this.currentIdeaIndex === this.IDEAS.length) {
|
||||||
// must have just finished ideas so move to contacts
|
// must have just finished ideas so move to contacts
|
||||||
this.findNextUnshownContact();
|
this.findNextUnshownContact()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// must be this.CATEGORY_CONTACTS
|
// must be this.CATEGORY_CONTACTS
|
||||||
this.findNextUnshownContact();
|
this.findNextUnshownContact()
|
||||||
// when that's finished, it'll reset to ideas
|
// when that's finished, it'll reset to ideas
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,54 +186,52 @@ export default class GivenPrompts extends Vue {
|
|||||||
async prevIdea() {
|
async prevIdea() {
|
||||||
// check if the next one is an idea or a contact
|
// check if the next one is an idea or a contact
|
||||||
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
||||||
this.currentIdeaIndex--;
|
this.currentIdeaIndex--
|
||||||
if (this.currentIdeaIndex < 0) {
|
if (this.currentIdeaIndex < 0) {
|
||||||
// must have just finished ideas so move to contacts
|
// must have just finished ideas so move to contacts
|
||||||
this.findNextUnshownContact();
|
this.findNextUnshownContact()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// must be this.CATEGORY_CONTACTS
|
// must be this.CATEGORY_CONTACTS
|
||||||
this.findNextUnshownContact();
|
this.findNextUnshownContact()
|
||||||
// when that's finished, it'll reset to ideas
|
// when that's finished, it'll reset to ideas
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextIdeaPastContacts() {
|
nextIdeaPastContacts() {
|
||||||
this.currentContact = undefined;
|
this.currentContact = undefined
|
||||||
this.shownContactDbIndices = new Array<boolean>(this.numContacts);
|
this.shownContactDbIndices = new Array<boolean>(this.numContacts)
|
||||||
|
|
||||||
this.currentCategory = this.CATEGORY_IDEAS;
|
this.currentCategory = this.CATEGORY_IDEAS
|
||||||
// look at the previous idea and switch to the other side of the list
|
// look at the previous idea and switch to the other side of the list
|
||||||
this.currentIdeaIndex =
|
this.currentIdeaIndex =
|
||||||
this.currentIdeaIndex >= this.IDEAS.length ? 0 : this.IDEAS.length - 1;
|
this.currentIdeaIndex >= this.IDEAS.length ? 0 : this.IDEAS.length - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
async findNextUnshownContact() {
|
async findNextUnshownContact() {
|
||||||
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
||||||
// we're not in the contact prompts, so reset index array
|
// we're not in the contact prompts, so reset index array
|
||||||
this.shownContactDbIndices = new Array<boolean>(this.numContacts);
|
this.shownContactDbIndices = new Array<boolean>(this.numContacts)
|
||||||
}
|
}
|
||||||
this.currentCategory = this.CATEGORY_CONTACTS;
|
this.currentCategory = this.CATEGORY_CONTACTS
|
||||||
|
|
||||||
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
|
let someContactDbIndex = Math.floor(Math.random() * this.numContacts)
|
||||||
let count = 0;
|
let count = 0
|
||||||
// as long as the index has an entry, loop
|
// as long as the index has an entry, loop
|
||||||
while (
|
while (
|
||||||
this.shownContactDbIndices[someContactDbIndex] != null &&
|
this.shownContactDbIndices[someContactDbIndex] != null &&
|
||||||
count++ < this.numContacts
|
count++ < this.numContacts
|
||||||
) {
|
) {
|
||||||
someContactDbIndex = (someContactDbIndex + 1) % this.numContacts;
|
someContactDbIndex = (someContactDbIndex + 1) % this.numContacts
|
||||||
}
|
}
|
||||||
if (count >= this.numContacts) {
|
if (count >= this.numContacts) {
|
||||||
// all contacts have been shown
|
// all contacts have been shown
|
||||||
this.nextIdeaPastContacts();
|
this.nextIdeaPastContacts()
|
||||||
} else {
|
} else {
|
||||||
// get the contact at that offset
|
// get the contact at that offset
|
||||||
await db.open();
|
await db.open()
|
||||||
this.currentContact = await db.contacts
|
this.currentContact = await db.contacts.offset(someContactDbIndex).first()
|
||||||
.offset(someContactDbIndex)
|
this.shownContactDbIndices[someContactDbIndex] = true
|
||||||
.first();
|
|
||||||
this.shownContactDbIndices[someContactDbIndex] = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,33 +100,33 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from 'vue-facing-decorator'
|
||||||
import * as R from "ramda";
|
import * as R from 'ramda'
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from '@vueuse/core'
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from '../db/tables/contacts'
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from '../libs/endorserServer'
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../constants/app'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class HiddenDidDialog extends Vue {
|
export default class HiddenDidDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
isOpen = false;
|
isOpen = false
|
||||||
roleName = "";
|
roleName = ''
|
||||||
visibleToDids: string[] = [];
|
visibleToDids: string[] = []
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = []
|
||||||
activeDid = "";
|
activeDid = ''
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = []
|
||||||
canShare = false;
|
canShare = false
|
||||||
windowLocation = window.location.href;
|
windowLocation = window.location.href
|
||||||
|
|
||||||
R = R;
|
R = R
|
||||||
serverUtil = serverUtil;
|
serverUtil = serverUtil
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
// When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
|
// When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
|
||||||
// then use this truer check: navigator.canShare && navigator.canShare()
|
// then use this truer check: navigator.canShare && navigator.canShare()
|
||||||
this.canShare = !!navigator.share;
|
this.canShare = !!navigator.share
|
||||||
}
|
}
|
||||||
|
|
||||||
open(
|
open(
|
||||||
@@ -134,18 +134,18 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
visibleToDids: string[],
|
visibleToDids: string[],
|
||||||
allContacts: Array<Contact>,
|
allContacts: Array<Contact>,
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
allMyDids: Array<string>,
|
allMyDids: Array<string>
|
||||||
) {
|
) {
|
||||||
this.roleName = roleName;
|
this.roleName = roleName
|
||||||
this.visibleToDids = visibleToDids;
|
this.visibleToDids = visibleToDids
|
||||||
this.allContacts = allContacts;
|
this.allContacts = allContacts
|
||||||
this.activeDid = activeDid;
|
this.activeDid = activeDid
|
||||||
this.allMyDids = allMyDids;
|
this.allMyDids = allMyDids
|
||||||
this.isOpen = true;
|
this.isOpen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.isOpen = false;
|
this.isOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
didInfo(did: string) {
|
didInfo(did: string) {
|
||||||
@@ -153,8 +153,8 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
did,
|
did,
|
||||||
this.activeDid,
|
this.activeDid,
|
||||||
this.allMyDids,
|
this.allMyDids,
|
||||||
this.allContacts,
|
this.allContacts
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
copyToClipboard(name: string, text: string) {
|
copyToClipboard(name: string, text: string) {
|
||||||
@@ -163,23 +163,23 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "toast",
|
type: 'toast',
|
||||||
title: "Copied",
|
title: 'Copied',
|
||||||
text: (name || "That") + " was copied to the clipboard.",
|
text: (name || 'That') + ' was copied to the clipboard.'
|
||||||
},
|
},
|
||||||
2000,
|
2000
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
this.copyToClipboard('A link to this page', this.windowLocation)
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: 'Help Connect Me',
|
||||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
url: this.windowLocation,
|
url: this.windowLocation
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
||||||
@click="close()"
|
@click="close()"
|
||||||
>
|
>
|
||||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
<font-awesome icon="xmark" class="w-[1em]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -56,103 +56,102 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from "axios";
|
import axios from 'axios'
|
||||||
import { ref } from "vue";
|
import { ref } from 'vue'
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from 'vue-facing-decorator'
|
||||||
|
|
||||||
import PhotoDialog from "../components/PhotoDialog.vue";
|
import PhotoDialog from '../components/PhotoDialog.vue'
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../constants/app'
|
||||||
|
|
||||||
const inputImageFileNameRef = ref<Blob>();
|
const inputImageFileNameRef = ref<Blob>()
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { PhotoDialog },
|
components: { PhotoDialog }
|
||||||
})
|
})
|
||||||
export default class ImageMethodDialog extends Vue {
|
export default class ImageMethodDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
claimType: string;
|
claimType: string
|
||||||
crop: boolean = false;
|
crop: boolean = false
|
||||||
imageCallback: (imageUrl?: string) => void = () => {};
|
imageCallback: (imageUrl?: string) => void = () => {}
|
||||||
imageUrl?: string;
|
imageUrl?: string
|
||||||
visible = false;
|
visible = false
|
||||||
|
|
||||||
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
|
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
|
||||||
this.claimType = claimType;
|
this.claimType = claimType
|
||||||
this.crop = !!crop;
|
this.crop = !!crop
|
||||||
this.imageCallback = setImageFn;
|
this.imageCallback = setImageFn
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
openPhotoDialog(blob?: Blob, fileName?: string) {
|
openPhotoDialog(blob?: Blob, fileName?: string) {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
|
;(this.$refs.photoDialog as PhotoDialog).open(
|
||||||
(this.$refs.photoDialog as PhotoDialog).open(
|
|
||||||
this.imageCallback,
|
this.imageCallback,
|
||||||
this.claimType,
|
this.claimType,
|
||||||
this.crop,
|
this.crop,
|
||||||
blob,
|
blob,
|
||||||
fileName,
|
fileName
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadImageFile(event: Event) {
|
async uploadImageFile(event: Event) {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
|
|
||||||
inputImageFileNameRef.value = event.target.files[0];
|
inputImageFileNameRef.value = event.target.files[0]
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/File
|
// https://developer.mozilla.org/en-US/docs/Web/API/File
|
||||||
// ... plus it has a `type` property from my testing
|
// ... plus it has a `type` property from my testing
|
||||||
const file = inputImageFileNameRef.value;
|
const file = inputImageFileNameRef.value
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader()
|
||||||
reader.onload = async (e) => {
|
reader.onload = async (e) => {
|
||||||
const data = e.target?.result as ArrayBuffer;
|
const data = e.target?.result as ArrayBuffer
|
||||||
if (data) {
|
if (data) {
|
||||||
const blob = new Blob([new Uint8Array(data)], {
|
const blob = new Blob([new Uint8Array(data)], {
|
||||||
type: file.type,
|
type: file.type
|
||||||
});
|
})
|
||||||
this.openPhotoDialog(blob, file.name as string);
|
this.openPhotoDialog(blob, file.name as string)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
reader.readAsArrayBuffer(file as Blob);
|
reader.readAsArrayBuffer(file as Blob)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async acceptUrl() {
|
async acceptUrl() {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
if (this.crop) {
|
if (this.crop) {
|
||||||
try {
|
try {
|
||||||
const urlBlobResponse: Blob = await axios.get(this.imageUrl as string, {
|
const urlBlobResponse: Blob = await axios.get(this.imageUrl as string, {
|
||||||
responseType: "blob", // This ensures the data is returned as a Blob
|
responseType: 'blob' // This ensures the data is returned as a Blob
|
||||||
});
|
})
|
||||||
const fullUrl = new URL(this.imageUrl as string);
|
const fullUrl = new URL(this.imageUrl as string)
|
||||||
const fileName = fullUrl.pathname.split("/").pop() as string;
|
const fileName = fullUrl.pathname.split('/').pop() as string
|
||||||
(this.$refs.photoDialog as PhotoDialog).open(
|
;(this.$refs.photoDialog as PhotoDialog).open(
|
||||||
this.imageCallback,
|
this.imageCallback,
|
||||||
this.claimType,
|
this.claimType,
|
||||||
this.crop,
|
this.crop,
|
||||||
urlBlobResponse.data as Blob,
|
urlBlobResponse.data as Blob,
|
||||||
fileName,
|
fileName
|
||||||
);
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: "There was an error retrieving that image.",
|
text: 'There was an error retrieving that image.'
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.imageCallback(this.imageUrl);
|
this.imageCallback(this.imageUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -38,44 +38,44 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from 'vue-facing-decorator'
|
||||||
import { UAParser } from "ua-parser-js";
|
import { UAParser } from 'ua-parser-js'
|
||||||
|
|
||||||
@Component({ emits: ["update:isOpen"] })
|
@Component({ emits: ['update:isOpen'] })
|
||||||
export default class ImageViewer extends Vue {
|
export default class ImageViewer extends Vue {
|
||||||
@Prop() imageUrl!: string;
|
@Prop() imageUrl!: string
|
||||||
@Prop() imageData!: Blob | null;
|
@Prop() imageData!: Blob | null
|
||||||
@Prop() isOpen!: boolean;
|
@Prop() isOpen!: boolean
|
||||||
|
|
||||||
userAgent = new UAParser();
|
userAgent = new UAParser()
|
||||||
|
|
||||||
get isMobile() {
|
get isMobile() {
|
||||||
const os = this.userAgent.getOS().name;
|
const os = this.userAgent.getOS().name
|
||||||
return os === "iOS" || os === "Android";
|
return os === 'iOS' || os === 'Android'
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.$emit("update:isOpen", false);
|
this.$emit('update:isOpen', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleShare() {
|
async handleShare() {
|
||||||
const os = this.userAgent.getOS().name;
|
const os = this.userAgent.getOS().name
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (os === "iOS" || os === "Android") {
|
if (os === 'iOS' || os === 'Android') {
|
||||||
if (navigator.share) {
|
if (navigator.share) {
|
||||||
// Always share the URL since it's more reliable across platforms
|
// Always share the URL since it's more reliable across platforms
|
||||||
await navigator.share({
|
await navigator.share({
|
||||||
url: this.imageUrl,
|
url: this.imageUrl
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// Fallback for browsers without share API
|
// Fallback for browsers without share API
|
||||||
window.open(this.imageUrl, "_blank");
|
window.open(this.imageUrl, '_blank')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("Share failed, opening in new tab:", error);
|
logger.warn('Share failed, opening in new tab:', error)
|
||||||
window.open(this.imageUrl, "_blank");
|
window.open(this.imageUrl, '_blank')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ loading state management. * * @author Matthew Raymer * @version 1.0.0 */
|
|||||||
<template>
|
<template>
|
||||||
<div ref="scrollContainer">
|
<div ref="scrollContainer">
|
||||||
<slot />
|
<slot />
|
||||||
<div ref="sentinel" style="height: 1px"></div>
|
<div ref="sentinel" style="height: 1px" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Emit, Prop, Vue } from "vue-facing-decorator";
|
import { Component, Emit, Prop, Vue } from 'vue-facing-decorator'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* InfiniteScroll Component
|
* InfiniteScroll Component
|
||||||
@@ -38,19 +38,19 @@ import { Component, Emit, Prop, Vue } from "vue-facing-decorator";
|
|||||||
export default class InfiniteScroll extends Vue {
|
export default class InfiniteScroll extends Vue {
|
||||||
/** Distance in pixels from the bottom at which to trigger the reached-bottom event */
|
/** Distance in pixels from the bottom at which to trigger the reached-bottom event */
|
||||||
@Prop({ default: 200 })
|
@Prop({ default: 200 })
|
||||||
readonly distance!: number;
|
readonly distance!: number
|
||||||
|
|
||||||
/** Intersection Observer instance for detecting scroll position */
|
/** Intersection Observer instance for detecting scroll position */
|
||||||
private observer!: IntersectionObserver;
|
private observer!: IntersectionObserver
|
||||||
|
|
||||||
/** Flag to track initial render state */
|
/** Flag to track initial render state */
|
||||||
private isInitialRender = true;
|
private isInitialRender = true
|
||||||
|
|
||||||
/** Flag to prevent multiple simultaneous loading states */
|
/** Flag to prevent multiple simultaneous loading states */
|
||||||
private isLoading = false;
|
private isLoading = false
|
||||||
|
|
||||||
/** Timeout ID for debouncing scroll events */
|
/** Timeout ID for debouncing scroll events */
|
||||||
private debounceTimeout: number | null = null;
|
private debounceTimeout: number | null = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vue lifecycle hook that runs after component updates.
|
* Vue lifecycle hook that runs after component updates.
|
||||||
@@ -64,13 +64,10 @@ export default class InfiniteScroll extends Vue {
|
|||||||
const options = {
|
const options = {
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: `0px 0px ${this.distance}px 0px`,
|
rootMargin: `0px 0px ${this.distance}px 0px`,
|
||||||
threshold: 1.0,
|
threshold: 1.0
|
||||||
};
|
}
|
||||||
this.observer = new IntersectionObserver(
|
this.observer = new IntersectionObserver(this.handleIntersection, options)
|
||||||
this.handleIntersection,
|
this.observer.observe(this.$refs.sentinel as HTMLElement)
|
||||||
options,
|
|
||||||
);
|
|
||||||
this.observer.observe(this.$refs.sentinel as HTMLElement);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +80,10 @@ export default class InfiniteScroll extends Vue {
|
|||||||
*/
|
*/
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
if (this.observer) {
|
if (this.observer) {
|
||||||
this.observer.disconnect();
|
this.observer.disconnect()
|
||||||
}
|
}
|
||||||
if (this.debounceTimeout) {
|
if (this.debounceTimeout) {
|
||||||
window.clearTimeout(this.debounceTimeout);
|
window.clearTimeout(this.debounceTimeout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,25 +98,25 @@ export default class InfiniteScroll extends Vue {
|
|||||||
* Used internally by the Intersection Observer
|
* Used internally by the Intersection Observer
|
||||||
* @emits reached-bottom - Emitted when the user scrolls near the bottom
|
* @emits reached-bottom - Emitted when the user scrolls near the bottom
|
||||||
*/
|
*/
|
||||||
@Emit("reached-bottom")
|
@Emit('reached-bottom')
|
||||||
handleIntersection(entries: IntersectionObserverEntry[]) {
|
handleIntersection(entries: IntersectionObserverEntry[]) {
|
||||||
const entry = entries[0];
|
const entry = entries[0]
|
||||||
if (entry.isIntersecting && !this.isLoading) {
|
if (entry.isIntersecting && !this.isLoading) {
|
||||||
// Debounce the intersection event
|
// Debounce the intersection event
|
||||||
if (this.debounceTimeout) {
|
if (this.debounceTimeout) {
|
||||||
window.clearTimeout(this.debounceTimeout);
|
window.clearTimeout(this.debounceTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debounceTimeout = window.setTimeout(() => {
|
this.debounceTimeout = window.setTimeout(() => {
|
||||||
this.isLoading = true;
|
this.isLoading = true
|
||||||
this.$emit("reached-bottom", true);
|
this.$emit('reached-bottom', true)
|
||||||
// Reset loading state after a short delay
|
// Reset loading state after a short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.isLoading = false;
|
this.isLoading = false
|
||||||
}, 1000);
|
}, 1000)
|
||||||
}, 300);
|
}, 300)
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -46,50 +46,50 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component } from "vue-facing-decorator";
|
import { Vue, Component } from 'vue-facing-decorator'
|
||||||
|
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../constants/app'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class InviteDialog extends Vue {
|
export default class InviteDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
callback: (text: string, expiresAt: string) => void = () => {};
|
callback: (text: string, expiresAt: string) => void = () => {}
|
||||||
inviteIdentifier = "";
|
inviteIdentifier = ''
|
||||||
text = "";
|
text = ''
|
||||||
visible = false;
|
visible = false
|
||||||
expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7)
|
expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7)
|
||||||
.toISOString()
|
.toISOString()
|
||||||
.substring(0, 10);
|
.substring(0, 10)
|
||||||
|
|
||||||
async open(
|
async open(
|
||||||
inviteIdentifier: string,
|
inviteIdentifier: string,
|
||||||
aCallback: (text: string, expiresAt: string) => void,
|
aCallback: (text: string, expiresAt: string) => void
|
||||||
) {
|
) {
|
||||||
this.callback = aCallback;
|
this.callback = aCallback
|
||||||
this.inviteIdentifier = inviteIdentifier;
|
this.inviteIdentifier = inviteIdentifier
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickSaveChanges() {
|
async onClickSaveChanges() {
|
||||||
if (!this.expiresAt) {
|
if (!this.expiresAt) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "warning",
|
type: 'warning',
|
||||||
title: "Needs Expiration",
|
title: 'Needs Expiration',
|
||||||
text: "You must select an expiration date.",
|
text: 'You must select an expiration date.'
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
this.callback(this.text, this.expiresAt);
|
this.callback(this.text, this.expiresAt)
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickCancel() {
|
onClickCancel() {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -80,7 +80,9 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<h3 class="text-lg font-medium">{{ member.name }}</h3>
|
<h3 class="text-lg font-medium">
|
||||||
|
{{ member.name }}
|
||||||
|
</h3>
|
||||||
<div
|
<div
|
||||||
v-if="!getContactFor(member.did) && member.did !== activeDid"
|
v-if="!getContactFor(member.did) && member.did !== activeDid"
|
||||||
class="flex justify-end"
|
class="flex justify-end"
|
||||||
@@ -99,7 +101,7 @@
|
|||||||
title="Contact info"
|
title="Contact info"
|
||||||
@click="
|
@click="
|
||||||
informAboutAddingContact(
|
informAboutAddingContact(
|
||||||
getContactFor(member.did) !== undefined,
|
getContactFor(member.did) !== undefined
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -157,138 +159,138 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from 'vue-facing-decorator'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
logConsoleAndDb,
|
logConsoleAndDb,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
db,
|
db
|
||||||
} from "../db/index";
|
} from '../db/index'
|
||||||
import {
|
import {
|
||||||
errorStringForLog,
|
errorStringForLog,
|
||||||
getHeaders,
|
getHeaders,
|
||||||
register,
|
register,
|
||||||
serverMessageForUser,
|
serverMessageForUser
|
||||||
} from "../libs/endorserServer";
|
} from '../libs/endorserServer'
|
||||||
import { decryptMessage } from "../libs/crypto";
|
import { decryptMessage } from '../libs/crypto'
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from '../db/tables/contacts'
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from '../libs/util'
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../constants/app'
|
||||||
|
|
||||||
interface Member {
|
interface Member {
|
||||||
admitted: boolean;
|
admitted: boolean
|
||||||
content: string;
|
content: string
|
||||||
memberId: number;
|
memberId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DecryptedMember {
|
interface DecryptedMember {
|
||||||
member: Member;
|
member: Member
|
||||||
name: string;
|
name: string
|
||||||
did: string;
|
did: string
|
||||||
isRegistered: boolean;
|
isRegistered: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class MembersList extends Vue {
|
export default class MembersList extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil
|
||||||
|
|
||||||
@Prop({ required: true }) password!: string;
|
@Prop({ required: true }) password!: string
|
||||||
@Prop({ default: false }) showOrganizerTools!: boolean;
|
@Prop({ default: false }) showOrganizerTools!: boolean
|
||||||
|
|
||||||
decryptedMembers: DecryptedMember[] = [];
|
decryptedMembers: DecryptedMember[] = []
|
||||||
firstName = "";
|
firstName = ''
|
||||||
isLoading = true;
|
isLoading = true
|
||||||
isOrganizer = false;
|
isOrganizer = false
|
||||||
members: Member[] = [];
|
members: Member[] = []
|
||||||
missingPassword = false;
|
missingPassword = false
|
||||||
missingMyself = false;
|
missingMyself = false
|
||||||
activeDid = "";
|
activeDid = ''
|
||||||
apiServer = "";
|
apiServer = ''
|
||||||
contacts: Array<Contact> = [];
|
contacts: Array<Contact> = []
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || ''
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || ''
|
||||||
this.firstName = settings.firstName || "";
|
this.firstName = settings.firstName || ''
|
||||||
await this.fetchMembers();
|
await this.fetchMembers()
|
||||||
await this.loadContacts();
|
await this.loadContacts()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMembers() {
|
async fetchMembers() {
|
||||||
try {
|
try {
|
||||||
this.isLoading = true;
|
this.isLoading = true
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid)
|
||||||
const response = await this.axios.get(
|
const response = await this.axios.get(
|
||||||
`${this.apiServer}/api/partner/groupOnboardMembers`,
|
`${this.apiServer}/api/partner/groupOnboardMembers`,
|
||||||
{ headers },
|
{ headers }
|
||||||
);
|
)
|
||||||
|
|
||||||
if (response.data && response.data.data) {
|
if (response.data && response.data.data) {
|
||||||
this.members = response.data.data;
|
this.members = response.data.data
|
||||||
await this.decryptMemberContents();
|
await this.decryptMemberContents()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Error fetching members: " + errorStringForLog(error),
|
'Error fetching members: ' + errorStringForLog(error),
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
this.$emit(
|
this.$emit(
|
||||||
"error",
|
'error',
|
||||||
serverMessageForUser(error) || "Failed to fetch members.",
|
serverMessageForUser(error) || 'Failed to fetch members.'
|
||||||
);
|
)
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptMemberContents() {
|
async decryptMemberContents() {
|
||||||
this.decryptedMembers = [];
|
this.decryptedMembers = []
|
||||||
|
|
||||||
if (!this.password) {
|
if (!this.password) {
|
||||||
this.missingPassword = true;
|
this.missingPassword = true
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let isFirstEntry = true,
|
let isFirstEntry = true,
|
||||||
foundMyself = false;
|
foundMyself = false
|
||||||
for (const member of this.members) {
|
for (const member of this.members) {
|
||||||
try {
|
try {
|
||||||
const decryptedContent = await decryptMessage(
|
const decryptedContent = await decryptMessage(
|
||||||
member.content,
|
member.content,
|
||||||
this.password,
|
this.password
|
||||||
);
|
)
|
||||||
const content = JSON.parse(decryptedContent);
|
const content = JSON.parse(decryptedContent)
|
||||||
|
|
||||||
this.decryptedMembers.push({
|
this.decryptedMembers.push({
|
||||||
member: member,
|
member: member,
|
||||||
name: content.name,
|
name: content.name,
|
||||||
did: content.did,
|
did: content.did,
|
||||||
isRegistered: !!content.isRegistered,
|
isRegistered: !!content.isRegistered
|
||||||
});
|
})
|
||||||
if (isFirstEntry && content.did === this.activeDid) {
|
if (isFirstEntry && content.did === this.activeDid) {
|
||||||
this.isOrganizer = true;
|
this.isOrganizer = true
|
||||||
}
|
}
|
||||||
if (content.did === this.activeDid) {
|
if (content.did === this.activeDid) {
|
||||||
foundMyself = true;
|
foundMyself = true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// do nothing, relying on the count of members to determine if there was an error
|
// do nothing, relying on the count of members to determine if there was an error
|
||||||
}
|
}
|
||||||
isFirstEntry = false;
|
isFirstEntry = false
|
||||||
}
|
}
|
||||||
this.missingMyself = !foundMyself;
|
this.missingMyself = !foundMyself
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptionErrorMessage(): string {
|
decryptionErrorMessage(): string {
|
||||||
if (this.isOrganizer) {
|
if (this.isOrganizer) {
|
||||||
if (this.decryptedMembers.length < this.members.length) {
|
if (this.decryptedMembers.length < this.members.length) {
|
||||||
return "Some members have data that cannot be decrypted with that password.";
|
return 'Some members have data that cannot be decrypted with that password.'
|
||||||
} else {
|
} else {
|
||||||
// the lists must be equal
|
// the lists must be equal
|
||||||
return "";
|
return ''
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// non-organizers should only see problems if the first (organizer) member is not decrypted
|
// non-organizers should only see problems if the first (organizer) member is not decrypted
|
||||||
@@ -296,10 +298,10 @@ export default class MembersList extends Vue {
|
|||||||
this.decryptedMembers.length === 0 ||
|
this.decryptedMembers.length === 0 ||
|
||||||
this.decryptedMembers[0].member.memberId !== this.members[0].memberId
|
this.decryptedMembers[0].member.memberId !== this.members[0].memberId
|
||||||
) {
|
) {
|
||||||
return "Your password is not the same as the organizer. Reload or have them check their password.";
|
return 'Your password is not the same as the organizer. Reload or have them check their password.'
|
||||||
} else {
|
} else {
|
||||||
// the first (organizer) member was decrypted OK
|
// the first (organizer) member was decrypted OK
|
||||||
return "";
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,118 +309,118 @@ export default class MembersList extends Vue {
|
|||||||
membersToShow(): DecryptedMember[] {
|
membersToShow(): DecryptedMember[] {
|
||||||
if (this.isOrganizer) {
|
if (this.isOrganizer) {
|
||||||
if (this.showOrganizerTools) {
|
if (this.showOrganizerTools) {
|
||||||
return this.decryptedMembers;
|
return this.decryptedMembers
|
||||||
} else {
|
} else {
|
||||||
return this.decryptedMembers.filter(
|
return this.decryptedMembers.filter(
|
||||||
(member: DecryptedMember) => member.member.admitted,
|
(member: DecryptedMember) => member.member.admitted
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// non-organizers only get visible members from server
|
// non-organizers only get visible members from server
|
||||||
return this.decryptedMembers;
|
return this.decryptedMembers
|
||||||
}
|
}
|
||||||
|
|
||||||
informAboutAdmission() {
|
informAboutAdmission() {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Admission info",
|
title: 'Admission info',
|
||||||
text: "This is to register people in Time Safari and to admit them to the meeting. A '+' symbol means they are not yet admitted and you can register and admit them. A '-' means you can remove them, but they will stay registered.",
|
text: "This is to register people in Time Safari and to admit them to the meeting. A '+' symbol means they are not yet admitted and you can register and admit them. A '-' means you can remove them, but they will stay registered."
|
||||||
},
|
},
|
||||||
10000,
|
10000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
informAboutAddingContact(contactImportedAlready: boolean) {
|
informAboutAddingContact(contactImportedAlready: boolean) {
|
||||||
if (contactImportedAlready) {
|
if (contactImportedAlready) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Contact Exists",
|
title: 'Contact Exists',
|
||||||
text: "They are in your contacts. If you want to remove them, you must do that from the contacts screen.",
|
text: 'They are in your contacts. If you want to remove them, you must do that from the contacts screen.'
|
||||||
},
|
},
|
||||||
10000,
|
10000
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Contact Available",
|
title: 'Contact Available',
|
||||||
text: "This is to add them to your contacts. If you want to remove them later, you must do that from the contacts screen.",
|
text: 'This is to add them to your contacts. If you want to remove them later, you must do that from the contacts screen.'
|
||||||
},
|
},
|
||||||
10000,
|
10000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadContacts() {
|
async loadContacts() {
|
||||||
this.contacts = await db.contacts.toArray();
|
this.contacts = await db.contacts.toArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
getContactFor(did: string): Contact | undefined {
|
getContactFor(did: string): Contact | undefined {
|
||||||
return this.contacts.find((contact) => contact.did === did);
|
return this.contacts.find((contact) => contact.did === did)
|
||||||
}
|
}
|
||||||
|
|
||||||
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
|
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
|
||||||
const contact = this.getContactFor(decrMember.did);
|
const contact = this.getContactFor(decrMember.did)
|
||||||
if (!decrMember.member.admitted && !contact) {
|
if (!decrMember.member.admitted && !contact) {
|
||||||
// If not a contact, show confirmation dialog
|
// If not a contact, show confirmation dialog
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "modal",
|
group: 'modal',
|
||||||
type: "confirm",
|
type: 'confirm',
|
||||||
title: "Add as Contact First?",
|
title: 'Add as Contact First?',
|
||||||
text: "This person is not in your contacts. Would you like to add them as a contact first?",
|
text: 'This person is not in your contacts. Would you like to add them as a contact first?',
|
||||||
yesText: "Add as Contact",
|
yesText: 'Add as Contact',
|
||||||
noText: "Skip Adding Contact",
|
noText: 'Skip Adding Contact',
|
||||||
onYes: async () => {
|
onYes: async () => {
|
||||||
await this.addAsContact(decrMember);
|
await this.addAsContact(decrMember)
|
||||||
// After adding as contact, proceed with admission
|
// After adding as contact, proceed with admission
|
||||||
await this.toggleAdmission(decrMember);
|
await this.toggleAdmission(decrMember)
|
||||||
},
|
},
|
||||||
onNo: async () => {
|
onNo: async () => {
|
||||||
// If they choose not to add as contact, show second confirmation
|
// If they choose not to add as contact, show second confirmation
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "modal",
|
group: 'modal',
|
||||||
type: "confirm",
|
type: 'confirm',
|
||||||
title: "Continue Without Adding?",
|
title: 'Continue Without Adding?',
|
||||||
text: "Are you sure you want to proceed with admission? If they are not a contact, you will not know their name after this meeting.",
|
text: 'Are you sure you want to proceed with admission? If they are not a contact, you will not know their name after this meeting.',
|
||||||
yesText: "Continue",
|
yesText: 'Continue',
|
||||||
onYes: async () => {
|
onYes: async () => {
|
||||||
await this.toggleAdmission(decrMember);
|
await this.toggleAdmission(decrMember)
|
||||||
},
|
},
|
||||||
onCancel: async () => {
|
onCancel: async () => {
|
||||||
// Do nothing, effectively canceling the operation
|
// Do nothing, effectively canceling the operation
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
},
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// If already a contact, proceed directly with admission
|
// If already a contact, proceed directly with admission
|
||||||
this.toggleAdmission(decrMember);
|
this.toggleAdmission(decrMember)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleAdmission(decrMember: DecryptedMember) {
|
async toggleAdmission(decrMember: DecryptedMember) {
|
||||||
try {
|
try {
|
||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid)
|
||||||
await this.axios.put(
|
await this.axios.put(
|
||||||
`${this.apiServer}/api/partner/groupOnboardMember/${decrMember.member.memberId}`,
|
`${this.apiServer}/api/partner/groupOnboardMember/${decrMember.member.memberId}`,
|
||||||
{ admitted: !decrMember.member.admitted },
|
{ admitted: !decrMember.member.admitted },
|
||||||
{ headers },
|
{ headers }
|
||||||
);
|
)
|
||||||
// Update local state
|
// Update local state
|
||||||
decrMember.member.admitted = !decrMember.member.admitted;
|
decrMember.member.admitted = !decrMember.member.admitted
|
||||||
|
|
||||||
const oldContact = this.getContactFor(decrMember.did);
|
const oldContact = this.getContactFor(decrMember.did)
|
||||||
// if admitted, now register that user if they are not registered
|
// if admitted, now register that user if they are not registered
|
||||||
if (
|
if (
|
||||||
decrMember.member.admitted &&
|
decrMember.member.admitted &&
|
||||||
@@ -427,61 +429,61 @@ export default class MembersList extends Vue {
|
|||||||
) {
|
) {
|
||||||
const contactOldOrNew: Contact = oldContact || {
|
const contactOldOrNew: Contact = oldContact || {
|
||||||
did: decrMember.did,
|
did: decrMember.did,
|
||||||
name: decrMember.name,
|
name: decrMember.name
|
||||||
};
|
}
|
||||||
try {
|
try {
|
||||||
const result = await register(
|
const result = await register(
|
||||||
this.activeDid,
|
this.activeDid,
|
||||||
this.apiServer,
|
this.apiServer,
|
||||||
this.axios,
|
this.axios,
|
||||||
contactOldOrNew,
|
contactOldOrNew
|
||||||
);
|
)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
decrMember.isRegistered = true;
|
decrMember.isRegistered = true
|
||||||
if (oldContact) {
|
if (oldContact) {
|
||||||
await db.contacts.update(decrMember.did, { registered: true });
|
await db.contacts.update(decrMember.did, { registered: true })
|
||||||
oldContact.registered = true;
|
oldContact.registered = true
|
||||||
}
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "success",
|
type: 'success',
|
||||||
title: "Registered",
|
title: 'Registered',
|
||||||
text: "Besides being admitted, they were also registered.",
|
text: 'Besides being admitted, they were also registered.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
throw result;
|
throw result
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// registration failure is likely explained by a message from the server
|
// registration failure is likely explained by a message from the server
|
||||||
const additionalInfo =
|
const additionalInfo =
|
||||||
serverMessageForUser(error) || error?.error || "";
|
serverMessageForUser(error) || error?.error || ''
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "warning",
|
type: 'warning',
|
||||||
title: "Registration failed",
|
title: 'Registration failed',
|
||||||
text:
|
text:
|
||||||
"They were admitted to the meeting. However, registration failed. You can register them from the contacts screen. " +
|
'They were admitted to the meeting. However, registration failed. You can register them from the contacts screen. ' +
|
||||||
additionalInfo,
|
additionalInfo
|
||||||
},
|
},
|
||||||
12000,
|
12000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Error toggling admission: " + errorStringForLog(error),
|
'Error toggling admission: ' + errorStringForLog(error),
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
this.$emit(
|
this.$emit(
|
||||||
"error",
|
'error',
|
||||||
serverMessageForUser(error) ||
|
serverMessageForUser(error) ||
|
||||||
"Failed to update member admission status.",
|
'Failed to update member admission status.'
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,36 +491,36 @@ export default class MembersList extends Vue {
|
|||||||
try {
|
try {
|
||||||
const newContact = {
|
const newContact = {
|
||||||
did: member.did,
|
did: member.did,
|
||||||
name: member.name,
|
name: member.name
|
||||||
};
|
}
|
||||||
|
|
||||||
await db.contacts.add(newContact);
|
await db.contacts.add(newContact)
|
||||||
this.contacts.push(newContact);
|
this.contacts.push(newContact)
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "success",
|
type: 'success',
|
||||||
title: "Contact Added",
|
title: 'Contact Added',
|
||||||
text: "They were added to your contacts.",
|
text: 'They were added to your contacts.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logConsoleAndDb("Error adding contact: " + errorStringForLog(err), true);
|
logConsoleAndDb('Error adding contact: ' + errorStringForLog(err), true)
|
||||||
let message = "An error prevented adding this contact.";
|
let message = 'An error prevented adding this contact.'
|
||||||
if (err instanceof Error && err.message?.indexOf("already exists") > -1) {
|
if (err instanceof Error && err.message?.indexOf('already exists') > -1) {
|
||||||
message = "This person is already in your contact list.";
|
message = 'This person is already in your contact list.'
|
||||||
}
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Contact Not Added",
|
title: 'Contact Not Added',
|
||||||
text: message,
|
text: message
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,8 @@
|
|||||||
projectName,
|
projectName,
|
||||||
recipientDid,
|
recipientDid,
|
||||||
recipientName,
|
recipientName,
|
||||||
unitCode: amountUnitCode,
|
unitCode: amountUnitCode
|
||||||
},
|
}
|
||||||
}"
|
}"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
@@ -80,117 +80,114 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from 'vue-facing-decorator'
|
||||||
|
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../constants/app'
|
||||||
import {
|
import {
|
||||||
createAndSubmitOffer,
|
createAndSubmitOffer,
|
||||||
serverMessageForUser,
|
serverMessageForUser
|
||||||
} from "../libs/endorserServer";
|
} from '../libs/endorserServer'
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from '../libs/util'
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from '../db/index'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../utils/logger'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class OfferDialog extends Vue {
|
export default class OfferDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
@Prop projectId?: string;
|
@Prop projectId?: string
|
||||||
@Prop projectName?: string;
|
@Prop projectName?: string
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = ''
|
||||||
apiServer = "";
|
apiServer = ''
|
||||||
|
|
||||||
amountInput = "0";
|
amountInput = '0'
|
||||||
amountUnitCode = "HUR";
|
amountUnitCode = 'HUR'
|
||||||
description = "";
|
description = ''
|
||||||
expirationDateInput = "";
|
expirationDateInput = ''
|
||||||
recipientDid? = "";
|
recipientDid? = ''
|
||||||
recipientName? = "";
|
recipientName? = ''
|
||||||
visible = false;
|
visible = false
|
||||||
|
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil
|
||||||
|
|
||||||
async open(recipientDid?: string, recipientName?: string) {
|
async open(recipientDid?: string, recipientName?: string) {
|
||||||
try {
|
try {
|
||||||
this.recipientDid = recipientDid;
|
this.recipientDid = recipientDid
|
||||||
this.recipientName = recipientName;
|
this.recipientName = recipientName
|
||||||
|
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || ''
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || ''
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error("Error retrieving settings from database:", err);
|
logger.error('Error retrieving settings from database:', err)
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: err.message || "There was an error retrieving your settings.",
|
text: err.message || 'There was an error retrieving your settings.'
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
// close the dialog but don't change values (since it might be submitting info)
|
// close the dialog but don't change values (since it might be submitting info)
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
changeUnitCode() {
|
changeUnitCode() {
|
||||||
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
const units = Object.keys(this.libsUtil.UNIT_SHORT)
|
||||||
const index = units.indexOf(this.amountUnitCode);
|
const index = units.indexOf(this.amountUnitCode)
|
||||||
this.amountUnitCode = units[(index + 1) % units.length];
|
this.amountUnitCode = units[(index + 1) % units.length]
|
||||||
}
|
}
|
||||||
|
|
||||||
increment() {
|
increment() {
|
||||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`
|
||||||
}
|
}
|
||||||
|
|
||||||
decrement() {
|
decrement() {
|
||||||
this.amountInput = `${Math.max(
|
this.amountInput = `${Math.max(0, (parseFloat(this.amountInput) || 1) - 1)}`
|
||||||
0,
|
|
||||||
(parseFloat(this.amountInput) || 1) - 1,
|
|
||||||
)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
this.close();
|
this.close()
|
||||||
this.eraseValues();
|
this.eraseValues()
|
||||||
}
|
}
|
||||||
|
|
||||||
eraseValues() {
|
eraseValues() {
|
||||||
this.description = "";
|
this.description = ''
|
||||||
this.amountInput = "0";
|
this.amountInput = '0'
|
||||||
this.amountUnitCode = "HUR";
|
this.amountUnitCode = 'HUR'
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirm() {
|
async confirm() {
|
||||||
this.close();
|
this.close()
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "toast",
|
type: 'toast',
|
||||||
text: "Recording the offer...",
|
text: 'Recording the offer...',
|
||||||
title: "",
|
title: ''
|
||||||
},
|
},
|
||||||
1000,
|
1000
|
||||||
);
|
)
|
||||||
// this is asynchronous, but we don't need to wait for it to complete
|
// this is asynchronous, but we don't need to wait for it to complete
|
||||||
this.recordOffer(
|
this.recordOffer(
|
||||||
this.description,
|
this.description,
|
||||||
parseFloat(this.amountInput),
|
parseFloat(this.amountInput),
|
||||||
this.amountUnitCode,
|
this.amountUnitCode,
|
||||||
this.expirationDateInput,
|
this.expirationDateInput
|
||||||
).then(() => {
|
).then(() => {
|
||||||
this.description = "";
|
this.description = ''
|
||||||
this.amountInput = "0";
|
this.amountInput = '0'
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,33 +199,33 @@ export default class OfferDialog extends Vue {
|
|||||||
public async recordOffer(
|
public async recordOffer(
|
||||||
description: string,
|
description: string,
|
||||||
amount: number,
|
amount: number,
|
||||||
unitCode: string = "HUR",
|
unitCode: string = 'HUR',
|
||||||
expirationDateInput?: string,
|
expirationDateInput?: string
|
||||||
) {
|
) {
|
||||||
if (!this.activeDid) {
|
if (!this.activeDid) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: "You must select an identity before you can record an offer.",
|
text: 'You must select an identity before you can record an offer.'
|
||||||
},
|
},
|
||||||
7000,
|
7000
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!description && !amount) {
|
if (!description && !amount) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`,
|
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -239,54 +236,54 @@ export default class OfferDialog extends Vue {
|
|||||||
description,
|
description,
|
||||||
amount,
|
amount,
|
||||||
unitCode,
|
unitCode,
|
||||||
"",
|
'',
|
||||||
expirationDateInput,
|
expirationDateInput,
|
||||||
this.recipientDid,
|
this.recipientDid,
|
||||||
this.projectId,
|
this.projectId
|
||||||
);
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
result.type === "error" ||
|
result.type === 'error' ||
|
||||||
this.isOfferCreationError(result.response)
|
this.isOfferCreationError(result.response)
|
||||||
) {
|
) {
|
||||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
const errorMessage = this.getOfferCreationErrorMessage(result)
|
||||||
logger.error("Error with offer creation result:", result);
|
logger.error('Error with offer creation result:', result)
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: errorMessage || "There was an error creating the offer.",
|
text: errorMessage || 'There was an error creating the offer.'
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "success",
|
type: 'success',
|
||||||
title: "Success",
|
title: 'Success',
|
||||||
text: "That offer was recorded.",
|
text: 'That offer was recorded.'
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error("Error with offer recordation caught:", error);
|
logger.error('Error with offer recordation caught:', error)
|
||||||
const message =
|
const message =
|
||||||
error.userMessage ||
|
error.userMessage ||
|
||||||
error.response?.data?.error?.message ||
|
error.response?.data?.error?.message ||
|
||||||
"There was an error recording the offer.";
|
'There was an error recording the offer.'
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: message,
|
text: message
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +295,7 @@ export default class OfferDialog extends Vue {
|
|||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
isOfferCreationError(result: any) {
|
isOfferCreationError(result: any) {
|
||||||
return result.status !== 201 || result.data?.error;
|
return result.status !== 201 || result.data?.error
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -311,7 +308,7 @@ export default class OfferDialog extends Vue {
|
|||||||
serverMessageForUser(result) ||
|
serverMessageForUser(result) ||
|
||||||
result.error?.userMessage ||
|
result.error?.userMessage ||
|
||||||
result.error?.error
|
result.error?.error
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -198,64 +198,64 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from 'vue-facing-decorator'
|
||||||
import { Router } from "vue-router";
|
import { Router } from 'vue-router'
|
||||||
|
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../constants/app'
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
updateAccountSettings,
|
updateAccountSettings
|
||||||
} from "../db/index";
|
} from '../db/index'
|
||||||
import { OnboardPage } from "../libs/util";
|
import { OnboardPage } from '../libs/util'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
computed: {
|
computed: {
|
||||||
OnboardPage() {
|
OnboardPage() {
|
||||||
return OnboardPage;
|
return OnboardPage
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
components: { OnboardPage }
|
||||||
components: { OnboardPage },
|
|
||||||
})
|
})
|
||||||
export default class OnboardingDialog extends Vue {
|
export default class OnboardingDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
$router!: Router;
|
$router!: Router
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = ''
|
||||||
firstContactName = null;
|
firstContactName = null
|
||||||
givenName = "";
|
givenName = ''
|
||||||
isRegistered = false;
|
isRegistered = false
|
||||||
numContacts = 0;
|
numContacts = 0
|
||||||
page = OnboardPage.Home;
|
page = OnboardPage.Home
|
||||||
visible = false;
|
visible = false
|
||||||
|
|
||||||
async open(page: OnboardPage) {
|
async open(page: OnboardPage) {
|
||||||
this.page = page;
|
this.page = page
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || ''
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered
|
||||||
const contacts = await db.contacts.toArray();
|
const contacts = await db.contacts.toArray()
|
||||||
this.numContacts = contacts.length;
|
this.numContacts = contacts.length
|
||||||
if (this.numContacts > 0) {
|
if (this.numContacts > 0) {
|
||||||
this.firstContactName = contacts[0].name;
|
this.firstContactName = contacts[0].name
|
||||||
}
|
}
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
if (this.page === OnboardPage.Create) {
|
if (this.page === OnboardPage.Create) {
|
||||||
// we'll assume that they've been through all the other pages
|
// we'll assume that they've been through all the other pages
|
||||||
await updateAccountSettings(this.activeDid, {
|
await updateAccountSettings(this.activeDid, {
|
||||||
finishedOnboarding: true,
|
finishedOnboarding: true
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickClose(done?: boolean, goHome?: boolean) {
|
async onClickClose(done?: boolean, goHome?: boolean) {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
if (done) {
|
if (done) {
|
||||||
await updateAccountSettings(this.activeDid, {
|
await updateAccountSettings(this.activeDid, {
|
||||||
finishedOnboarding: true,
|
finishedOnboarding: true
|
||||||
});
|
})
|
||||||
if (goHome) {
|
if (goHome) {
|
||||||
this.$router.push({ name: "home" });
|
this.$router.push({ name: 'home' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
||||||
@click="close()"
|
@click="close()"
|
||||||
>
|
>
|
||||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
<font-awesome icon="xmark" class="w-[1em]" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@
|
|||||||
<VuePictureCropper
|
<VuePictureCropper
|
||||||
:box-style="{
|
:box-style="{
|
||||||
backgroundColor: '#f8f8f8',
|
backgroundColor: '#f8f8f8',
|
||||||
margin: 'auto',
|
margin: 'auto'
|
||||||
}"
|
}"
|
||||||
:img="createBlobURL(blob)"
|
:img="createBlobURL(blob)"
|
||||||
:options="{
|
:options="{
|
||||||
viewMode: 1,
|
viewMode: 1,
|
||||||
dragMode: 'crop',
|
dragMode: 'crop',
|
||||||
aspectRatio: 9 / 9,
|
aspectRatio: 9 / 9
|
||||||
}"
|
}"
|
||||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||||
/>
|
/>
|
||||||
@@ -100,47 +100,47 @@
|
|||||||
* @file PhotoDialog.vue
|
* @file PhotoDialog.vue
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from 'axios'
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from 'vue-facing-decorator'
|
||||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
import VuePictureCropper, { cropper } from 'vue-picture-cropper'
|
||||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from '../constants/app'
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from '../db/index'
|
||||||
import { accessToken } from "../libs/crypto";
|
import { accessToken } from '../libs/crypto'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../utils/logger'
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from '../services/PlatformServiceFactory'
|
||||||
|
|
||||||
@Component({ components: { VuePictureCropper } })
|
@Component({ components: { VuePictureCropper } })
|
||||||
export default class PhotoDialog extends Vue {
|
export default class PhotoDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = ''
|
||||||
blob?: Blob;
|
blob?: Blob
|
||||||
claimType = "";
|
claimType = ''
|
||||||
crop = false;
|
crop = false
|
||||||
fileName?: string;
|
fileName?: string
|
||||||
setImageCallback: (arg: string) => void = () => {};
|
setImageCallback: (arg: string) => void = () => {}
|
||||||
showRetry = true;
|
showRetry = true
|
||||||
uploading = false;
|
uploading = false
|
||||||
visible = false;
|
visible = false
|
||||||
|
|
||||||
private platformService = PlatformServiceFactory.getInstance();
|
private platformService = PlatformServiceFactory.getInstance()
|
||||||
URL = window.URL || window.webkitURL;
|
URL = window.URL || window.webkitURL
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || ''
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
logger.error("Error retrieving settings from database:", err);
|
logger.error('Error retrieving settings from database:', err)
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: err.message || "There was an error retrieving your settings.",
|
text: err.message || 'There was an error retrieving your settings.'
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,140 +149,140 @@ export default class PhotoDialog extends Vue {
|
|||||||
claimType: string,
|
claimType: string,
|
||||||
crop?: boolean,
|
crop?: boolean,
|
||||||
blob?: Blob,
|
blob?: Blob,
|
||||||
inputFileName?: string,
|
inputFileName?: string
|
||||||
) {
|
) {
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
this.claimType = claimType;
|
this.claimType = claimType
|
||||||
this.crop = !!crop;
|
this.crop = !!crop
|
||||||
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
const bottomNav = document.querySelector('#QuickNav') as HTMLElement
|
||||||
if (bottomNav) {
|
if (bottomNav) {
|
||||||
bottomNav.style.display = "none";
|
bottomNav.style.display = 'none'
|
||||||
}
|
}
|
||||||
this.setImageCallback = setImageFn;
|
this.setImageCallback = setImageFn
|
||||||
if (blob) {
|
if (blob) {
|
||||||
this.blob = blob;
|
this.blob = blob
|
||||||
this.fileName = inputFileName;
|
this.fileName = inputFileName
|
||||||
this.showRetry = false;
|
this.showRetry = false
|
||||||
} else {
|
} else {
|
||||||
this.blob = undefined;
|
this.blob = undefined
|
||||||
this.fileName = undefined;
|
this.fileName = undefined
|
||||||
this.showRetry = true;
|
this.showRetry = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
const bottomNav = document.querySelector('#QuickNav') as HTMLElement
|
||||||
if (bottomNav) {
|
if (bottomNav) {
|
||||||
bottomNav.style.display = "";
|
bottomNav.style.display = ''
|
||||||
}
|
}
|
||||||
this.blob = undefined;
|
this.blob = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async takePhoto() {
|
async takePhoto() {
|
||||||
try {
|
try {
|
||||||
const result = await this.platformService.takePicture();
|
const result = await this.platformService.takePicture()
|
||||||
this.blob = result.blob;
|
this.blob = result.blob
|
||||||
this.fileName = result.fileName;
|
this.fileName = result.fileName
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error("Error taking picture:", error);
|
logger.error('Error taking picture:', error)
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: "Failed to take picture. Please try again.",
|
text: 'Failed to take picture. Please try again.'
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async pickPhoto() {
|
async pickPhoto() {
|
||||||
try {
|
try {
|
||||||
const result = await this.platformService.pickImage();
|
const result = await this.platformService.pickImage()
|
||||||
this.blob = result.blob;
|
this.blob = result.blob
|
||||||
this.fileName = result.fileName;
|
this.fileName = result.fileName
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error("Error picking image:", error);
|
logger.error('Error picking image:', error)
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: "Failed to pick image. Please try again.",
|
text: 'Failed to pick image. Please try again.'
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private createBlobURL(blob: Blob): string {
|
private createBlobURL(blob: Blob): string {
|
||||||
return URL.createObjectURL(blob);
|
return URL.createObjectURL(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
async retryImage() {
|
async retryImage() {
|
||||||
this.blob = undefined;
|
this.blob = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadImage() {
|
async uploadImage() {
|
||||||
this.uploading = true;
|
this.uploading = true
|
||||||
|
|
||||||
if (this.crop) {
|
if (this.crop) {
|
||||||
this.blob = (await cropper?.getBlob()) || undefined;
|
this.blob = (await cropper?.getBlob()) || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await accessToken(this.activeDid);
|
const token = await accessToken(this.activeDid)
|
||||||
const headers = {
|
const headers = {
|
||||||
Authorization: "Bearer " + token,
|
Authorization: 'Bearer ' + token
|
||||||
};
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData()
|
||||||
if (!this.blob) {
|
if (!this.blob) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: "There was an error finding the picture. Please try again.",
|
text: 'There was an error finding the picture. Please try again.'
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
this.uploading = false;
|
this.uploading = false
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
formData.append('image', this.blob, this.fileName || 'photo.jpg')
|
||||||
formData.append("claimType", this.claimType);
|
formData.append('claimType', this.claimType)
|
||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
window.location.hostname === "localhost" &&
|
window.location.hostname === 'localhost' &&
|
||||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
!DEFAULT_IMAGE_API_SERVER.includes('localhost')
|
||||||
) {
|
) {
|
||||||
logger.log(
|
logger.log(
|
||||||
"Using shared image API server, so only users on that server can play with images.",
|
'Using shared image API server, so only users on that server can play with images.'
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
DEFAULT_IMAGE_API_SERVER + "/image",
|
DEFAULT_IMAGE_API_SERVER + '/image',
|
||||||
formData,
|
formData,
|
||||||
{ headers },
|
{ headers }
|
||||||
);
|
)
|
||||||
this.uploading = false;
|
this.uploading = false
|
||||||
|
|
||||||
this.close();
|
this.close()
|
||||||
this.setImageCallback(response.data.url as string);
|
this.setImageCallback(response.data.url as string)
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Log the raw error first
|
// Log the raw error first
|
||||||
logger.error("Raw error object:", JSON.stringify(error, null, 2));
|
logger.error('Raw error object:', JSON.stringify(error, null, 2))
|
||||||
|
|
||||||
let errorMessage = "There was an error saving the picture.";
|
let errorMessage = 'There was an error saving the picture.'
|
||||||
|
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
const status = error.response?.status;
|
const status = error.response?.status
|
||||||
const statusText = error.response?.statusText;
|
const statusText = error.response?.statusText
|
||||||
const data = error.response?.data;
|
const data = error.response?.data
|
||||||
|
|
||||||
// Log detailed error information
|
// Log detailed error information
|
||||||
logger.error("Upload error details:", {
|
logger.error('Upload error details:', {
|
||||||
status,
|
status,
|
||||||
statusText,
|
statusText,
|
||||||
data: JSON.stringify(data, null, 2),
|
data: JSON.stringify(data, null, 2),
|
||||||
@@ -290,49 +290,49 @@ export default class PhotoDialog extends Vue {
|
|||||||
config: {
|
config: {
|
||||||
url: error.config?.url,
|
url: error.config?.url,
|
||||||
method: error.config?.method,
|
method: error.config?.method,
|
||||||
headers: error.config?.headers,
|
headers: error.config?.headers
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
errorMessage = "Authentication failed. Please try logging in again.";
|
errorMessage = 'Authentication failed. Please try logging in again.'
|
||||||
} else if (status === 413) {
|
} else if (status === 413) {
|
||||||
errorMessage = "Image file is too large. Please try a smaller image.";
|
errorMessage = 'Image file is too large. Please try a smaller image.'
|
||||||
} else if (status === 415) {
|
} else if (status === 415) {
|
||||||
errorMessage =
|
errorMessage =
|
||||||
"Unsupported image format. Please try a different image.";
|
'Unsupported image format. Please try a different image.'
|
||||||
} else if (status && status >= 500) {
|
} else if (status && status >= 500) {
|
||||||
errorMessage = "Server error. Please try again later.";
|
errorMessage = 'Server error. Please try again later.'
|
||||||
} else if (data?.message) {
|
} else if (data?.message) {
|
||||||
errorMessage = data.message;
|
errorMessage = data.message
|
||||||
}
|
}
|
||||||
} else if (error instanceof Error) {
|
} else if (error instanceof Error) {
|
||||||
// Log non-Axios error with full details
|
// Log non-Axios error with full details
|
||||||
logger.error("Non-Axios error details:", {
|
logger.error('Non-Axios error details:', {
|
||||||
name: error.name,
|
name: error.name,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
stack: error.stack,
|
stack: error.stack,
|
||||||
error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
|
error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
// Log any other type of error
|
// Log any other type of error
|
||||||
logger.error("Unknown error type:", {
|
logger.error('Unknown error type:', {
|
||||||
error: JSON.stringify(error, null, 2),
|
error: JSON.stringify(error, null, 2),
|
||||||
type: typeof error,
|
type: typeof error
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: errorMessage,
|
text: errorMessage
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
this.uploading = false;
|
this.uploading = false
|
||||||
this.blob = undefined;
|
this.blob = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,35 +15,35 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toSvg } from "jdenticon";
|
import { toSvg } from 'jdenticon'
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from 'vue-facing-decorator'
|
||||||
|
|
||||||
const BLANK_CONFIG = {
|
const BLANK_CONFIG = {
|
||||||
lightness: {
|
lightness: {
|
||||||
color: [1.0, 1.0],
|
color: [1.0, 1.0],
|
||||||
grayscale: [1.0, 1.0],
|
grayscale: [1.0, 1.0]
|
||||||
},
|
},
|
||||||
saturation: {
|
saturation: {
|
||||||
color: 0.0,
|
color: 0.0,
|
||||||
grayscale: 0.0,
|
grayscale: 0.0
|
||||||
},
|
},
|
||||||
backColor: "#0000",
|
backColor: '#0000'
|
||||||
};
|
}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class ProjectIcon extends Vue {
|
export default class ProjectIcon extends Vue {
|
||||||
@Prop entityId = "";
|
@Prop entityId = ''
|
||||||
@Prop iconSize = 0;
|
@Prop iconSize = 0
|
||||||
@Prop imageUrl = "";
|
@Prop imageUrl = ''
|
||||||
@Prop linkToFull = false;
|
@Prop linkToFull = false
|
||||||
|
|
||||||
generateIdenticon() {
|
generateIdenticon() {
|
||||||
if (this.imageUrl) {
|
if (this.imageUrl) {
|
||||||
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
|
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`
|
||||||
} else {
|
} else {
|
||||||
const config = this.entityId ? undefined : BLANK_CONFIG;
|
const config = this.entityId ? undefined : BLANK_CONFIG
|
||||||
const svgString = toSvg(this.entityId, this.iconSize, config);
|
const svgString = toSvg(this.entityId, this.iconSize, config)
|
||||||
return svgString;
|
return svgString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
>
|
>
|
||||||
<!-- eslint-enable -->
|
<!-- eslint-enable -->
|
||||||
<span class="w-full flex justify-between text-xs text-slate-500">
|
<span class="w-full flex justify-between text-xs text-slate-500">
|
||||||
<span></span>
|
<span />
|
||||||
<span>(100 characters max)</span>
|
<span>(100 characters max)</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
<button
|
<button
|
||||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white mt-2 px-2 py-2 rounded-md"
|
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white mt-2 px-2 py-2 rounded-md"
|
||||||
@click="
|
@click="
|
||||||
close();
|
close()
|
||||||
turnOnNotifications();
|
turnOnNotifications()
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Turn on Daily Message
|
Turn on Daily Message
|
||||||
@@ -100,46 +100,46 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from 'vue-facing-decorator'
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
import { DEFAULT_PUSH_SERVER, NotificationIface } from '../constants/app'
|
||||||
import {
|
import {
|
||||||
logConsoleAndDb,
|
logConsoleAndDb,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
secretDB,
|
secretDB
|
||||||
} from "../db/index";
|
} from '../db/index'
|
||||||
import { MASTER_SECRET_KEY } from "../db/tables/secret";
|
import { MASTER_SECRET_KEY } from '../db/tables/secret'
|
||||||
import { urlBase64ToUint8Array } from "../libs/crypto/vc/util";
|
import { urlBase64ToUint8Array } from '../libs/crypto/vc/util'
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from '../libs/util'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../utils/logger'
|
||||||
|
|
||||||
// Example interface for error
|
// Example interface for error
|
||||||
interface ErrorResponse {
|
interface ErrorResponse {
|
||||||
message: string;
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushSubscriptionJSON is defined in the Push API https://www.w3.org/TR/push-api/#dom-pushsubscriptionjson
|
// PushSubscriptionJSON is defined in the Push API https://www.w3.org/TR/push-api/#dom-pushsubscriptionjson
|
||||||
interface PushSubscriptionWithTime extends PushSubscriptionJSON {
|
interface PushSubscriptionWithTime extends PushSubscriptionJSON {
|
||||||
message?: string;
|
message?: string
|
||||||
notifyTime: { utcHour: number; minute: number };
|
notifyTime: { utcHour: number; minute: number }
|
||||||
notifyType: string;
|
notifyType: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceWorkerMessage {
|
interface ServiceWorkerMessage {
|
||||||
type: string;
|
type: string
|
||||||
data: string;
|
data: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ServiceWorkerResponse {
|
interface ServiceWorkerResponse {
|
||||||
// Define the properties and their types
|
// Define the properties and their types
|
||||||
success: boolean;
|
success: boolean
|
||||||
message?: string;
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VapidResponse {
|
interface VapidResponse {
|
||||||
data: {
|
data: {
|
||||||
vapidKey: string;
|
vapidKey: string
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -147,135 +147,135 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => Promise<() => void>;
|
$notify!: (notification: NotificationIface, timeout?: number) => Promise<() => void>;
|
||||||
|
|
||||||
DAILY_CHECK_TITLE = libsUtil.DAILY_CHECK_TITLE;
|
DAILY_CHECK_TITLE = libsUtil.DAILY_CHECK_TITLE
|
||||||
DIRECT_PUSH_TITLE = libsUtil.DIRECT_PUSH_TITLE;
|
DIRECT_PUSH_TITLE = libsUtil.DIRECT_PUSH_TITLE
|
||||||
|
|
||||||
callback: (success: boolean, time: string, message?: string) => void =
|
callback: (success: boolean, time: string, message?: string) => void =
|
||||||
() => {};
|
() => {}
|
||||||
hourAm = true;
|
hourAm = true
|
||||||
hourInput = "8";
|
hourInput = '8'
|
||||||
isVisible = false;
|
isVisible = false
|
||||||
messageInput = "";
|
messageInput = ''
|
||||||
minuteInput = "00";
|
minuteInput = '00'
|
||||||
pushType = "";
|
pushType = ''
|
||||||
serviceWorkerReady = false;
|
serviceWorkerReady = false
|
||||||
vapidKey = "";
|
vapidKey = ''
|
||||||
|
|
||||||
async open(
|
async open(
|
||||||
pushType: string,
|
pushType: string,
|
||||||
callback?: (success: boolean, time: string, message?: string) => void,
|
callback?: (success: boolean, time: string, message?: string) => void
|
||||||
) {
|
) {
|
||||||
this.callback = callback || this.callback;
|
this.callback = callback || this.callback
|
||||||
this.isVisible = true;
|
this.isVisible = true
|
||||||
this.pushType = pushType;
|
this.pushType = pushType
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
let pushUrl = DEFAULT_PUSH_SERVER;
|
let pushUrl = DEFAULT_PUSH_SERVER
|
||||||
if (settings?.webPushServer) {
|
if (settings?.webPushServer) {
|
||||||
pushUrl = settings.webPushServer;
|
pushUrl = settings.webPushServer
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pushUrl.startsWith("http://localhost")) {
|
if (pushUrl.startsWith('http://localhost')) {
|
||||||
logConsoleAndDb("Not checking for VAPID in this local environment.");
|
logConsoleAndDb('Not checking for VAPID in this local environment.')
|
||||||
} else {
|
} else {
|
||||||
let responseData = "";
|
let responseData = ''
|
||||||
await this.axios
|
await this.axios
|
||||||
.get(pushUrl + "/web-push/vapid")
|
.get(pushUrl + '/web-push/vapid')
|
||||||
.then((response: VapidResponse) => {
|
.then((response: VapidResponse) => {
|
||||||
this.vapidKey = response.data?.vapidKey || "";
|
this.vapidKey = response.data?.vapidKey || ''
|
||||||
logConsoleAndDb("Got vapid key: " + this.vapidKey);
|
logConsoleAndDb('Got vapid key: ' + this.vapidKey)
|
||||||
responseData = JSON.stringify(response.data);
|
responseData = JSON.stringify(response.data)
|
||||||
navigator.serviceWorker?.addEventListener(
|
navigator.serviceWorker?.addEventListener(
|
||||||
"controllerchange",
|
'controllerchange',
|
||||||
() => {
|
() => {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"New service worker is now controlling the page",
|
'New service worker is now controlling the page'
|
||||||
);
|
)
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
if (!this.vapidKey) {
|
if (!this.vapidKey) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error Setting Notifications",
|
title: 'Error Setting Notifications',
|
||||||
text: "Could not set notifications.",
|
text: 'Could not set notifications.'
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Error Setting Notifications: web push server response didn't have vapidKey: " +
|
"Error Setting Notifications: web push server response didn't have vapidKey: " +
|
||||||
responseData,
|
responseData,
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (window.location.host.startsWith("localhost")) {
|
if (window.location.host.startsWith('localhost')) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Ignoring the error getting VAPID for local development.",
|
'Ignoring the error getting VAPID for local development.'
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Got an error initializing notifications: " + JSON.stringify(error),
|
'Got an error initializing notifications: ' + JSON.stringify(error),
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error Setting Notifications",
|
title: 'Error Setting Notifications',
|
||||||
text: "Got an error setting notifications.",
|
text: 'Got an error setting notifications.'
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// there may be a long pause here on first initialization
|
// there may be a long pause here on first initialization
|
||||||
navigator.serviceWorker?.ready.then(() => {
|
navigator.serviceWorker?.ready.then(() => {
|
||||||
this.serviceWorkerReady = true;
|
this.serviceWorkerReady = true
|
||||||
});
|
})
|
||||||
|
|
||||||
if (this.pushType === this.DIRECT_PUSH_TITLE) {
|
if (this.pushType === this.DIRECT_PUSH_TITLE) {
|
||||||
this.messageInput =
|
this.messageInput =
|
||||||
"Click to share some gratitude with the world -- even if they're unnamed.";
|
"Click to share some gratitude with the world -- even if they're unnamed."
|
||||||
// focus on the message input
|
// focus on the message input
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
document.getElementById("push-message")?.focus();
|
document.getElementById('push-message')?.focus()
|
||||||
}, 100);
|
}, 100)
|
||||||
} else {
|
} else {
|
||||||
// not critical but doesn't make sense in a daily check
|
// not critical but doesn't make sense in a daily check
|
||||||
this.messageInput = "";
|
this.messageInput = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private close() {
|
private close() {
|
||||||
this.isVisible = false;
|
this.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendMessageToServiceWorker(
|
private sendMessageToServiceWorker(
|
||||||
message: ServiceWorkerMessage,
|
message: ServiceWorkerMessage
|
||||||
): Promise<unknown> {
|
): Promise<unknown> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (navigator.serviceWorker?.controller) {
|
if (navigator.serviceWorker?.controller) {
|
||||||
const messageChannel = new MessageChannel();
|
const messageChannel = new MessageChannel()
|
||||||
|
|
||||||
messageChannel.port1.onmessage = (event: MessageEvent) => {
|
messageChannel.port1.onmessage = (event: MessageEvent) => {
|
||||||
if (event.data.error) {
|
if (event.data.error) {
|
||||||
reject(event.data.error as ErrorResponse);
|
reject(event.data.error as ErrorResponse)
|
||||||
} else {
|
} else {
|
||||||
resolve(event.data as ServiceWorkerResponse);
|
resolve(event.data as ServiceWorkerResponse)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
navigator.serviceWorker?.controller.postMessage(message, [
|
navigator.serviceWorker?.controller.postMessage(message, [
|
||||||
messageChannel.port2,
|
messageChannel.port2
|
||||||
]);
|
])
|
||||||
} else {
|
} else {
|
||||||
reject("Service worker controller not available");
|
reject('Service worker controller not available')
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private async askPermission(): Promise<NotificationPermission> {
|
private async askPermission(): Promise<NotificationPermission> {
|
||||||
@@ -283,136 +283,136 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
// "Requesting permission for notifications: " + JSON.stringify(navigator),
|
// "Requesting permission for notifications: " + JSON.stringify(navigator),
|
||||||
// );
|
// );
|
||||||
if (
|
if (
|
||||||
!("serviceWorker" in navigator && navigator.serviceWorker?.controller)
|
!('serviceWorker' in navigator && navigator.serviceWorker?.controller)
|
||||||
) {
|
) {
|
||||||
return Promise.reject("Service worker not available.");
|
return Promise.reject('Service worker not available.')
|
||||||
}
|
}
|
||||||
|
|
||||||
await secretDB.open();
|
await secretDB.open()
|
||||||
const secret = (await secretDB.secret.get(MASTER_SECRET_KEY))?.secret;
|
const secret = (await secretDB.secret.get(MASTER_SECRET_KEY))?.secret
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
return Promise.reject("No secret found.");
|
return Promise.reject('No secret found.')
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.sendSecretToServiceWorker(secret)
|
return this.sendSecretToServiceWorker(secret)
|
||||||
.then(() => this.checkNotificationSupport())
|
.then(() => this.checkNotificationSupport())
|
||||||
.then(() => this.requestNotificationPermission())
|
.then(() => this.requestNotificationPermission())
|
||||||
.catch((error) => Promise.reject(error));
|
.catch((error) => Promise.reject(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendSecretToServiceWorker(secret: string): Promise<void> {
|
private sendSecretToServiceWorker(secret: string): Promise<void> {
|
||||||
const message: ServiceWorkerMessage = {
|
const message: ServiceWorkerMessage = {
|
||||||
type: "SEND_LOCAL_DATA",
|
type: 'SEND_LOCAL_DATA',
|
||||||
data: secret,
|
data: secret
|
||||||
};
|
}
|
||||||
|
|
||||||
return this.sendMessageToServiceWorker(message).then((response) => {
|
return this.sendMessageToServiceWorker(message).then((response) => {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Response from service worker: " + JSON.stringify(response),
|
'Response from service worker: ' + JSON.stringify(response)
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkNotificationSupport(): Promise<void> {
|
private checkNotificationSupport(): Promise<void> {
|
||||||
if (!("Notification" in window)) {
|
if (!('Notification' in window)) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Browser Notifications Are Not Supported",
|
title: 'Browser Notifications Are Not Supported',
|
||||||
text: "This browser does not support notifications.",
|
text: 'This browser does not support notifications.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
return Promise.reject("This browser does not support notifications.");
|
return Promise.reject('This browser does not support notifications.')
|
||||||
}
|
}
|
||||||
if (window.Notification.permission === "granted") {
|
if (window.Notification.permission === 'granted') {
|
||||||
return Promise.resolve();
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
private requestNotificationPermission(): Promise<NotificationPermission> {
|
private requestNotificationPermission(): Promise<NotificationPermission> {
|
||||||
return window.Notification.requestPermission().then(
|
return window.Notification.requestPermission().then(
|
||||||
(permission: string) => {
|
(permission: string) => {
|
||||||
if (permission !== "granted") {
|
if (permission !== 'granted') {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error Requesting Notification Permission",
|
title: 'Error Requesting Notification Permission',
|
||||||
text:
|
text:
|
||||||
"Allow this app permission to make notifications for personal reminders." +
|
'Allow this app permission to make notifications for personal reminders.' +
|
||||||
" You can adjust them at any time in your settings.",
|
' You can adjust them at any time in your settings.'
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
throw new Error("Permission was not granted to this app.");
|
throw new Error('Permission was not granted to this app.')
|
||||||
}
|
}
|
||||||
return permission;
|
return permission
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkHourInput() {
|
private checkHourInput() {
|
||||||
const hourNum = parseInt(this.hourInput);
|
const hourNum = parseInt(this.hourInput)
|
||||||
if (isNaN(hourNum)) {
|
if (isNaN(hourNum)) {
|
||||||
this.hourInput = "12";
|
this.hourInput = '12'
|
||||||
} else if (hourNum < 1) {
|
} else if (hourNum < 1) {
|
||||||
this.hourInput = "12";
|
this.hourInput = '12'
|
||||||
this.hourAm = !this.hourAm;
|
this.hourAm = !this.hourAm
|
||||||
} else if (hourNum > 12) {
|
} else if (hourNum > 12) {
|
||||||
this.hourInput = "1";
|
this.hourInput = '1'
|
||||||
this.hourAm = !this.hourAm;
|
this.hourAm = !this.hourAm
|
||||||
} else {
|
} else {
|
||||||
this.hourInput = hourNum.toString();
|
this.hourInput = hourNum.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkMinuteInput() {
|
private checkMinuteInput() {
|
||||||
const minuteNum = parseInt(this.minuteInput);
|
const minuteNum = parseInt(this.minuteInput)
|
||||||
if (isNaN(minuteNum)) {
|
if (isNaN(minuteNum)) {
|
||||||
this.minuteInput = "00";
|
this.minuteInput = '00'
|
||||||
} else if (minuteNum < 0) {
|
} else if (minuteNum < 0) {
|
||||||
this.minuteInput = "59";
|
this.minuteInput = '59'
|
||||||
} else if (minuteNum < 10) {
|
} else if (minuteNum < 10) {
|
||||||
this.minuteInput = "0" + minuteNum;
|
this.minuteInput = '0' + minuteNum
|
||||||
} else if (minuteNum > 59) {
|
} else if (minuteNum > 59) {
|
||||||
this.minuteInput = "00";
|
this.minuteInput = '00'
|
||||||
} else {
|
} else {
|
||||||
this.minuteInput = minuteNum.toString();
|
this.minuteInput = minuteNum.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async turnOnNotifications() {
|
private async turnOnNotifications() {
|
||||||
let notifyCloser = () => {};
|
let notifyCloser = () => {}
|
||||||
return this.askPermission()
|
return this.askPermission()
|
||||||
.then((permission) => {
|
.then((permission) => {
|
||||||
logConsoleAndDb("Permission granted: " + JSON.stringify(permission));
|
logConsoleAndDb('Permission granted: ' + JSON.stringify(permission))
|
||||||
|
|
||||||
// Call the function and handle promises
|
// Call the function and handle promises
|
||||||
return this.subscribeToPush();
|
return this.subscribeToPush()
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logConsoleAndDb("Subscribed successfully.");
|
logConsoleAndDb('Subscribed successfully.')
|
||||||
return navigator.serviceWorker?.ready;
|
return navigator.serviceWorker?.ready
|
||||||
})
|
})
|
||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
return registration.pushManager.getSubscription();
|
return registration.pushManager.getSubscription()
|
||||||
})
|
})
|
||||||
.then(async (subscription) => {
|
.then(async (subscription) => {
|
||||||
if (subscription) {
|
if (subscription) {
|
||||||
notifyCloser = await this.$notify(
|
notifyCloser = await this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Notification Setup Underway",
|
title: 'Notification Setup Underway',
|
||||||
text: "Setting up notifications for interesting activity, which takes about 10 seconds. If you don't see a final confirmation, check the 'Troubleshoot' page.",
|
text: "Setting up notifications for interesting activity, which takes about 10 seconds. If you don't see a final confirmation, check the 'Troubleshoot' page."
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
// we already checked that this is a valid hour number
|
// we already checked that this is a valid hour number
|
||||||
const rawHourNum = libsUtil.numberOrZero(this.hourInput);
|
const rawHourNum = libsUtil.numberOrZero(this.hourInput)
|
||||||
const adjHourNum = this.hourAm
|
const adjHourNum = this.hourAm
|
||||||
? // If it's AM, then we'll change it to 0 for 12 AM but otherwise use rawHourNum
|
? // If it's AM, then we'll change it to 0 for 12 AM but otherwise use rawHourNum
|
||||||
rawHourNum === 12
|
rawHourNum === 12
|
||||||
@@ -421,153 +421,153 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
: // Otherwise it's PM, so keep a 12 but otherwise add 12
|
: // Otherwise it's PM, so keep a 12 but otherwise add 12
|
||||||
rawHourNum === 12
|
rawHourNum === 12
|
||||||
? 12
|
? 12
|
||||||
: rawHourNum + 12;
|
: rawHourNum + 12
|
||||||
const hourNum = adjHourNum % 24; // probably unnecessary now
|
const hourNum = adjHourNum % 24 // probably unnecessary now
|
||||||
const utcHour =
|
const utcHour =
|
||||||
hourNum + Math.round(new Date().getTimezoneOffset() / 60);
|
hourNum + Math.round(new Date().getTimezoneOffset() / 60)
|
||||||
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24;
|
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24
|
||||||
const minuteNum = libsUtil.numberOrZero(this.minuteInput);
|
const minuteNum = libsUtil.numberOrZero(this.minuteInput)
|
||||||
const utcMinute =
|
const utcMinute =
|
||||||
minuteNum + Math.round(new Date().getTimezoneOffset() % 60);
|
minuteNum + Math.round(new Date().getTimezoneOffset() % 60)
|
||||||
const finalUtcMinute = (utcMinute + (utcMinute < 0 ? 60 : 0)) % 60;
|
const finalUtcMinute = (utcMinute + (utcMinute < 0 ? 60 : 0)) % 60
|
||||||
|
|
||||||
const subscriptionWithTime: PushSubscriptionWithTime = {
|
const subscriptionWithTime: PushSubscriptionWithTime = {
|
||||||
notifyTime: { utcHour: finalUtcHour, minute: finalUtcMinute },
|
notifyTime: { utcHour: finalUtcHour, minute: finalUtcMinute },
|
||||||
notifyType: this.pushType,
|
notifyType: this.pushType,
|
||||||
message: this.messageInput,
|
message: this.messageInput,
|
||||||
...subscription.toJSON(),
|
...subscription.toJSON()
|
||||||
};
|
}
|
||||||
await this.sendSubscriptionToServer(subscriptionWithTime);
|
await this.sendSubscriptionToServer(subscriptionWithTime)
|
||||||
// To help investigate potential issues with this: https://firebase.google.com/docs/cloud-messaging/migrate-v1
|
// To help investigate potential issues with this: https://firebase.google.com/docs/cloud-messaging/migrate-v1
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Subscription data sent to server with endpoint: " +
|
'Subscription data sent to server with endpoint: ' +
|
||||||
subscription.endpoint,
|
subscription.endpoint
|
||||||
);
|
)
|
||||||
return subscriptionWithTime;
|
return subscriptionWithTime
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Subscription object is not available.");
|
throw new Error('Subscription object is not available.')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(async (subscription: PushSubscriptionWithTime) => {
|
.then(async (subscription: PushSubscriptionWithTime) => {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Subscription data sent to server and all finished successfully.",
|
'Subscription data sent to server and all finished successfully.'
|
||||||
);
|
)
|
||||||
await libsUtil.sendTestThroughPushServer(subscription, true);
|
await libsUtil.sendTestThroughPushServer(subscription, true)
|
||||||
notifyCloser();
|
notifyCloser()
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "success",
|
type: 'success',
|
||||||
title: "Notification Is On",
|
title: 'Notification Is On',
|
||||||
text: "You should see at least one on your device; if not, check the 'Troubleshoot' link.",
|
text: "You should see at least one on your device; if not, check the 'Troubleshoot' link."
|
||||||
},
|
},
|
||||||
7000,
|
7000
|
||||||
);
|
)
|
||||||
}, 500);
|
}, 500)
|
||||||
const timeText =
|
const timeText =
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
this.hourInput + ":" + this.minuteInput + " " + (this.hourAm ? "AM" : "PM");
|
this.hourInput + ":" + this.minuteInput + " " + (this.hourAm ? "AM" : "PM");
|
||||||
this.callback(true, timeText, this.messageInput);
|
this.callback(true, timeText, this.messageInput)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Got an error setting notification permissions: " +
|
'Got an error setting notification permissions: ' +
|
||||||
" string " +
|
' string ' +
|
||||||
error.toString() +
|
error.toString() +
|
||||||
" JSON " +
|
' JSON ' +
|
||||||
JSON.stringify(error),
|
JSON.stringify(error),
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error Setting Notification Permissions",
|
title: 'Error Setting Notification Permissions',
|
||||||
text: "Could not set notification permissions.",
|
text: 'Could not set notification permissions.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
// if we want to also unsubscribe, be sure to do that only if no other notification is active
|
// if we want to also unsubscribe, be sure to do that only if no other notification is active
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToPush(): Promise<void> {
|
private subscribeToPush(): Promise<void> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (!("serviceWorker" in navigator && "PushManager" in window)) {
|
if (!('serviceWorker' in navigator && 'PushManager' in window)) {
|
||||||
const errorMsg = "Push messaging is not supported";
|
const errorMsg = 'Push messaging is not supported'
|
||||||
logger.warn(errorMsg);
|
logger.warn(errorMsg)
|
||||||
return reject(new Error(errorMsg));
|
return reject(new Error(errorMsg))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.Notification.permission !== "granted") {
|
if (window.Notification.permission !== 'granted') {
|
||||||
const errorMsg = "Notification permission not granted";
|
const errorMsg = 'Notification permission not granted'
|
||||||
logger.warn(errorMsg);
|
logger.warn(errorMsg)
|
||||||
return reject(new Error(errorMsg));
|
return reject(new Error(errorMsg))
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationServerKey = urlBase64ToUint8Array(this.vapidKey);
|
const applicationServerKey = urlBase64ToUint8Array(this.vapidKey)
|
||||||
const options: PushSubscriptionOptions = {
|
const options: PushSubscriptionOptions = {
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
applicationServerKey: applicationServerKey,
|
applicationServerKey: applicationServerKey
|
||||||
};
|
}
|
||||||
|
|
||||||
navigator.serviceWorker?.ready
|
navigator.serviceWorker?.ready
|
||||||
.then((registration) => {
|
.then((registration) => {
|
||||||
return registration.pushManager.subscribe(options);
|
return registration.pushManager.subscribe(options)
|
||||||
})
|
})
|
||||||
.then((subscription) => {
|
.then((subscription) => {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Push subscription successful: " + JSON.stringify(subscription),
|
'Push subscription successful: ' + JSON.stringify(subscription)
|
||||||
);
|
)
|
||||||
resolve();
|
resolve()
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Push subscription failed: " +
|
'Push subscription failed: ' +
|
||||||
JSON.stringify(error) +
|
JSON.stringify(error) +
|
||||||
" - " +
|
' - ' +
|
||||||
JSON.stringify(options),
|
JSON.stringify(options),
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
|
|
||||||
// Inform the user about the issue
|
// Inform the user about the issue
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error Setting Push Notifications",
|
title: 'Error Setting Push Notifications',
|
||||||
text:
|
text:
|
||||||
"We encountered an issue setting up push notifications. " +
|
'We encountered an issue setting up push notifications. ' +
|
||||||
"If you wish to revoke notification permissions, please do so in your browser settings.",
|
'If you wish to revoke notification permissions, please do so in your browser settings.'
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
|
|
||||||
reject(error);
|
reject(error)
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendSubscriptionToServer(
|
private sendSubscriptionToServer(
|
||||||
subscription: PushSubscriptionWithTime,
|
subscription: PushSubscriptionWithTime
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"About to send subscription... " + JSON.stringify(subscription),
|
'About to send subscription... ' + JSON.stringify(subscription)
|
||||||
);
|
)
|
||||||
return fetch("/web-push/subscribe", {
|
return fetch('/web-push/subscribe', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(subscription),
|
body: JSON.stringify(subscription)
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
logger.error("Bad response subscribing to web push: ", response);
|
logger.error('Bad response subscribing to web push: ', response)
|
||||||
throw new Error("Failed to send push subscription to server");
|
throw new Error('Failed to send push subscription to server')
|
||||||
}
|
}
|
||||||
logConsoleAndDb("Push subscription sent to server successfully.");
|
logConsoleAndDb('Push subscription sent to server successfully.')
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
124
src/components/QRScanner/CapacitorScanner.ts
Normal file
124
src/components/QRScanner/CapacitorScanner.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import {
|
||||||
|
BarcodeScanner,
|
||||||
|
BarcodeFormat,
|
||||||
|
LensFacing,
|
||||||
|
ScanResult
|
||||||
|
} from '@capacitor-mlkit/barcode-scanning'
|
||||||
|
import type { QRScannerService, ScanListener } from './types'
|
||||||
|
import { logger } from '../../utils/logger'
|
||||||
|
|
||||||
|
export class CapacitorQRScanner implements QRScannerService {
|
||||||
|
private scanListener: ScanListener | null = null
|
||||||
|
private isScanning = false
|
||||||
|
private listenerHandles: Array<() => Promise<void>> = []
|
||||||
|
|
||||||
|
async checkPermissions() {
|
||||||
|
try {
|
||||||
|
const { camera } = await BarcodeScanner.checkPermissions()
|
||||||
|
return camera === 'granted'
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error checking camera permissions:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestPermissions() {
|
||||||
|
try {
|
||||||
|
const { camera } = await BarcodeScanner.requestPermissions()
|
||||||
|
return camera === 'granted'
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error requesting camera permissions:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isSupported() {
|
||||||
|
try {
|
||||||
|
const { supported } = await BarcodeScanner.isSupported()
|
||||||
|
return supported
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error checking barcode scanner support:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startScan() {
|
||||||
|
if (this.isScanning) {
|
||||||
|
logger.warn('Scanner is already active')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First register listeners before starting scan
|
||||||
|
await this.registerListeners()
|
||||||
|
|
||||||
|
this.isScanning = true
|
||||||
|
await BarcodeScanner.startScan({
|
||||||
|
formats: [BarcodeFormat.QrCode],
|
||||||
|
lensFacing: LensFacing.Back
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// Ensure cleanup on error
|
||||||
|
this.isScanning = false
|
||||||
|
await this.removeListeners()
|
||||||
|
logger.error('Error starting barcode scan:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async registerListeners() {
|
||||||
|
try {
|
||||||
|
const handle = await BarcodeScanner.addListener(
|
||||||
|
'barcodesScanned',
|
||||||
|
async (result: ScanResult) => {
|
||||||
|
if (result.barcodes.length > 0 && this.scanListener) {
|
||||||
|
const barcode = result.barcodes[0]
|
||||||
|
this.scanListener.onScan(barcode.rawValue)
|
||||||
|
await this.stopScan()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
this.listenerHandles.push(() => handle.remove())
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error registering barcode listener:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeListeners() {
|
||||||
|
for (const remove of this.listenerHandles) {
|
||||||
|
try {
|
||||||
|
await remove()
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error removing listener:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.listenerHandles = []
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopScan() {
|
||||||
|
if (!this.isScanning) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First stop the scan
|
||||||
|
await BarcodeScanner.stopScan()
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error stopping barcode scan:', error)
|
||||||
|
} finally {
|
||||||
|
// Always cleanup state even if stop fails
|
||||||
|
this.isScanning = false
|
||||||
|
await this.removeListeners()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener: ScanListener): void {
|
||||||
|
this.scanListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup(): Promise<void> {
|
||||||
|
await this.stopScan()
|
||||||
|
this.scanListener = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,9 @@
|
|||||||
</button>
|
</button>
|
||||||
<div v-else class="text-center">
|
<div v-else class="text-center">
|
||||||
<font-awesome icon="spinner" class="fa-spin fa-3x" />
|
<font-awesome icon="spinner" class="fa-spin fa-3x" />
|
||||||
<p class="mt-2">{{ state.processingDetails }}</p>
|
<p class="mt-2">
|
||||||
|
{{ state.processingDetails }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -55,216 +57,216 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from 'vue-facing-decorator'
|
||||||
import { QrcodeStream } from "vue-qrcode-reader";
|
import { QrcodeStream } from 'vue-qrcode-reader'
|
||||||
import { reactive } from "vue";
|
import { reactive } from 'vue'
|
||||||
import {
|
import {
|
||||||
BarcodeScanner,
|
BarcodeScanner,
|
||||||
type ScanResult,
|
type ScanResult
|
||||||
} from "@capacitor-mlkit/barcode-scanning";
|
} from '@capacitor-mlkit/barcode-scanning'
|
||||||
import type { PluginListenerHandle } from "@capacitor/core";
|
import type { PluginListenerHandle } from '@capacitor/core'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../../utils/logger'
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../../constants/app'
|
||||||
|
|
||||||
// Declare global constants
|
// Declare global constants
|
||||||
declare const __USE_QR_READER__: boolean;
|
declare const __USE_QR_READER__: boolean
|
||||||
declare const __IS_MOBILE__: boolean;
|
declare const __IS_MOBILE__: boolean
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
isProcessing: boolean;
|
isProcessing: boolean
|
||||||
processingStatus: string;
|
processingStatus: string
|
||||||
processingDetails: string;
|
processingDetails: string
|
||||||
error: string;
|
error: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
QrcodeStream,
|
QrcodeStream
|
||||||
},
|
}
|
||||||
})
|
})
|
||||||
export default class QRScannerDialog extends Vue {
|
export default class QRScannerDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
visible = false;
|
visible = false
|
||||||
private scanListener: PluginListenerHandle | null = null;
|
private scanListener: PluginListenerHandle | null = null
|
||||||
private onScanCallback: ((result: string) => void) | null = null;
|
private onScanCallback: ((result: string) => void) | null = null
|
||||||
|
|
||||||
state = reactive<AppState>({
|
state = reactive<AppState>({
|
||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
processingStatus: "",
|
processingStatus: '',
|
||||||
processingDetails: "",
|
processingDetails: '',
|
||||||
error: "",
|
error: ''
|
||||||
});
|
})
|
||||||
|
|
||||||
async open(onScan: (result: string) => void) {
|
async open(onScan: (result: string) => void) {
|
||||||
this.onScanCallback = onScan;
|
this.onScanCallback = onScan
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
this.state.error = "";
|
this.state.error = ''
|
||||||
|
|
||||||
if (!this.useQRReader) {
|
if (!this.useQRReader) {
|
||||||
// Check if barcode scanning is supported on mobile
|
// Check if barcode scanning is supported on mobile
|
||||||
try {
|
try {
|
||||||
const { supported } = await BarcodeScanner.isSupported();
|
const { supported } = await BarcodeScanner.isSupported()
|
||||||
if (!supported) {
|
if (!supported) {
|
||||||
this.showError("Barcode scanning is not supported on this device");
|
this.showError('Barcode scanning is not supported on this device')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showError("Failed to check barcode scanner support");
|
this.showError('Failed to check barcode scanner support')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
this.stopScanning().catch((error) => {
|
this.stopScanning().catch((error) => {
|
||||||
logger.error("Error stopping scanner during close:", error);
|
logger.error('Error stopping scanner during close:', error)
|
||||||
});
|
})
|
||||||
this.onScanCallback = null;
|
this.onScanCallback = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async openMobileCamera() {
|
async openMobileCamera() {
|
||||||
try {
|
try {
|
||||||
this.state.isProcessing = true;
|
this.state.isProcessing = true
|
||||||
this.state.processingStatus = "Starting camera...";
|
this.state.processingStatus = 'Starting camera...'
|
||||||
logger.log("Opening mobile camera - starting initialization");
|
logger.log('Opening mobile camera - starting initialization')
|
||||||
|
|
||||||
// Check current permission status
|
// Check current permission status
|
||||||
const status = await BarcodeScanner.checkPermissions();
|
const status = await BarcodeScanner.checkPermissions()
|
||||||
logger.log("Camera permission status:", JSON.stringify(status, null, 2));
|
logger.log('Camera permission status:', JSON.stringify(status, null, 2))
|
||||||
|
|
||||||
if (status.camera !== "granted") {
|
if (status.camera !== 'granted') {
|
||||||
// Request permission if not granted
|
// Request permission if not granted
|
||||||
logger.log("Requesting camera permissions...");
|
logger.log('Requesting camera permissions...')
|
||||||
const permissionStatus = await BarcodeScanner.requestPermissions();
|
const permissionStatus = await BarcodeScanner.requestPermissions()
|
||||||
if (permissionStatus.camera !== "granted") {
|
if (permissionStatus.camera !== 'granted') {
|
||||||
throw new Error("Camera permission not granted");
|
throw new Error('Camera permission not granted')
|
||||||
}
|
}
|
||||||
logger.log(
|
logger.log(
|
||||||
"Camera permission granted:",
|
'Camera permission granted:',
|
||||||
JSON.stringify(permissionStatus, null, 2),
|
JSON.stringify(permissionStatus, null, 2)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any existing listener first
|
// Remove any existing listener first
|
||||||
await this.cleanupScanListener();
|
await this.cleanupScanListener()
|
||||||
|
|
||||||
// Set up the listener before starting the scan
|
// Set up the listener before starting the scan
|
||||||
logger.log("Setting up new barcode listener");
|
logger.log('Setting up new barcode listener')
|
||||||
this.scanListener = await BarcodeScanner.addListener(
|
this.scanListener = await BarcodeScanner.addListener(
|
||||||
"barcodesScanned",
|
'barcodesScanned',
|
||||||
async (result: ScanResult) => {
|
async (result: ScanResult) => {
|
||||||
logger.log(
|
logger.log(
|
||||||
"Barcode scan result received:",
|
'Barcode scan result received:',
|
||||||
JSON.stringify(result, null, 2),
|
JSON.stringify(result, null, 2)
|
||||||
);
|
)
|
||||||
if (result.barcodes && result.barcodes.length > 0) {
|
if (result.barcodes && result.barcodes.length > 0) {
|
||||||
this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}`;
|
this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}`
|
||||||
await this.handleScanResult(result.barcodes[0].rawValue);
|
await this.handleScanResult(result.barcodes[0].rawValue)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
logger.log("Barcode listener setup complete");
|
logger.log('Barcode listener setup complete')
|
||||||
|
|
||||||
// Start the scanner
|
// Start the scanner
|
||||||
logger.log("Starting barcode scanner");
|
logger.log('Starting barcode scanner')
|
||||||
await BarcodeScanner.startScan();
|
await BarcodeScanner.startScan()
|
||||||
logger.log("Barcode scanner started successfully");
|
logger.log('Barcode scanner started successfully')
|
||||||
|
|
||||||
this.state.isProcessing = false;
|
this.state.isProcessing = false
|
||||||
this.state.processingStatus = "";
|
this.state.processingStatus = ''
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to open camera:", error);
|
logger.error('Failed to open camera:', error)
|
||||||
this.state.isProcessing = false;
|
this.state.isProcessing = false
|
||||||
this.state.processingStatus = "";
|
this.state.processingStatus = ''
|
||||||
this.showError(
|
this.showError(
|
||||||
error instanceof Error ? error.message : "Failed to open camera",
|
error instanceof Error ? error.message : 'Failed to open camera'
|
||||||
);
|
)
|
||||||
|
|
||||||
// Cleanup on error
|
// Cleanup on error
|
||||||
await this.cleanupScanListener();
|
await this.cleanupScanListener()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleScanResult(rawValue: string) {
|
private async handleScanResult(rawValue: string) {
|
||||||
try {
|
try {
|
||||||
this.state.isProcessing = true;
|
this.state.isProcessing = true
|
||||||
this.state.processingStatus = "Processing QR code...";
|
this.state.processingStatus = 'Processing QR code...'
|
||||||
this.state.processingDetails = `Scanned value: ${rawValue}`;
|
this.state.processingDetails = `Scanned value: ${rawValue}`
|
||||||
|
|
||||||
// Stop scanning before processing
|
// Stop scanning before processing
|
||||||
await this.stopScanning();
|
await this.stopScanning()
|
||||||
|
|
||||||
if (this.onScanCallback) {
|
if (this.onScanCallback) {
|
||||||
await this.onScanCallback(rawValue);
|
await this.onScanCallback(rawValue)
|
||||||
// Only close after the callback is complete
|
// Only close after the callback is complete
|
||||||
this.close();
|
this.close()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error handling scan result:", error);
|
logger.error('Error handling scan result:', error)
|
||||||
this.showError("Failed to process scan result");
|
this.showError('Failed to process scan result')
|
||||||
} finally {
|
} finally {
|
||||||
this.state.isProcessing = false;
|
this.state.isProcessing = false
|
||||||
this.state.processingStatus = "";
|
this.state.processingStatus = ''
|
||||||
this.state.processingDetails = "";
|
this.state.processingDetails = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async cleanupScanListener() {
|
private async cleanupScanListener() {
|
||||||
try {
|
try {
|
||||||
if (this.scanListener) {
|
if (this.scanListener) {
|
||||||
await this.scanListener.remove();
|
await this.scanListener.remove()
|
||||||
this.scanListener = null;
|
this.scanListener = null
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error removing scan listener:", error);
|
logger.error('Error removing scan listener:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopScanning() {
|
async stopScanning() {
|
||||||
try {
|
try {
|
||||||
await this.cleanupScanListener();
|
await this.cleanupScanListener()
|
||||||
|
|
||||||
if (!this.useQRReader) {
|
if (!this.useQRReader) {
|
||||||
// Stop the native scanner
|
// Stop the native scanner
|
||||||
await BarcodeScanner.stopScan();
|
await BarcodeScanner.stopScan()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error stopping scanner:", error);
|
logger.error('Error stopping scanner:', error)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web QR reader handlers
|
// Web QR reader handlers
|
||||||
async onScanDetect(result: { rawValue: string }) {
|
async onScanDetect(result: { rawValue: string }) {
|
||||||
await this.handleScanResult(result.rawValue);
|
await this.handleScanResult(result.rawValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
onScanError(error: Error) {
|
onScanError(error: Error) {
|
||||||
logger.error("Scan error:", error);
|
logger.error('Scan error:', error)
|
||||||
this.showError("Failed to scan QR code");
|
this.showError('Failed to scan QR code')
|
||||||
}
|
}
|
||||||
|
|
||||||
private showError(message: string) {
|
private showError(message: string) {
|
||||||
this.state.error = message;
|
this.state.error = message
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error",
|
title: 'Error',
|
||||||
text: message,
|
text: message
|
||||||
},
|
},
|
||||||
5000,
|
5000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get useQRReader(): boolean {
|
get useQRReader(): boolean {
|
||||||
return __USE_QR_READER__;
|
return __USE_QR_READER__
|
||||||
}
|
}
|
||||||
|
|
||||||
get isMobile(): boolean {
|
get isMobile(): boolean {
|
||||||
return __IS_MOBILE__;
|
return __IS_MOBILE__
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
74
src/components/QRScanner/WebDialogQRScanner.ts
Normal file
74
src/components/QRScanner/WebDialogQRScanner.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import type { QRScannerService, ScanListener } from './types'
|
||||||
|
import QRScannerDialog from './QRScannerDialog.vue'
|
||||||
|
import { createApp, type App } from 'vue'
|
||||||
|
import { logger } from '../../utils/logger'
|
||||||
|
|
||||||
|
// Import platform-specific flags from Vite config
|
||||||
|
declare const __USE_QR_READER__: boolean
|
||||||
|
|
||||||
|
export class WebDialogQRScanner implements QRScannerService {
|
||||||
|
private dialogApp: App | null = null
|
||||||
|
private dialogElement: HTMLDivElement | null = null
|
||||||
|
private scanListener: ScanListener | null = null
|
||||||
|
|
||||||
|
async checkPermissions(): Promise<boolean> {
|
||||||
|
return navigator?.mediaDevices !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestPermissions(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({ video: true })
|
||||||
|
stream.getTracks().forEach((track) => track.stop())
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get camera permissions:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isSupported(): Promise<boolean> {
|
||||||
|
return Promise.resolve(
|
||||||
|
__USE_QR_READER__ && navigator?.mediaDevices !== undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async startScan(): Promise<void> {
|
||||||
|
if (!(await this.isSupported())) {
|
||||||
|
throw new Error('QR scanning is not supported in this environment')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dialogElement = document.createElement('div')
|
||||||
|
document.body.appendChild(this.dialogElement)
|
||||||
|
|
||||||
|
this.dialogApp = createApp(QRScannerDialog, {
|
||||||
|
onScan: (result: string) => {
|
||||||
|
if (this.scanListener) {
|
||||||
|
this.scanListener.onScan(result)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClose: () => {
|
||||||
|
this.stopScan()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.dialogApp.mount(this.dialogElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopScan(): Promise<void> {
|
||||||
|
if (this.dialogApp && this.dialogElement) {
|
||||||
|
this.dialogApp.unmount()
|
||||||
|
this.dialogElement.remove()
|
||||||
|
this.dialogApp = null
|
||||||
|
this.dialogElement = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener: ScanListener): void {
|
||||||
|
this.scanListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup(): Promise<void> {
|
||||||
|
await this.stopScan()
|
||||||
|
this.scanListener = null
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/components/QRScanner/factory.ts
Normal file
38
src/components/QRScanner/factory.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Capacitor } from '@capacitor/core'
|
||||||
|
import type { QRScannerService } from './types'
|
||||||
|
import { logger } from '../../utils/logger'
|
||||||
|
import { WebDialogQRScanner } from './WebDialogScanner'
|
||||||
|
import { CapacitorQRScanner } from './CapacitorScanner'
|
||||||
|
|
||||||
|
// Import platform-specific flags from Vite config
|
||||||
|
declare const __USE_QR_READER__: boolean
|
||||||
|
declare const __IS_MOBILE__: boolean
|
||||||
|
|
||||||
|
export class QRScannerFactory {
|
||||||
|
private static instance: QRScannerService | null = null
|
||||||
|
|
||||||
|
static getInstance(): QRScannerService {
|
||||||
|
if (!this.instance) {
|
||||||
|
// Use platform-specific flags for more accurate detection
|
||||||
|
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) {
|
||||||
|
logger.log('Creating native QR scanner instance')
|
||||||
|
this.instance = new CapacitorQRScanner()
|
||||||
|
} else if (__USE_QR_READER__) {
|
||||||
|
logger.log('Creating web QR scanner instance')
|
||||||
|
this.instance = new WebDialogQRScanner()
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
'No QR scanner implementation available for this platform'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
static async cleanup() {
|
||||||
|
if (this.instance) {
|
||||||
|
await this.instance.cleanup()
|
||||||
|
this.instance = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/components/QRScanner/types.ts
Normal file
14
src/components/QRScanner/types.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export interface ScanListener {
|
||||||
|
onScan: (result: string) => void
|
||||||
|
onError?: (error: Error) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QRScannerService {
|
||||||
|
checkPermissions(): Promise<boolean>
|
||||||
|
requestPermissions(): Promise<boolean>
|
||||||
|
isSupported(): Promise<boolean>
|
||||||
|
startScan(): Promise<void>
|
||||||
|
stopScan(): Promise<void>
|
||||||
|
addListener(listener: ScanListener): void
|
||||||
|
cleanup(): Promise<void>
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
'basis-1/5': true,
|
'basis-1/5': true,
|
||||||
'rounded-md': true,
|
'rounded-md': true,
|
||||||
'bg-slate-400 text-white': selected === 'Home',
|
'bg-slate-400 text-white': selected === 'Home',
|
||||||
'text-slate-500': selected !== 'Home',
|
'text-slate-500': selected !== 'Home'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<router-link :to="{ name: 'home' }" class="block text-center py-2 px-1">
|
<router-link :to="{ name: 'home' }" class="block text-center py-2 px-1">
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
'basis-1/5': true,
|
'basis-1/5': true,
|
||||||
'rounded-md': true,
|
'rounded-md': true,
|
||||||
'bg-slate-400 text-white': selected === 'Discover',
|
'bg-slate-400 text-white': selected === 'Discover',
|
||||||
'text-slate-500': selected !== 'Discover',
|
'text-slate-500': selected !== 'Discover'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
'basis-1/5': true,
|
'basis-1/5': true,
|
||||||
'rounded-md': true,
|
'rounded-md': true,
|
||||||
'bg-slate-400 text-white': selected === 'Projects',
|
'bg-slate-400 text-white': selected === 'Projects',
|
||||||
'text-slate-500': selected !== 'Projects',
|
'text-slate-500': selected !== 'Projects'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
'basis-1/5': true,
|
'basis-1/5': true,
|
||||||
'rounded-md': true,
|
'rounded-md': true,
|
||||||
'bg-slate-400 text-white': selected === 'Contacts',
|
'bg-slate-400 text-white': selected === 'Contacts',
|
||||||
'text-slate-500': selected !== 'Contacts',
|
'text-slate-500': selected !== 'Contacts'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
'basis-1/5': true,
|
'basis-1/5': true,
|
||||||
'rounded-md': true,
|
'rounded-md': true,
|
||||||
'bg-slate-400 text-white': selected === 'Profile',
|
'bg-slate-400 text-white': selected === 'Profile',
|
||||||
'text-slate-500': selected !== 'Profile',
|
'text-slate-500': selected !== 'Profile'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
@@ -106,10 +106,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from 'vue-facing-decorator'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class QuickNav extends Vue {
|
export default class QuickNav extends Vue {
|
||||||
@Prop selected = "";
|
@Prop selected = ''
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -13,46 +13,46 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from 'vue-facing-decorator'
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface } from '../constants/app'
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from '../db/index'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class TopMessage extends Vue {
|
export default class TopMessage extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
@Prop selected = "";
|
@Prop selected = ''
|
||||||
|
|
||||||
message = "";
|
message = ''
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
if (
|
if (
|
||||||
settings.warnIfTestServer &&
|
settings.warnIfTestServer &&
|
||||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||||
) {
|
) {
|
||||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
const didPrefix = settings.activeDid?.slice(11, 15)
|
||||||
this.message = "You're linked to a non-prod server, user " + didPrefix;
|
this.message = "You're linked to a non-prod server, user " + didPrefix
|
||||||
} else if (
|
} else if (
|
||||||
settings.warnIfProdServer &&
|
settings.warnIfProdServer &&
|
||||||
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
||||||
) {
|
) {
|
||||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
const didPrefix = settings.activeDid?.slice(11, 15)
|
||||||
this.message =
|
this.message =
|
||||||
"You're linked to the production server, user " + didPrefix;
|
"You're linked to the production server, user " + didPrefix
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "danger",
|
type: 'danger',
|
||||||
title: "Error Detecting Server",
|
title: 'Error Detecting Server',
|
||||||
text: JSON.stringify(err),
|
text: JSON.stringify(err)
|
||||||
},
|
},
|
||||||
-1,
|
-1
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,49 +35,49 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from 'vue-facing-decorator'
|
||||||
|
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from '../constants/app'
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from '../db/index'
|
||||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from '../db/tables/settings'
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class UserNameDialog extends Vue {
|
export default class UserNameDialog extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||||
|
|
||||||
@Prop({
|
@Prop({
|
||||||
default:
|
default:
|
||||||
"This is not sent to servers. It is only shared with people when you send it to them.",
|
'This is not sent to servers. It is only shared with people when you send it to them.'
|
||||||
})
|
})
|
||||||
sharingExplanation!: string;
|
sharingExplanation!: string
|
||||||
@Prop({ default: false }) callbackOnCancel!: boolean;
|
@Prop({ default: false }) callbackOnCancel!: boolean
|
||||||
|
|
||||||
callback: (name?: string) => void = () => {};
|
callback: (name?: string) => void = () => {}
|
||||||
givenName = "";
|
givenName = ''
|
||||||
visible = false;
|
visible = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param aCallback - callback function for name, which may be ""
|
* @param aCallback - callback function for name, which may be ""
|
||||||
*/
|
*/
|
||||||
async open(aCallback?: (name?: string) => void) {
|
async open(aCallback?: (name?: string) => void) {
|
||||||
this.callback = aCallback || this.callback;
|
this.callback = aCallback || this.callback
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
this.givenName = settings.firstName || "";
|
this.givenName = settings.firstName || ''
|
||||||
this.visible = true;
|
this.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickSaveChanges() {
|
async onClickSaveChanges() {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
firstName: this.givenName,
|
firstName: this.givenName
|
||||||
});
|
})
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
this.callback(this.givenName);
|
this.callback(this.givenName)
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickCancel() {
|
onClickCancel() {
|
||||||
this.visible = false;
|
this.visible = false
|
||||||
if (this.callbackOnCancel) {
|
if (this.callbackOnCancel) {
|
||||||
this.callback();
|
this.callback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +1,53 @@
|
|||||||
// from https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80
|
// from https://medium.com/nicasource/building-an-interactive-web-portfolio-with-vue-three-js-part-three-implementing-three-js-452cb375ef80
|
||||||
|
|
||||||
import * as TWEEN from "@tweenjs/tween.js";
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
import * as THREE from "three";
|
import * as THREE from 'three'
|
||||||
|
|
||||||
import { createCamera } from "./components/camera.js";
|
import { createCamera } from './components/camera.js'
|
||||||
import { createLights } from "./components/lights.js";
|
import { createLights } from './components/lights.js'
|
||||||
import { createScene } from "./components/scene.js";
|
import { createScene } from './components/scene.js'
|
||||||
import { loadLandmarks } from "./components/objects/landmarks.js";
|
import { loadLandmarks } from './components/objects/landmarks.js'
|
||||||
import { createTerrain } from "./components/objects/terrain.js";
|
import { createTerrain } from './components/objects/terrain.js'
|
||||||
import { Loop } from "./systems/Loop.js";
|
import { Loop } from './systems/Loop.js'
|
||||||
import { Resizer } from "./systems/Resizer.js";
|
import { Resizer } from './systems/Resizer.js'
|
||||||
import { createControls } from "./systems/controls.js";
|
import { createControls } from './systems/controls.js'
|
||||||
import { createRenderer } from "./systems/renderer.js";
|
import { createRenderer } from './systems/renderer.js'
|
||||||
|
|
||||||
const COLOR1 = "#dddddd";
|
const COLOR1 = '#dddddd'
|
||||||
const COLOR2 = "#0055aa";
|
const COLOR2 = '#0055aa'
|
||||||
|
|
||||||
class World {
|
class World {
|
||||||
constructor(container, vue) {
|
constructor(container, vue) {
|
||||||
this.PLATFORM_BORDER = 5;
|
this.PLATFORM_BORDER = 5
|
||||||
this.PLATFORM_EDGE_FOR_UNKNOWNS = 10;
|
this.PLATFORM_EDGE_FOR_UNKNOWNS = 10
|
||||||
this.PLATFORM_SIZE = 100; // note that the loadLandmarks calculations may still assume 100
|
this.PLATFORM_SIZE = 100 // note that the loadLandmarks calculations may still assume 100
|
||||||
|
|
||||||
this.update = this.update.bind(this);
|
this.update = this.update.bind(this)
|
||||||
|
|
||||||
this.vue = vue;
|
this.vue = vue
|
||||||
|
|
||||||
// Instances of camera, scene, and renderer
|
// Instances of camera, scene, and renderer
|
||||||
this.camera = createCamera();
|
this.camera = createCamera()
|
||||||
this.scene = createScene(COLOR2);
|
this.scene = createScene(COLOR2)
|
||||||
this.renderer = createRenderer();
|
this.renderer = createRenderer()
|
||||||
|
|
||||||
// necessary for models, says https://threejs.org/docs/index.html#examples/en/loaders/GLTFLoader
|
// necessary for models, says https://threejs.org/docs/index.html#examples/en/loaders/GLTFLoader
|
||||||
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
|
this.renderer.outputColorSpace = THREE.SRGBColorSpace
|
||||||
|
|
||||||
this.light = null;
|
this.light = null
|
||||||
this.lights = [];
|
this.lights = []
|
||||||
this.bushes = [];
|
this.bushes = []
|
||||||
|
|
||||||
// Initialize Loop
|
// Initialize Loop
|
||||||
this.loop = new Loop(this.camera, this.scene, this.renderer);
|
this.loop = new Loop(this.camera, this.scene, this.renderer)
|
||||||
|
|
||||||
container.append(this.renderer.domElement);
|
container.append(this.renderer.domElement)
|
||||||
|
|
||||||
// Orbit Controls
|
// Orbit Controls
|
||||||
const controls = createControls(this.camera, this.renderer.domElement);
|
const controls = createControls(this.camera, this.renderer.domElement)
|
||||||
|
|
||||||
// Light Instance, with optional light helper
|
// Light Instance, with optional light helper
|
||||||
const { light } = createLights(COLOR1);
|
const { light } = createLights(COLOR1)
|
||||||
|
|
||||||
// Terrain Instance
|
// Terrain Instance
|
||||||
const terrain = createTerrain({
|
const terrain = createTerrain({
|
||||||
@@ -56,55 +56,55 @@ class World {
|
|||||||
width:
|
width:
|
||||||
this.PLATFORM_SIZE +
|
this.PLATFORM_SIZE +
|
||||||
this.PLATFORM_BORDER * 2 +
|
this.PLATFORM_BORDER * 2 +
|
||||||
this.PLATFORM_EDGE_FOR_UNKNOWNS * 2,
|
this.PLATFORM_EDGE_FOR_UNKNOWNS * 2
|
||||||
});
|
})
|
||||||
|
|
||||||
this.loop.updatables.push(controls);
|
this.loop.updatables.push(controls)
|
||||||
this.loop.updatables.push(light);
|
this.loop.updatables.push(light)
|
||||||
this.loop.updatables.push(terrain);
|
this.loop.updatables.push(terrain)
|
||||||
|
|
||||||
this.scene.add(light, terrain);
|
this.scene.add(light, terrain)
|
||||||
|
|
||||||
loadLandmarks(vue, this, this.scene, this.loop);
|
loadLandmarks(vue, this, this.scene, this.loop)
|
||||||
|
|
||||||
requestAnimationFrame(this.update);
|
requestAnimationFrame(this.update)
|
||||||
|
|
||||||
// Responsive handler
|
// Responsive handler
|
||||||
const resizer = new Resizer(container, this.camera, this.renderer);
|
const resizer = new Resizer(container, this.camera, this.renderer)
|
||||||
resizer.onResize = () => {
|
resizer.onResize = () => {
|
||||||
this.render();
|
this.render()
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update(time) {
|
update(time) {
|
||||||
TWEEN.update(time);
|
TWEEN.update(time)
|
||||||
this.lights.forEach((light) => {
|
this.lights.forEach((light) => {
|
||||||
light.updateMatrixWorld();
|
light.updateMatrixWorld()
|
||||||
light.target.updateMatrixWorld();
|
light.target.updateMatrixWorld()
|
||||||
});
|
})
|
||||||
this.lights.forEach((bush) => {
|
this.lights.forEach((bush) => {
|
||||||
bush.updateMatrixWorld();
|
bush.updateMatrixWorld()
|
||||||
});
|
})
|
||||||
requestAnimationFrame(this.update);
|
requestAnimationFrame(this.update)
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// draw a single frame
|
// draw a single frame
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animation handlers
|
// Animation handlers
|
||||||
start() {
|
start() {
|
||||||
this.loop.start();
|
this.loop.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this.loop.stop();
|
this.loop.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
setExposedWorldProperties(key, value) {
|
setExposedWorldProperties(key, value) {
|
||||||
this.vue.setWorldProperty(key, value);
|
this.vue.setWorldProperty(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { World };
|
export { World }
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { PerspectiveCamera } from "three";
|
import { PerspectiveCamera } from 'three'
|
||||||
|
|
||||||
function createCamera() {
|
function createCamera() {
|
||||||
const camera = new PerspectiveCamera(
|
const camera = new PerspectiveCamera(
|
||||||
35, // fov = Field Of View
|
35, // fov = Field Of View
|
||||||
1, // aspect ratio (dummy value)
|
1, // aspect ratio (dummy value)
|
||||||
0.1, // near clipping plane
|
0.1, // near clipping plane
|
||||||
350, // far clipping plane
|
350 // far clipping plane
|
||||||
);
|
)
|
||||||
|
|
||||||
// move the camera back so we can view the scene
|
// move the camera back so we can view the scene
|
||||||
camera.position.set(0, 100, 200);
|
camera.position.set(0, 100, 200)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
camera.tick = () => {};
|
camera.tick = () => {}
|
||||||
|
|
||||||
return camera;
|
return camera
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createCamera };
|
export { createCamera }
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { DirectionalLight, DirectionalLightHelper } from "three";
|
import { DirectionalLight, DirectionalLightHelper } from 'three'
|
||||||
|
|
||||||
function createLights(color) {
|
function createLights(color) {
|
||||||
const light = new DirectionalLight(color, 4);
|
const light = new DirectionalLight(color, 4)
|
||||||
const lightHelper = new DirectionalLightHelper(light, 0);
|
const lightHelper = new DirectionalLightHelper(light, 0)
|
||||||
light.position.set(60, 100, 30);
|
light.position.set(60, 100, 30)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
light.tick = () => {};
|
light.tick = () => {}
|
||||||
|
|
||||||
return { light, lightHelper };
|
return { light, lightHelper }
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createLights };
|
export { createLights }
|
||||||
|
|||||||
@@ -1,46 +1,46 @@
|
|||||||
import axios from "axios";
|
import axios from 'axios'
|
||||||
import * as THREE from "three";
|
import * as THREE from 'three'
|
||||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader'
|
||||||
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
|
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils'
|
||||||
import * as TWEEN from "@tweenjs/tween.js";
|
import * as TWEEN from '@tweenjs/tween.js'
|
||||||
import { retrieveSettingsForActiveAccount } from "../../../../db";
|
import { retrieveSettingsForActiveAccount } from '../../../../db'
|
||||||
import { getHeaders } from "../../../../libs/endorserServer";
|
import { getHeaders } from '../../../../libs/endorserServer'
|
||||||
import { logger } from "../../../../utils/logger";
|
import { logger } from '../../../../utils/logger'
|
||||||
|
|
||||||
const ANIMATION_DURATION_SECS = 10;
|
const ANIMATION_DURATION_SECS = 10
|
||||||
const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/";
|
const ENDORSER_ENTITY_PREFIX = 'https://endorser.ch/entity/'
|
||||||
|
|
||||||
export async function loadLandmarks(vue, world, scene, loop) {
|
export async function loadLandmarks(vue, world, scene, loop) {
|
||||||
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
|
vue.setWorldProperty('animationDurationSeconds', ANIMATION_DURATION_SECS)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
const activeDid = settings.activeDid || "";
|
const activeDid = settings.activeDid || ''
|
||||||
const apiServer = settings.apiServer;
|
const apiServer = settings.apiServer
|
||||||
const headers = await getHeaders(activeDid);
|
const headers = await getHeaders(activeDid)
|
||||||
|
|
||||||
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
|
const url = apiServer + '/api/v2/report/claims?claimType=GiveAction'
|
||||||
const resp = await axios.get(url, { headers: headers });
|
const resp = await axios.get(url, { headers: headers })
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const landmarks = resp.data.data;
|
const landmarks = resp.data.data
|
||||||
|
|
||||||
const minDate = landmarks[landmarks.length - 1].issuedAt;
|
const minDate = landmarks[landmarks.length - 1].issuedAt
|
||||||
const maxDate = landmarks[0].issuedAt;
|
const maxDate = landmarks[0].issuedAt
|
||||||
|
|
||||||
world.setExposedWorldProperties("startTime", minDate.replace("T", " "));
|
world.setExposedWorldProperties('startTime', minDate.replace('T', ' '))
|
||||||
world.setExposedWorldProperties("endTime", maxDate.replace("T", " "));
|
world.setExposedWorldProperties('endTime', maxDate.replace('T', ' '))
|
||||||
|
|
||||||
const minTimeMillis = new Date(minDate).getTime();
|
const minTimeMillis = new Date(minDate).getTime()
|
||||||
const fullTimeMillis =
|
const fullTimeMillis =
|
||||||
maxDate > minDate ? new Date(maxDate).getTime() - minTimeMillis : 1; // avoid divide by zero
|
maxDate > minDate ? new Date(maxDate).getTime() - minTimeMillis : 1 // avoid divide by zero
|
||||||
// ratio of animation time to real time
|
// ratio of animation time to real time
|
||||||
const fakeRealRatio = (ANIMATION_DURATION_SECS * 1000) / fullTimeMillis;
|
const fakeRealRatio = (ANIMATION_DURATION_SECS * 1000) / fullTimeMillis
|
||||||
|
|
||||||
// load plant model first because it takes a second
|
// load plant model first because it takes a second
|
||||||
const loader = new GLTFLoader();
|
const loader = new GLTFLoader()
|
||||||
// choose the right plant
|
// choose the right plant
|
||||||
const modelLoc = "/models/lupine_plant/scene.gltf", // push with pokies
|
const modelLoc = '/models/lupine_plant/scene.gltf', // push with pokies
|
||||||
modScale = 0.1;
|
modScale = 0.1
|
||||||
//const modelLoc = "/models/round_bush/scene.gltf", // green & pink
|
//const modelLoc = "/models/round_bush/scene.gltf", // green & pink
|
||||||
// modScale = 1;
|
// modScale = 1;
|
||||||
//const modelLoc = "/models/coreopsis-flower.glb", // 3 flowers
|
//const modelLoc = "/models/coreopsis-flower.glb", // 3 flowers
|
||||||
@@ -50,59 +50,55 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
|
|
||||||
// calculate positions for each claim, especially because some are random
|
// calculate positions for each claim, especially because some are random
|
||||||
const locations = landmarks.map((claim) =>
|
const locations = landmarks.map((claim) =>
|
||||||
locForGive(
|
locForGive(claim, world.PLATFORM_SIZE, world.PLATFORM_EDGE_FOR_UNKNOWNS)
|
||||||
claim,
|
)
|
||||||
world.PLATFORM_SIZE,
|
|
||||||
world.PLATFORM_EDGE_FOR_UNKNOWNS,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||||
loader.load(
|
loader.load(
|
||||||
modelLoc,
|
modelLoc,
|
||||||
function (gltf) {
|
function (gltf) {
|
||||||
gltf.scene.scale.set(0, 0, 0);
|
gltf.scene.scale.set(0, 0, 0)
|
||||||
for (let i = 0; i < landmarks.length; i++) {
|
for (let i = 0; i < landmarks.length; i++) {
|
||||||
// claim is a GiveServerRecord (see endorserServer.ts)
|
// claim is a GiveServerRecord (see endorserServer.ts)
|
||||||
const claim = landmarks[i];
|
const claim = landmarks[i]
|
||||||
const newPlant = SkeletonUtils.clone(gltf.scene);
|
const newPlant = SkeletonUtils.clone(gltf.scene)
|
||||||
|
|
||||||
const loc = locations[i];
|
const loc = locations[i]
|
||||||
newPlant.position.set(loc.x, 0, loc.z);
|
newPlant.position.set(loc.x, 0, loc.z)
|
||||||
|
|
||||||
world.scene.add(newPlant);
|
world.scene.add(newPlant)
|
||||||
const timeDelayMillis =
|
const timeDelayMillis =
|
||||||
fakeRealRatio *
|
fakeRealRatio *
|
||||||
(new Date(claim.issuedAt).getTime() - minTimeMillis);
|
(new Date(claim.issuedAt).getTime() - minTimeMillis)
|
||||||
new TWEEN.Tween(newPlant.scale)
|
new TWEEN.Tween(newPlant.scale)
|
||||||
.delay(timeDelayMillis)
|
.delay(timeDelayMillis)
|
||||||
.to({ x: modScale, y: modScale, z: modScale }, 5000)
|
.to({ x: modScale, y: modScale, z: modScale }, 5000)
|
||||||
.start();
|
.start()
|
||||||
world.bushes = [...world.bushes, newPlant];
|
world.bushes = [...world.bushes, newPlant]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
function (error) {
|
function (error) {
|
||||||
logger.error(error);
|
logger.error(error)
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
// calculate when lights shine on appearing claim area
|
// calculate when lights shine on appearing claim area
|
||||||
for (let i = 0; i < landmarks.length; i++) {
|
for (let i = 0; i < landmarks.length; i++) {
|
||||||
// claim is a GiveServerRecord (see endorserServer.ts)
|
// claim is a GiveServerRecord (see endorserServer.ts)
|
||||||
const claim = landmarks[i];
|
const claim = landmarks[i]
|
||||||
|
|
||||||
const loc = locations[i];
|
const loc = locations[i]
|
||||||
const light = createLight();
|
const light = createLight()
|
||||||
light.position.set(loc.x, 20, loc.z);
|
light.position.set(loc.x, 20, loc.z)
|
||||||
light.target.position.set(loc.x, 0, loc.z);
|
light.target.position.set(loc.x, 0, loc.z)
|
||||||
loop.updatables.push(light);
|
loop.updatables.push(light)
|
||||||
scene.add(light);
|
scene.add(light)
|
||||||
scene.add(light.target);
|
scene.add(light.target)
|
||||||
|
|
||||||
// now figure out the timing and shine a light
|
// now figure out the timing and shine a light
|
||||||
const timeDelayMillis =
|
const timeDelayMillis =
|
||||||
fakeRealRatio * (new Date(claim.issuedAt).getTime() - minTimeMillis);
|
fakeRealRatio * (new Date(claim.issuedAt).getTime() - minTimeMillis)
|
||||||
new TWEEN.Tween(light)
|
new TWEEN.Tween(light)
|
||||||
.delay(timeDelayMillis)
|
.delay(timeDelayMillis)
|
||||||
.to({ intensity: 100 }, 10)
|
.to({ intensity: 100 }, 10)
|
||||||
@@ -110,30 +106,30 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
new TWEEN.Tween(light.position)
|
new TWEEN.Tween(light.position)
|
||||||
.to({ y: 5 }, 5000)
|
.to({ y: 5 }, 5000)
|
||||||
.onComplete(() => {
|
.onComplete(() => {
|
||||||
scene.remove(light);
|
scene.remove(light)
|
||||||
light.dispose();
|
light.dispose()
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.start();
|
.start()
|
||||||
world.lights = [...world.lights, light];
|
world.lights = [...world.lights, light]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.error(
|
logger.error(
|
||||||
"Got bad server response status & data of",
|
'Got bad server response status & data of',
|
||||||
resp.status,
|
resp.status,
|
||||||
resp.data,
|
resp.data
|
||||||
);
|
)
|
||||||
vue.setAlert(
|
vue.setAlert(
|
||||||
"Error With Server",
|
'Error With Server',
|
||||||
"There was an error retrieving your claims from the server.",
|
'There was an error retrieving your claims from the server.'
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Got exception contacting server:", error);
|
logger.error('Got exception contacting server:', error)
|
||||||
vue.setAlert(
|
vue.setAlert(
|
||||||
"Error With Server",
|
'Error With Server',
|
||||||
"There was a problem retrieving your claims from the server.",
|
'There was a problem retrieving your claims from the server.'
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,30 +139,30 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
* @returns {x:float, z:float} where -50 <= x & z < 50
|
* @returns {x:float, z:float} where -50 <= x & z < 50
|
||||||
*/
|
*/
|
||||||
function locForGive(giveClaim, platformWidth, borderWidth) {
|
function locForGive(giveClaim, platformWidth, borderWidth) {
|
||||||
let loc;
|
let loc
|
||||||
if (giveClaim?.claim?.recipient?.identifier) {
|
if (giveClaim?.claim?.recipient?.identifier) {
|
||||||
// this is directly to a person
|
// this is directly to a person
|
||||||
loc = locForEthrDid(giveClaim.claim.recipient.identifier);
|
loc = locForEthrDid(giveClaim.claim.recipient.identifier)
|
||||||
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 };
|
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 }
|
||||||
} else if (giveClaim?.object?.isPartOf?.identifier) {
|
} else if (giveClaim?.object?.isPartOf?.identifier) {
|
||||||
// this is probably to a project
|
// this is probably to a project
|
||||||
const objId = giveClaim.object.isPartOf.identifier;
|
const objId = giveClaim.object.isPartOf.identifier
|
||||||
if (objId.startsWith(ENDORSER_ENTITY_PREFIX)) {
|
if (objId.startsWith(ENDORSER_ENTITY_PREFIX)) {
|
||||||
loc = locForUlid(objId.substring(ENDORSER_ENTITY_PREFIX.length));
|
loc = locForUlid(objId.substring(ENDORSER_ENTITY_PREFIX.length))
|
||||||
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 };
|
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!loc) {
|
if (!loc) {
|
||||||
// it must be outside our known addresses so let's put it somewhere random on the side
|
// it must be outside our known addresses so let's put it somewhere random on the side
|
||||||
const leftSide = Math.random() < 0.5;
|
const leftSide = Math.random() < 0.5
|
||||||
loc = {
|
loc = {
|
||||||
x: leftSide
|
x: leftSide
|
||||||
? -platformWidth / 2 - borderWidth / 2
|
? -platformWidth / 2 - borderWidth / 2
|
||||||
: platformWidth / 2 + borderWidth / 2,
|
: platformWidth / 2 + borderWidth / 2,
|
||||||
z: Math.random() * platformWidth - platformWidth / 2,
|
z: Math.random() * platformWidth - platformWidth / 2
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return loc;
|
}
|
||||||
|
return loc
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,20 +191,20 @@ function locForGive(giveClaim, platformWidth, borderWidth) {
|
|||||||
* @returns {x: float, z: float} where 0 <= x & z < 100
|
* @returns {x: float, z: float} where 0 <= x & z < 100
|
||||||
*/
|
*/
|
||||||
function locForUlid(ulid) {
|
function locForUlid(ulid) {
|
||||||
const xChars = ulid.substring(0, 13).split("").reverse().join("");
|
const xChars = ulid.substring(0, 13).split('').reverse().join('')
|
||||||
const zChars = ulid.substring(13, 26).split("").reverse().join("");
|
const zChars = ulid.substring(13, 26).split('').reverse().join('')
|
||||||
|
|
||||||
// from https://github.com/ulid/javascript/blob/5e9727b527aec5b841737c395a20085c4361e971/lib/index.ts#L21
|
// from https://github.com/ulid/javascript/blob/5e9727b527aec5b841737c395a20085c4361e971/lib/index.ts#L21
|
||||||
const BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford's Base32
|
const BASE32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ' // Crockford's Base32
|
||||||
|
|
||||||
// We're currently only using 1024 possible x and z values
|
// We're currently only using 1024 possible x and z values
|
||||||
// because the display is pretty low-fidelity at this point.
|
// because the display is pretty low-fidelity at this point.
|
||||||
const rawX = BASE32.indexOf(xChars[1]) * 32 + BASE32.indexOf(xChars[0]);
|
const rawX = BASE32.indexOf(xChars[1]) * 32 + BASE32.indexOf(xChars[0])
|
||||||
const rawZ = BASE32.indexOf(zChars[1]) * 32 + BASE32.indexOf(zChars[0]);
|
const rawZ = BASE32.indexOf(zChars[1]) * 32 + BASE32.indexOf(zChars[0])
|
||||||
|
|
||||||
const x = (100 * rawX) / 1024;
|
const x = (100 * rawX) / 1024
|
||||||
const z = (100 * rawZ) / 1024;
|
const z = (100 * rawZ) / 1024
|
||||||
return { x, z };
|
return { x, z }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -219,24 +215,24 @@ function locForUlid(ulid) {
|
|||||||
function locForEthrDid(did) {
|
function locForEthrDid(did) {
|
||||||
// "did:ethr:0x..."
|
// "did:ethr:0x..."
|
||||||
if (did.length < 51) {
|
if (did.length < 51) {
|
||||||
return { x: 0, z: 0 };
|
return { x: 0, z: 0 }
|
||||||
} else {
|
} else {
|
||||||
const randomness = did.substring("did:ethr:0x".length);
|
const randomness = did.substring('did:ethr:0x'.length)
|
||||||
// We'll use all the randomness for fully unique x & z values.
|
// We'll use all the randomness for fully unique x & z values.
|
||||||
// But we'll only calculate this view with the first byte since our rendering resolution is low.
|
// But we'll only calculate this view with the first byte since our rendering resolution is low.
|
||||||
const xOff = parseInt(Number("0x" + randomness.substring(0, 2)), 10);
|
const xOff = parseInt(Number('0x' + randomness.substring(0, 2)), 10)
|
||||||
const x = (xOff * 100) / 256;
|
const x = (xOff * 100) / 256
|
||||||
// ... and since we're reserving 20 bytes total for x, start z with character 20,
|
// ... and since we're reserving 20 bytes total for x, start z with character 20,
|
||||||
// again with one byte.
|
// again with one byte.
|
||||||
const zOff = parseInt(Number("0x" + randomness.substring(20, 22)), 10);
|
const zOff = parseInt(Number('0x' + randomness.substring(20, 22)), 10)
|
||||||
const z = (zOff * 100) / 256;
|
const z = (zOff * 100) / 256
|
||||||
return { x, z };
|
return { x, z }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createLight() {
|
function createLight() {
|
||||||
const light = new THREE.SpotLight(0xffffff, 0, 0, Math.PI / 8, 0.5, 0);
|
const light = new THREE.SpotLight(0xffffff, 0, 0, Math.PI / 8, 0.5, 0)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
light.tick = () => {};
|
light.tick = () => {}
|
||||||
return light;
|
return light
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three";
|
import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from 'three'
|
||||||
|
|
||||||
export function createTerrain(props) {
|
export function createTerrain(props) {
|
||||||
const loader = new TextureLoader();
|
const loader = new TextureLoader()
|
||||||
const height = loader.load("img/textures/leafy-autumn-forest-floor.jpg");
|
const height = loader.load('img/textures/leafy-autumn-forest-floor.jpg')
|
||||||
// w h
|
// w h
|
||||||
const geometry = new PlaneGeometry(props.width, props.height, 64, 64);
|
const geometry = new PlaneGeometry(props.width, props.height, 64, 64)
|
||||||
|
|
||||||
const material = new MeshLambertMaterial({
|
const material = new MeshLambertMaterial({
|
||||||
color: props.color,
|
color: props.color,
|
||||||
flatShading: true,
|
flatShading: true,
|
||||||
map: height,
|
map: height
|
||||||
//displacementMap: height,
|
//displacementMap: height,
|
||||||
//displacementScale: 5,
|
//displacementScale: 5,
|
||||||
});
|
})
|
||||||
|
|
||||||
const plane = new Mesh(geometry, material);
|
const plane = new Mesh(geometry, material)
|
||||||
plane.position.set(0, 0, 0);
|
plane.position.set(0, 0, 0)
|
||||||
plane.rotation.x -= Math.PI * 0.5;
|
plane.rotation.x -= Math.PI * 0.5
|
||||||
|
|
||||||
//Storing our original vertices position on a new attribute
|
//Storing our original vertices position on a new attribute
|
||||||
plane.geometry.attributes.position.originalPosition =
|
plane.geometry.attributes.position.originalPosition =
|
||||||
plane.geometry.attributes.position.array;
|
plane.geometry.attributes.position.array
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
plane.tick = () => {};
|
plane.tick = () => {}
|
||||||
|
|
||||||
return plane;
|
return plane
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Color, Scene } from "three";
|
import { Color, Scene } from 'three'
|
||||||
|
|
||||||
function createScene(color) {
|
function createScene(color) {
|
||||||
const scene = new Scene();
|
const scene = new Scene()
|
||||||
|
|
||||||
scene.background = new Color(color);
|
scene.background = new Color(color)
|
||||||
//scene.fog = new Fog(color, 60, 90);
|
//scene.fog = new Fog(color, 60, 90);
|
||||||
return scene;
|
return scene
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createScene };
|
export { createScene }
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
import { Clock } from "three";
|
import { Clock } from 'three'
|
||||||
|
|
||||||
const clock = new Clock();
|
const clock = new Clock()
|
||||||
|
|
||||||
class Loop {
|
class Loop {
|
||||||
constructor(camera, scene, renderer) {
|
constructor(camera, scene, renderer) {
|
||||||
this.camera = camera;
|
this.camera = camera
|
||||||
this.scene = scene;
|
this.scene = scene
|
||||||
this.renderer = renderer;
|
this.renderer = renderer
|
||||||
this.updatables = [];
|
this.updatables = []
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
this.renderer.setAnimationLoop(() => {
|
this.renderer.setAnimationLoop(() => {
|
||||||
this.tick();
|
this.tick()
|
||||||
// render a frame
|
// render a frame
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
this.renderer.setAnimationLoop(null);
|
this.renderer.setAnimationLoop(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
tick() {
|
tick() {
|
||||||
const delta = clock.getDelta();
|
const delta = clock.getDelta()
|
||||||
for (const object of this.updatables) {
|
for (const object of this.updatables) {
|
||||||
object.tick(delta);
|
object.tick(delta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Loop };
|
export { Loop }
|
||||||
|
|||||||
@@ -1,33 +1,33 @@
|
|||||||
const setSize = (container, camera, renderer) => {
|
const setSize = (container, camera, renderer) => {
|
||||||
// These are great for full-screen, which adjusts to a window.
|
// These are great for full-screen, which adjusts to a window.
|
||||||
const height = window.innerHeight;
|
const height = window.innerHeight
|
||||||
const width = window.innerWidth - 50;
|
const width = window.innerWidth - 50
|
||||||
// These are better for fitting in a container, which stays that size.
|
// These are better for fitting in a container, which stays that size.
|
||||||
//const height = container.scrollHeight;
|
//const height = container.scrollHeight;
|
||||||
//const width = container.scrollWidth;
|
//const width = container.scrollWidth;
|
||||||
|
|
||||||
camera.aspect = width / height;
|
camera.aspect = width / height
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix()
|
||||||
|
|
||||||
renderer.setSize(width, height);
|
renderer.setSize(width, height)
|
||||||
renderer.setPixelRatio(window.devicePixelRatio);
|
renderer.setPixelRatio(window.devicePixelRatio)
|
||||||
};
|
}
|
||||||
|
|
||||||
class Resizer {
|
class Resizer {
|
||||||
constructor(container, camera, renderer) {
|
constructor(container, camera, renderer) {
|
||||||
// set initial size on load
|
// set initial size on load
|
||||||
setSize(container, camera, renderer);
|
setSize(container, camera, renderer)
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener('resize', () => {
|
||||||
// set the size again if a resize occurs
|
// set the size again if a resize occurs
|
||||||
setSize(container, camera, renderer);
|
setSize(container, camera, renderer)
|
||||||
// perform any custom actions
|
// perform any custom actions
|
||||||
this.onResize();
|
this.onResize()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
onResize() {}
|
onResize() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Resizer };
|
export { Resizer }
|
||||||
|
|||||||
24
src/components/World/systems/controls.js
vendored
24
src/components/World/systems/controls.js
vendored
@@ -1,12 +1,12 @@
|
|||||||
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||||
import { MathUtils } from "three";
|
import { MathUtils } from 'three'
|
||||||
|
|
||||||
function createControls(camera, canvas) {
|
function createControls(camera, canvas) {
|
||||||
const controls = new OrbitControls(camera, canvas);
|
const controls = new OrbitControls(camera, canvas)
|
||||||
|
|
||||||
//enable controls?
|
//enable controls?
|
||||||
controls.enabled = true;
|
controls.enabled = true
|
||||||
controls.autoRotate = false;
|
controls.autoRotate = false
|
||||||
//controls.autoRotateSpeed = 0.2;
|
//controls.autoRotateSpeed = 0.2;
|
||||||
|
|
||||||
// control limits
|
// control limits
|
||||||
@@ -14,8 +14,8 @@ function createControls(camera, canvas) {
|
|||||||
// to prevent the user from clipping with the objects.
|
// to prevent the user from clipping with the objects.
|
||||||
|
|
||||||
// y axis
|
// y axis
|
||||||
controls.minPolarAngle = MathUtils.degToRad(40); // default
|
controls.minPolarAngle = MathUtils.degToRad(40) // default
|
||||||
controls.maxPolarAngle = MathUtils.degToRad(75);
|
controls.maxPolarAngle = MathUtils.degToRad(75)
|
||||||
|
|
||||||
// x axis
|
// x axis
|
||||||
// controls.minAzimuthAngle = ...
|
// controls.minAzimuthAngle = ...
|
||||||
@@ -23,16 +23,16 @@ function createControls(camera, canvas) {
|
|||||||
|
|
||||||
//smooth camera:
|
//smooth camera:
|
||||||
// remember to add to loop updatables to work
|
// remember to add to loop updatables to work
|
||||||
controls.enableDamping = true;
|
controls.enableDamping = true
|
||||||
|
|
||||||
//controls.enableZoom = false;
|
//controls.enableZoom = false;
|
||||||
controls.maxDistance = 250;
|
controls.maxDistance = 250
|
||||||
|
|
||||||
//controls.enablePan = false;
|
//controls.enablePan = false;
|
||||||
|
|
||||||
controls.tick = () => controls.update();
|
controls.tick = () => controls.update()
|
||||||
|
|
||||||
return controls;
|
return controls
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createControls };
|
export { createControls }
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { WebGLRenderer } from "three";
|
import { WebGLRenderer } from 'three'
|
||||||
|
|
||||||
function createRenderer() {
|
function createRenderer() {
|
||||||
const renderer = new WebGLRenderer({ antialias: true });
|
const renderer = new WebGLRenderer({ antialias: true })
|
||||||
|
|
||||||
// turn on the physically correct lighting model
|
// turn on the physically correct lighting model
|
||||||
// (The browser complains: "THREE.WebGLRenderer: the property .physicallyCorrectLights has been removed. Set renderer.useLegacyLights instead." However, that changes the lighting in a way that doesn't look better.)
|
// (The browser complains: "THREE.WebGLRenderer: the property .physicallyCorrectLights has been removed. Set renderer.useLegacyLights instead." However, that changes the lighting in a way that doesn't look better.)
|
||||||
renderer.physicallyCorrectLights = true;
|
renderer.physicallyCorrectLights = true
|
||||||
|
|
||||||
return renderer;
|
return renderer
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createRenderer };
|
export { createRenderer }
|
||||||
|
|||||||
@@ -6,64 +6,63 @@
|
|||||||
export enum AppString {
|
export enum AppString {
|
||||||
// This is used in titles and verbiage inside the app.
|
// This is used in titles and verbiage inside the app.
|
||||||
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
|
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
|
||||||
APP_NAME = "Time Safari",
|
APP_NAME = 'Time Safari',
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
PROD_ENDORSER_API_SERVER = 'https://api.endorser.ch',
|
||||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
TEST_ENDORSER_API_SERVER = 'https://test-api.endorser.ch',
|
||||||
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
LOCAL_ENDORSER_API_SERVER = 'http://localhost:3000',
|
||||||
|
|
||||||
PROD_IMAGE_API_SERVER = "https://image-api.timesafari.app",
|
PROD_IMAGE_API_SERVER = 'https://image-api.timesafari.app',
|
||||||
TEST_IMAGE_API_SERVER = "https://test-image-api.timesafari.app",
|
TEST_IMAGE_API_SERVER = 'https://test-image-api.timesafari.app',
|
||||||
LOCAL_IMAGE_API_SERVER = "http://localhost:3001",
|
LOCAL_IMAGE_API_SERVER = 'http://localhost:3001',
|
||||||
|
|
||||||
PROD_PARTNER_API_SERVER = "https://partner-api.endorser.ch",
|
PROD_PARTNER_API_SERVER = 'https://partner-api.endorser.ch',
|
||||||
TEST_PARTNER_API_SERVER = "https://test-partner-api.endorser.ch",
|
TEST_PARTNER_API_SERVER = 'https://test-partner-api.endorser.ch',
|
||||||
LOCAL_PARTNER_API_SERVER = LOCAL_ENDORSER_API_SERVER,
|
LOCAL_PARTNER_API_SERVER = LOCAL_ENDORSER_API_SERVER,
|
||||||
|
|
||||||
PROD_PUSH_SERVER = "https://timesafari.app",
|
PROD_PUSH_SERVER = 'https://timesafari.app',
|
||||||
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
TEST1_PUSH_SERVER = 'https://test.timesafari.app',
|
||||||
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
|
TEST2_PUSH_SERVER = 'https://timesafari-pwa.anomalistlabs.com',
|
||||||
|
|
||||||
NO_CONTACT_NAME = "(no name)",
|
NO_CONTACT_NAME = '(no name)'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const APP_SERVER =
|
export const APP_SERVER =
|
||||||
import.meta.env.VITE_APP_SERVER || "https://timesafari.app";
|
import.meta.env.VITE_APP_SERVER || 'https://timesafari.app'
|
||||||
|
|
||||||
export const DEFAULT_ENDORSER_API_SERVER =
|
export const DEFAULT_ENDORSER_API_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
||||||
AppString.TEST_ENDORSER_API_SERVER;
|
AppString.TEST_ENDORSER_API_SERVER
|
||||||
|
|
||||||
export const DEFAULT_IMAGE_API_SERVER =
|
export const DEFAULT_IMAGE_API_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
|
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
|
||||||
AppString.TEST_IMAGE_API_SERVER;
|
AppString.TEST_IMAGE_API_SERVER
|
||||||
|
|
||||||
export const DEFAULT_PARTNER_API_SERVER =
|
export const DEFAULT_PARTNER_API_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
|
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
|
||||||
AppString.TEST_PARTNER_API_SERVER;
|
AppString.TEST_PARTNER_API_SERVER
|
||||||
|
|
||||||
export const DEFAULT_PUSH_SERVER =
|
export const DEFAULT_PUSH_SERVER =
|
||||||
window.location.protocol + "//" + window.location.host;
|
window.location.protocol + '//' + window.location.host
|
||||||
|
|
||||||
export const IMAGE_TYPE_PROFILE = "profile";
|
export const IMAGE_TYPE_PROFILE = 'profile'
|
||||||
|
|
||||||
export const PASSKEYS_ENABLED =
|
export const PASSKEYS_ENABLED = !!import.meta.env.VITE_PASSKEYS_ENABLED || false
|
||||||
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The possible values for "group" and "type" are in App.vue.
|
* The possible values for "group" and "type" are in App.vue.
|
||||||
* Some of this comes from the notiwind package, some is custom.
|
* Some of this comes from the notiwind package, some is custom.
|
||||||
*/
|
*/
|
||||||
export interface NotificationIface {
|
export interface NotificationIface {
|
||||||
group: string; // "alert" | "modal"
|
group: string // "alert" | "modal"
|
||||||
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
type: string // "toast" | "info" | "success" | "warning" | "danger"
|
||||||
title: string;
|
title: string
|
||||||
text?: string;
|
text?: string
|
||||||
callback?: (success: boolean) => Promise<void>; // if this triggered an action
|
callback?: (success: boolean) => Promise<void> // if this triggered an action
|
||||||
noText?: string;
|
noText?: string
|
||||||
onCancel?: (stopAsking?: boolean) => Promise<void>;
|
onCancel?: (stopAsking?: boolean) => Promise<void>
|
||||||
onNo?: (stopAsking?: boolean) => Promise<void>;
|
onNo?: (stopAsking?: boolean) => Promise<void>
|
||||||
onYes?: () => Promise<void>;
|
onYes?: () => Promise<void>
|
||||||
promptToStopAsking?: boolean;
|
promptToStopAsking?: boolean
|
||||||
yesText?: string;
|
yesText?: string
|
||||||
}
|
}
|
||||||
|
|||||||
172
src/db/index.ts
172
src/db/index.ts
@@ -1,44 +1,44 @@
|
|||||||
import BaseDexie, { Table } from "dexie";
|
import BaseDexie, { Table } from 'dexie'
|
||||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
import { encrypted, Encryption } from '@pvermeer/dexie-encrypted-addon'
|
||||||
import * as R from "ramda";
|
import * as R from 'ramda'
|
||||||
|
|
||||||
import { Account, AccountsSchema } from "./tables/accounts";
|
import { Account, AccountsSchema } from './tables/accounts'
|
||||||
import { Contact, ContactSchema } from "./tables/contacts";
|
import { Contact, ContactSchema } from './tables/contacts'
|
||||||
import { Log, LogSchema } from "./tables/logs";
|
import { Log, LogSchema } from './tables/logs'
|
||||||
import { MASTER_SECRET_KEY, Secret, SecretSchema } from "./tables/secret";
|
import { MASTER_SECRET_KEY, Secret, SecretSchema } from './tables/secret'
|
||||||
import {
|
import {
|
||||||
MASTER_SETTINGS_KEY,
|
MASTER_SETTINGS_KEY,
|
||||||
Settings,
|
Settings,
|
||||||
SettingsSchema,
|
SettingsSchema
|
||||||
} from "./tables/settings";
|
} from './tables/settings'
|
||||||
import { Temp, TempSchema } from "./tables/temp";
|
import { Temp, TempSchema } from './tables/temp'
|
||||||
import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app";
|
import { DEFAULT_ENDORSER_API_SERVER } from '../constants/app'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../utils/logger'
|
||||||
|
|
||||||
// Define types for tables that hold sensitive and non-sensitive data
|
// Define types for tables that hold sensitive and non-sensitive data
|
||||||
type SecretTable = { secret: Table<Secret> };
|
type SecretTable = { secret: Table<Secret> }
|
||||||
type SensitiveTables = { accounts: Table<Account> };
|
type SensitiveTables = { accounts: Table<Account> }
|
||||||
type NonsensitiveTables = {
|
type NonsensitiveTables = {
|
||||||
contacts: Table<Contact>;
|
contacts: Table<Contact>
|
||||||
logs: Table<Log>;
|
logs: Table<Log>
|
||||||
settings: Table<Settings>;
|
settings: Table<Settings>
|
||||||
temp: Table<Temp>;
|
temp: Table<Temp>
|
||||||
};
|
}
|
||||||
|
|
||||||
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
||||||
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T
|
||||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T
|
||||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||||
BaseDexie & T;
|
BaseDexie & T
|
||||||
|
|
||||||
//// Initialize the DBs, starting with the sensitive ones.
|
//// Initialize the DBs, starting with the sensitive ones.
|
||||||
|
|
||||||
// Initialize Dexie database for secret, which is then used to encrypt accountsDB
|
// Initialize Dexie database for secret, which is then used to encrypt accountsDB
|
||||||
export const secretDB = new BaseDexie("TimeSafariSecret") as SecretDexie;
|
export const secretDB = new BaseDexie('TimeSafariSecret') as SecretDexie
|
||||||
secretDB.version(1).stores(SecretSchema);
|
secretDB.version(1).stores(SecretSchema)
|
||||||
|
|
||||||
// Initialize Dexie database for accounts
|
// Initialize Dexie database for accounts
|
||||||
const accountsDexie = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
const accountsDexie = new BaseDexie('TimeSafariAccounts') as SensitiveDexie
|
||||||
|
|
||||||
// Instead of accountsDBPromise, use libs/util retrieveAccountMetadata or retrieveFullyDecryptedAccount
|
// Instead of accountsDBPromise, use libs/util retrieveAccountMetadata or retrieveFullyDecryptedAccount
|
||||||
// so that it's clear whether the usage needs the private key inside.
|
// so that it's clear whether the usage needs the private key inside.
|
||||||
@@ -49,13 +49,13 @@ const accountsDexie = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
|||||||
// to a user action required to unlock the data.
|
// to a user action required to unlock the data.
|
||||||
export const accountsDBPromise = useSecretAndInitializeAccountsDB(
|
export const accountsDBPromise = useSecretAndInitializeAccountsDB(
|
||||||
secretDB,
|
secretDB,
|
||||||
accountsDexie,
|
accountsDexie
|
||||||
);
|
)
|
||||||
|
|
||||||
//// Now initialize the other DB.
|
//// Now initialize the other DB.
|
||||||
|
|
||||||
// Initialize Dexie databases for non-sensitive data
|
// Initialize Dexie databases for non-sensitive data
|
||||||
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
export const db = new BaseDexie('TimeSafari') as NonsensitiveDexie
|
||||||
|
|
||||||
// Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning
|
// Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning
|
||||||
|
|
||||||
@@ -64,31 +64,31 @@ export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
|||||||
db.version(2).stores({
|
db.version(2).stores({
|
||||||
...ContactSchema,
|
...ContactSchema,
|
||||||
...LogSchema,
|
...LogSchema,
|
||||||
...{ settings: "id" }, // old Settings schema
|
...{ settings: 'id' } // old Settings schema
|
||||||
});
|
})
|
||||||
// v3 added Temp
|
// v3 added Temp
|
||||||
db.version(3).stores(TempSchema);
|
db.version(3).stores(TempSchema)
|
||||||
db.version(4)
|
db.version(4)
|
||||||
.stores(SettingsSchema)
|
.stores(SettingsSchema)
|
||||||
.upgrade((tx) => {
|
.upgrade((tx) => {
|
||||||
return tx
|
return tx
|
||||||
.table("settings")
|
.table('settings')
|
||||||
.toCollection()
|
.toCollection()
|
||||||
.modify((settings) => {
|
.modify((settings) => {
|
||||||
settings.accountDid = ""; // make it non-null for the default master settings, but still indexable
|
settings.accountDid = '' // make it non-null for the default master settings, but still indexable
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: Settings = {
|
const DEFAULT_SETTINGS: Settings = {
|
||||||
id: MASTER_SETTINGS_KEY,
|
id: MASTER_SETTINGS_KEY,
|
||||||
activeDid: undefined,
|
activeDid: undefined,
|
||||||
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
apiServer: DEFAULT_ENDORSER_API_SERVER
|
||||||
};
|
}
|
||||||
|
|
||||||
// Event handler to initialize the non-sensitive database with default settings
|
// Event handler to initialize the non-sensitive database with default settings
|
||||||
db.on("populate", async () => {
|
db.on('populate', async () => {
|
||||||
await db.settings.add(DEFAULT_SETTINGS);
|
await db.settings.add(DEFAULT_SETTINGS)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Manage the encryption key.
|
// Manage the encryption key.
|
||||||
|
|
||||||
@@ -114,130 +114,130 @@ db.on("populate", async () => {
|
|||||||
|
|
||||||
async function useSecretAndInitializeAccountsDB(
|
async function useSecretAndInitializeAccountsDB(
|
||||||
secretDB: SecretDexie,
|
secretDB: SecretDexie,
|
||||||
accountsDB: SensitiveDexie,
|
accountsDB: SensitiveDexie
|
||||||
): Promise<SensitiveDexie> {
|
): Promise<SensitiveDexie> {
|
||||||
return secretDB
|
return secretDB
|
||||||
.open()
|
.open()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return secretDB.secret.get(MASTER_SECRET_KEY);
|
return secretDB.secret.get(MASTER_SECRET_KEY)
|
||||||
})
|
})
|
||||||
.then((secretRow?: Secret) => {
|
.then((secretRow?: Secret) => {
|
||||||
let secret = secretRow?.secret;
|
let secret = secretRow?.secret
|
||||||
if (secret != null) {
|
if (secret != null) {
|
||||||
// they already have it in IndexedDB, so just pass it along
|
// they already have it in IndexedDB, so just pass it along
|
||||||
return secret;
|
return secret
|
||||||
} else {
|
} else {
|
||||||
// check localStorage (for users before v 0.3.37)
|
// check localStorage (for users before v 0.3.37)
|
||||||
const localSecret = localStorage.getItem("secret");
|
const localSecret = localStorage.getItem('secret')
|
||||||
if (localSecret != null) {
|
if (localSecret != null) {
|
||||||
// they had one, so we want to move it to IndexedDB
|
// they had one, so we want to move it to IndexedDB
|
||||||
secret = localSecret;
|
secret = localSecret
|
||||||
} else {
|
} else {
|
||||||
// they didn't have one, so let's generate one
|
// they didn't have one, so let's generate one
|
||||||
secret = Encryption.createRandomEncryptionKey();
|
secret = Encryption.createRandomEncryptionKey()
|
||||||
}
|
}
|
||||||
// it is not in IndexedDB, so add it now
|
// it is not in IndexedDB, so add it now
|
||||||
return secretDB.secret
|
return secretDB.secret
|
||||||
.add({ id: MASTER_SECRET_KEY, secret })
|
.add({ id: MASTER_SECRET_KEY, secret })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return secret;
|
return secret
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((secret?: string) => {
|
.then((secret?: string) => {
|
||||||
if (secret == null) {
|
if (secret == null) {
|
||||||
throw new Error("No secret found or created.");
|
throw new Error('No secret found or created.')
|
||||||
} else {
|
} else {
|
||||||
// apply encryption to the sensitive database using the secret key
|
// apply encryption to the sensitive database using the secret key
|
||||||
encrypted(accountsDB, { secretKey: secret });
|
encrypted(accountsDB, { secretKey: secret })
|
||||||
accountsDB.version(1).stores(AccountsSchema);
|
accountsDB.version(1).stores(AccountsSchema)
|
||||||
accountsDB.open();
|
accountsDB.open()
|
||||||
return accountsDB;
|
return accountsDB
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logConsoleAndDb("Error processing secret & encrypted accountsDB.", error);
|
logConsoleAndDb('Error processing secret & encrypted accountsDB.', error)
|
||||||
// alert("There was an error processing encrypted data. See the Help page.");
|
// alert("There was an error processing encrypted data. See the Help page.");
|
||||||
throw error;
|
throw error
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieves default settings
|
// retrieves default settings
|
||||||
// calls db.open()
|
// calls db.open()
|
||||||
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
||||||
await db.open();
|
await db.open()
|
||||||
return (await db.settings.get(MASTER_SETTINGS_KEY)) || DEFAULT_SETTINGS;
|
return (await db.settings.get(MASTER_SETTINGS_KEY)) || DEFAULT_SETTINGS
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||||
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
const defaultSettings = await retrieveSettingsForDefaultAccount()
|
||||||
if (!defaultSettings.activeDid) {
|
if (!defaultSettings.activeDid) {
|
||||||
return defaultSettings;
|
return defaultSettings
|
||||||
} else {
|
} else {
|
||||||
const overrideSettings =
|
const overrideSettings =
|
||||||
(await db.settings
|
(await db.settings
|
||||||
.where("accountDid")
|
.where('accountDid')
|
||||||
.equals(defaultSettings.activeDid)
|
.equals(defaultSettings.activeDid)
|
||||||
.first()) || {};
|
.first()) || {}
|
||||||
return R.mergeDeepRight(defaultSettings, overrideSettings);
|
return R.mergeDeepRight(defaultSettings, overrideSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateDefaultSettings(
|
export async function updateDefaultSettings(
|
||||||
settingsChanges: Settings,
|
settingsChanges: Settings
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
delete settingsChanges.accountDid; // just in case
|
delete settingsChanges.accountDid // just in case
|
||||||
// ensure there is no "id" that would override the key
|
// ensure there is no "id" that would override the key
|
||||||
delete settingsChanges.id;
|
delete settingsChanges.id
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, settingsChanges);
|
await db.settings.update(MASTER_SETTINGS_KEY, settingsChanges)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateAccountSettings(
|
export async function updateAccountSettings(
|
||||||
accountDid: string,
|
accountDid: string,
|
||||||
settingsChanges: Settings,
|
settingsChanges: Settings
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
settingsChanges.accountDid = accountDid;
|
settingsChanges.accountDid = accountDid
|
||||||
delete settingsChanges.id; // key off account, not ID
|
delete settingsChanges.id // key off account, not ID
|
||||||
const result = await db.settings
|
const result = await db.settings
|
||||||
.where("accountDid")
|
.where('accountDid')
|
||||||
.equals(settingsChanges.accountDid)
|
.equals(settingsChanges.accountDid)
|
||||||
.modify(settingsChanges);
|
.modify(settingsChanges)
|
||||||
if (result === 0) {
|
if (result === 0) {
|
||||||
if (!settingsChanges.id) {
|
if (!settingsChanges.id) {
|
||||||
// It is unfortunate that we have to set this explicitly.
|
// It is unfortunate that we have to set this explicitly.
|
||||||
// We didn't make id a "++id" at the beginning and Dexie won't let us change it,
|
// We didn't make id a "++id" at the beginning and Dexie won't let us change it,
|
||||||
// plus we made our first settings objects MASTER_SETTINGS_KEY = 1 instead of 0
|
// plus we made our first settings objects MASTER_SETTINGS_KEY = 1 instead of 0
|
||||||
settingsChanges.id = (await db.settings.count()) + 1;
|
settingsChanges.id = (await db.settings.count()) + 1
|
||||||
}
|
}
|
||||||
await db.settings.add(settingsChanges);
|
await db.settings.add(settingsChanges)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logToDb(message: string): Promise<void> {
|
export async function logToDb(message: string): Promise<void> {
|
||||||
await db.open();
|
await db.open()
|
||||||
const todayKey = new Date().toDateString();
|
const todayKey = new Date().toDateString()
|
||||||
// only keep one day's worth of logs
|
// only keep one day's worth of logs
|
||||||
const previous = await db.logs.get(todayKey);
|
const previous = await db.logs.get(todayKey)
|
||||||
if (!previous) {
|
if (!previous) {
|
||||||
// when this is today's first log, clear out everything previous
|
// when this is today's first log, clear out everything previous
|
||||||
// to avoid the log table getting too large
|
// to avoid the log table getting too large
|
||||||
// (let's limit a different way someday)
|
// (let's limit a different way someday)
|
||||||
await db.logs.clear();
|
await db.logs.clear()
|
||||||
}
|
}
|
||||||
const prevMessages = (previous && previous.message) || "";
|
const prevMessages = (previous && previous.message) || ''
|
||||||
const fullMessage = `${prevMessages}\n${new Date().toISOString()} ${message}`;
|
const fullMessage = `${prevMessages}\n${new Date().toISOString()} ${message}`
|
||||||
await db.logs.update(todayKey, { message: fullMessage });
|
await db.logs.update(todayKey, { message: fullMessage })
|
||||||
}
|
}
|
||||||
|
|
||||||
// similar method is in the sw_scripts/additional-scripts.js file
|
// similar method is in the sw_scripts/additional-scripts.js file
|
||||||
export async function logConsoleAndDb(
|
export async function logConsoleAndDb(
|
||||||
message: string,
|
message: string,
|
||||||
isError = false,
|
isError = false
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (isError) {
|
if (isError) {
|
||||||
logger.error(`${new Date().toISOString()} ${message}`);
|
logger.error(`${new Date().toISOString()} ${message}`)
|
||||||
} else {
|
} else {
|
||||||
logger.log(`${new Date().toISOString()} ${message}`);
|
logger.log(`${new Date().toISOString()} ${message}`)
|
||||||
}
|
}
|
||||||
await logToDb(message);
|
await logToDb(message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,45 +5,45 @@ export type Account = {
|
|||||||
/**
|
/**
|
||||||
* Auto-generated ID by Dexie
|
* Auto-generated ID by Dexie
|
||||||
*/
|
*/
|
||||||
id?: number; // this is only blank on input, when the database assigns it
|
id?: number // this is only blank on input, when the database assigns it
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date the account was created
|
* The date the account was created
|
||||||
*/
|
*/
|
||||||
dateCreated: string;
|
dateCreated: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The derivation path for the account, if this is from a mnemonic
|
* The derivation path for the account, if this is from a mnemonic
|
||||||
*/
|
*/
|
||||||
derivationPath?: string;
|
derivationPath?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decentralized Identifier (DID) for the account
|
* Decentralized Identifier (DID) for the account
|
||||||
*/
|
*/
|
||||||
did: string;
|
did: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stringified JSON containing underlying key material, if generated from a mnemonic
|
* Stringified JSON containing underlying key material, if generated from a mnemonic
|
||||||
* Based on the IIdentifier type from Veramo
|
* Based on the IIdentifier type from Veramo
|
||||||
* @see {@link https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts}
|
* @see {@link https://github.com/uport-project/veramo/blob/next/packages/core-types/src/types/IIdentifier.ts}
|
||||||
*/
|
*/
|
||||||
identity?: string;
|
identity?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mnemonic phrase for the account, if this is from a mnemonic
|
* The mnemonic phrase for the account, if this is from a mnemonic
|
||||||
*/
|
*/
|
||||||
mnemonic?: string;
|
mnemonic?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Webauthn credential ID in hex, if this is from a passkey
|
* The Webauthn credential ID in hex, if this is from a passkey
|
||||||
*/
|
*/
|
||||||
passkeyCredIdHex?: string;
|
passkeyCredIdHex?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The public key in hexadecimal format
|
* The public key in hexadecimal format
|
||||||
*/
|
*/
|
||||||
publicKeyHex: string;
|
publicKeyHex: string
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema for the accounts table in the database.
|
* Schema for the accounts table in the database.
|
||||||
@@ -52,5 +52,5 @@ export type Account = {
|
|||||||
*/
|
*/
|
||||||
export const AccountsSchema = {
|
export const AccountsSchema = {
|
||||||
accounts:
|
accounts:
|
||||||
"++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex",
|
'++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex'
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
export interface ContactMethod {
|
export interface ContactMethod {
|
||||||
label: string;
|
label: string
|
||||||
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
type: string // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
||||||
value: string;
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Contact {
|
export interface Contact {
|
||||||
//
|
//
|
||||||
// When adding a property, consider whether it should be added when exporting & sharing contacts.
|
// When adding a property, consider whether it should be added when exporting & sharing contacts.
|
||||||
|
|
||||||
did: string;
|
did: string
|
||||||
contactMethods?: Array<ContactMethod>;
|
contactMethods?: Array<ContactMethod>
|
||||||
name?: string;
|
name?: string
|
||||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
nextPubKeyHashB64?: string // base64-encoded SHA256 hash of next public key
|
||||||
notes?: string;
|
notes?: string
|
||||||
profileImageUrl?: string;
|
profileImageUrl?: string
|
||||||
publicKeyBase64?: string;
|
publicKeyBase64?: string
|
||||||
seesMe?: boolean; // cached value of the server setting
|
seesMe?: boolean // cached value of the server setting
|
||||||
registered?: boolean; // cached value of the server setting
|
registered?: boolean // cached value of the server setting
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContactSchema = {
|
export const ContactSchema = {
|
||||||
contacts: "&did, name", // no need to key by other things
|
contacts: '&did, name' // no need to key by other things
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export interface Log {
|
export interface Log {
|
||||||
date: string;
|
date: string
|
||||||
message: string;
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LogSchema = {
|
export const LogSchema = {
|
||||||
// Currently keyed by "date" because A) today's log data is what we need so we append, and
|
// Currently keyed by "date" because A) today's log data is what we need so we append, and
|
||||||
// B) we don't want it to grow so we remove everything if this is the first entry today.
|
// B) we don't want it to grow so we remove everything if this is the first entry today.
|
||||||
// See safari-notifications.js logMessage for the associated logic.
|
// See safari-notifications.js logMessage for the associated logic.
|
||||||
logs: "date", // definitely don't key by the potentially large message field
|
logs: 'date' // definitely don't key by the potentially large message field
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ export type Secret = {
|
|||||||
/**
|
/**
|
||||||
* Auto-generated ID by Dexie
|
* Auto-generated ID by Dexie
|
||||||
*/
|
*/
|
||||||
id: number;
|
id: number
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The secret key used to decrypt the identity if they're not using their own password
|
* The secret key used to decrypt the identity if they're not using their own password
|
||||||
*/
|
*/
|
||||||
secret: string;
|
secret: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export const SecretSchema = { secret: "++id, secret" };
|
export const SecretSchema = { secret: '++id, secret' }
|
||||||
|
|
||||||
export const MASTER_SECRET_KEY = 0;
|
export const MASTER_SECRET_KEY = 0
|
||||||
|
|||||||
@@ -2,82 +2,82 @@
|
|||||||
* BoundingBox type describes the geographical bounding box coordinates.
|
* BoundingBox type describes the geographical bounding box coordinates.
|
||||||
*/
|
*/
|
||||||
export type BoundingBox = {
|
export type BoundingBox = {
|
||||||
eastLong: number; // Eastern longitude
|
eastLong: number // Eastern longitude
|
||||||
maxLat: number; // Maximum (Northernmost) latitude
|
maxLat: number // Maximum (Northernmost) latitude
|
||||||
minLat: number; // Minimum (Southernmost) latitude
|
minLat: number // Minimum (Southernmost) latitude
|
||||||
westLong: number; // Western longitude
|
westLong: number // Western longitude
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings type encompasses user-specific configuration details.
|
* Settings type encompasses user-specific configuration details.
|
||||||
*/
|
*/
|
||||||
export type Settings = {
|
export type Settings = {
|
||||||
// default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID
|
// default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID
|
||||||
id?: number; // this is erased for all those entries that are keyed with accountDid
|
id?: number // this is erased for all those entries that are keyed with accountDid
|
||||||
|
|
||||||
// if supplied, this settings record overrides the master record when the user switches to this account
|
// if supplied, this settings record overrides the master record when the user switches to this account
|
||||||
accountDid?: string; // not used in the MASTER_SETTINGS_KEY entry
|
accountDid?: string // not used in the MASTER_SETTINGS_KEY entry
|
||||||
// active Decentralized ID
|
// active Decentralized ID
|
||||||
activeDid?: string; // only used in the MASTER_SETTINGS_KEY entry
|
activeDid?: string // only used in the MASTER_SETTINGS_KEY entry
|
||||||
|
|
||||||
apiServer?: string; // API server URL
|
apiServer?: string // API server URL
|
||||||
|
|
||||||
filterFeedByNearby?: boolean; // filter by nearby
|
filterFeedByNearby?: boolean // filter by nearby
|
||||||
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
|
filterFeedByVisible?: boolean // filter by visible users ie. anyone not hidden
|
||||||
finishedOnboarding?: boolean; // the user has completed the onboarding process
|
finishedOnboarding?: boolean // the user has completed the onboarding process
|
||||||
|
|
||||||
firstName?: string; // user's full name, may be null if unwanted for a particular account
|
firstName?: string // user's full name, may be null if unwanted for a particular account
|
||||||
hideRegisterPromptOnNewContact?: boolean;
|
hideRegisterPromptOnNewContact?: boolean
|
||||||
isRegistered?: boolean;
|
isRegistered?: boolean
|
||||||
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
|
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
|
||||||
lastName?: string; // deprecated - put all names in firstName
|
lastName?: string // deprecated - put all names in firstName
|
||||||
|
|
||||||
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
|
lastAckedOfferToUserJwtId?: string // the last JWT ID for offer-to-user that they've acknowledged seeing
|
||||||
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
|
lastAckedOfferToUserProjectsJwtId?: string // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
|
||||||
|
|
||||||
// The claim list has a most recent one used in notifications that's separate from the last viewed
|
// The claim list has a most recent one used in notifications that's separate from the last viewed
|
||||||
lastNotifiedClaimId?: string;
|
lastNotifiedClaimId?: string
|
||||||
lastViewedClaimId?: string;
|
lastViewedClaimId?: string
|
||||||
|
|
||||||
notifyingNewActivityTime?: string; // set to their chosen time if they have turned on daily check for new activity via the push server
|
notifyingNewActivityTime?: string // set to their chosen time if they have turned on daily check for new activity via the push server
|
||||||
notifyingReminderMessage?: string; // set to their chosen message for a daily reminder
|
notifyingReminderMessage?: string // set to their chosen message for a daily reminder
|
||||||
notifyingReminderTime?: string; // set to their chosen time for a daily reminder
|
notifyingReminderTime?: string // set to their chosen time for a daily reminder
|
||||||
|
|
||||||
partnerApiServer?: string; // partner server API URL
|
partnerApiServer?: string // partner server API URL
|
||||||
|
|
||||||
passkeyExpirationMinutes?: number; // passkey access token time-to-live in minutes
|
passkeyExpirationMinutes?: number // passkey access token time-to-live in minutes
|
||||||
|
|
||||||
profileImageUrl?: string; // may be null if unwanted for a particular account
|
profileImageUrl?: string // may be null if unwanted for a particular account
|
||||||
|
|
||||||
// Array of named search boxes defined by bounding boxes
|
// Array of named search boxes defined by bounding boxes
|
||||||
searchBoxes?: Array<{
|
searchBoxes?: Array<{
|
||||||
name: string;
|
name: string
|
||||||
bbox: BoundingBox;
|
bbox: BoundingBox
|
||||||
}>;
|
}>
|
||||||
|
|
||||||
showContactGivesInline?: boolean; // Display contact inline or not
|
showContactGivesInline?: boolean // Display contact inline or not
|
||||||
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
|
showGeneralAdvanced?: boolean // Show advanced features which don't have their own flag
|
||||||
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
|
showShortcutBvc?: boolean // Show shortcut for Bountiful Voluntaryist Community actions
|
||||||
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
vapid?: string // VAPID (Voluntary Application Server Identification) field for web push
|
||||||
warnIfProdServer?: boolean; // Warn if using a production server
|
warnIfProdServer?: boolean // Warn if using a production server
|
||||||
warnIfTestServer?: boolean; // Warn if using a testing server
|
warnIfTestServer?: boolean // Warn if using a testing server
|
||||||
webPushServer?: string; // Web Push server URL
|
webPushServer?: string // Web Push server URL
|
||||||
};
|
}
|
||||||
|
|
||||||
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
|
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
|
||||||
return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible);
|
return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema for the Settings table in the database.
|
* Schema for the Settings table in the database.
|
||||||
*/
|
*/
|
||||||
export const SettingsSchema = {
|
export const SettingsSchema = {
|
||||||
settings: "id, &accountDid",
|
settings: 'id, &accountDid'
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants.
|
* Constants.
|
||||||
*/
|
*/
|
||||||
export const MASTER_SETTINGS_KEY = 1;
|
export const MASTER_SETTINGS_KEY = 1
|
||||||
|
|
||||||
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;
|
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// for ephemeral uses, eg. passing a blob from the service worker to the main thread
|
// for ephemeral uses, eg. passing a blob from the service worker to the main thread
|
||||||
|
|
||||||
export type Temp = {
|
export type Temp = {
|
||||||
id: string;
|
id: string
|
||||||
blob?: Blob; // deprecated because webkit (Safari) does not support Blob
|
blob?: Blob // deprecated because webkit (Safari) does not support Blob
|
||||||
blobB64?: string; // base64-encoded blob
|
blobB64?: string // base64-encoded blob
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema for the Temp table in the database.
|
* Schema for the Temp table in the database.
|
||||||
*/
|
*/
|
||||||
export const TempSchema = { temp: "id" };
|
export const TempSchema = { temp: 'id' }
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
const { app, BrowserWindow } = require("electron");
|
const { app, BrowserWindow } = require('electron')
|
||||||
const path = require("path");
|
const path = require('path')
|
||||||
const fs = require("fs");
|
const fs = require('fs')
|
||||||
const logger = require("../utils/logger");
|
const logger = require('../utils/logger')
|
||||||
|
|
||||||
// Check if running in dev mode
|
// Check if running in dev mode
|
||||||
const isDev = process.argv.includes("--inspect");
|
const isDev = process.argv.includes('--inspect')
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
// Add before createWindow function
|
// Add before createWindow function
|
||||||
const preloadPath = path.join(__dirname, "preload.js");
|
const preloadPath = path.join(__dirname, 'preload.js')
|
||||||
logger.log("Checking preload path:", preloadPath);
|
logger.log('Checking preload path:', preloadPath)
|
||||||
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
logger.log('Preload exists:', fs.existsSync(preloadPath))
|
||||||
|
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
@@ -21,71 +21,71 @@ function createWindow() {
|
|||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
webSecurity: true,
|
webSecurity: true,
|
||||||
allowRunningInsecureContent: false,
|
allowRunningInsecureContent: false,
|
||||||
preload: path.join(__dirname, "preload.js"),
|
preload: path.join(__dirname, 'preload.js')
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// Always open DevTools for now
|
// Always open DevTools for now
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools()
|
||||||
|
|
||||||
// Intercept requests to fix asset paths
|
// Intercept requests to fix asset paths
|
||||||
mainWindow.webContents.session.webRequest.onBeforeRequest(
|
mainWindow.webContents.session.webRequest.onBeforeRequest(
|
||||||
{
|
{
|
||||||
urls: [
|
urls: [
|
||||||
"file://*/*/assets/*",
|
'file://*/*/assets/*',
|
||||||
"file://*/assets/*",
|
'file://*/assets/*',
|
||||||
"file:///assets/*", // Catch absolute paths
|
'file:///assets/*', // Catch absolute paths
|
||||||
"<all_urls>", // Catch all URLs as a fallback
|
'<all_urls>' // Catch all URLs as a fallback
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
(details, callback) => {
|
(details, callback) => {
|
||||||
let url = details.url;
|
let url = details.url
|
||||||
|
|
||||||
// Handle paths that don't start with file://
|
// Handle paths that don't start with file://
|
||||||
if (!url.startsWith("file://") && url.includes("/assets/")) {
|
if (!url.startsWith('file://') && url.includes('/assets/')) {
|
||||||
url = `file://${path.join(__dirname, "www", url)}`;
|
url = `file://${path.join(__dirname, 'www', url)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle absolute paths starting with /assets/
|
// Handle absolute paths starting with /assets/
|
||||||
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
|
if (url.includes('/assets/') && !url.includes('/www/assets/')) {
|
||||||
const baseDir = url.includes("dist-electron")
|
const baseDir = url.includes('dist-electron')
|
||||||
? url.substring(
|
? url.substring(
|
||||||
0,
|
0,
|
||||||
url.indexOf("/dist-electron") + "/dist-electron".length,
|
url.indexOf('/dist-electron') + '/dist-electron'.length
|
||||||
)
|
)
|
||||||
: `file://${__dirname}`;
|
: `file://${__dirname}`
|
||||||
const assetPath = url.split("/assets/")[1];
|
const assetPath = url.split('/assets/')[1]
|
||||||
const newUrl = `${baseDir}/www/assets/${assetPath}`;
|
const newUrl = `${baseDir}/www/assets/${assetPath}`
|
||||||
callback({ redirectURL: newUrl });
|
callback({ redirectURL: newUrl })
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
callback({}); // No redirect for other URLs
|
callback({}) // No redirect for other URLs
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
// Debug info
|
// Debug info
|
||||||
logger.log("Debug Info:");
|
logger.log('Debug Info:')
|
||||||
logger.log("Running in dev mode:", isDev);
|
logger.log('Running in dev mode:', isDev)
|
||||||
logger.log("App is packaged:", app.isPackaged);
|
logger.log('App is packaged:', app.isPackaged)
|
||||||
logger.log("Process resource path:", process.resourcesPath);
|
logger.log('Process resource path:', process.resourcesPath)
|
||||||
logger.log("App path:", app.getAppPath());
|
logger.log('App path:', app.getAppPath())
|
||||||
logger.log("__dirname:", __dirname);
|
logger.log('__dirname:', __dirname)
|
||||||
logger.log("process.cwd():", process.cwd());
|
logger.log('process.cwd():', process.cwd())
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexPath = path.join(__dirname, "www", "index.html");
|
const indexPath = path.join(__dirname, 'www', 'index.html')
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
logger.log("Loading index from:", indexPath);
|
logger.log('Loading index from:', indexPath)
|
||||||
logger.log("www path:", path.join(__dirname, "www"));
|
logger.log('www path:', path.join(__dirname, 'www'))
|
||||||
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
|
logger.log('www assets path:', path.join(__dirname, 'www', 'assets'))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fs.existsSync(indexPath)) {
|
if (!fs.existsSync(indexPath)) {
|
||||||
logger.error(`Index file not found at: ${indexPath}`);
|
logger.error(`Index file not found at: ${indexPath}`)
|
||||||
throw new Error("Index file not found");
|
throw new Error('Index file not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add CSP headers to allow API connections
|
// Add CSP headers to allow API connections
|
||||||
@@ -94,81 +94,81 @@ function createWindow() {
|
|||||||
callback({
|
callback({
|
||||||
responseHeaders: {
|
responseHeaders: {
|
||||||
...details.responseHeaders,
|
...details.responseHeaders,
|
||||||
"Content-Security-Policy": [
|
'Content-Security-Policy': [
|
||||||
"default-src 'self';" +
|
"default-src 'self';" +
|
||||||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" +
|
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" +
|
||||||
"img-src 'self' data: https: blob:;" +
|
"img-src 'self' data: https: blob:;" +
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
|
||||||
"style-src 'self' 'unsafe-inline';" +
|
"style-src 'self' 'unsafe-inline';" +
|
||||||
"font-src 'self' data:;",
|
"font-src 'self' data:;"
|
||||||
],
|
]
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
// Load the index.html
|
// Load the index.html
|
||||||
mainWindow
|
mainWindow
|
||||||
.loadFile(indexPath)
|
.loadFile(indexPath)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
logger.log("Successfully loaded index.html");
|
logger.log('Successfully loaded index.html')
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools()
|
||||||
logger.log("DevTools opened - running in dev mode");
|
logger.log('DevTools opened - running in dev mode')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
logger.error("Failed to load index.html:", err);
|
logger.error('Failed to load index.html:', err)
|
||||||
logger.error("Attempted path:", indexPath);
|
logger.error('Attempted path:', indexPath)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Listen for console messages from the renderer
|
// Listen for console messages from the renderer
|
||||||
mainWindow.webContents.on("console-message", (_event, level, message) => {
|
mainWindow.webContents.on('console-message', (_event, level, message) => {
|
||||||
logger.log("Renderer Console:", message);
|
logger.log('Renderer Console:', message)
|
||||||
});
|
})
|
||||||
|
|
||||||
// Add right after creating the BrowserWindow
|
// Add right after creating the BrowserWindow
|
||||||
mainWindow.webContents.on(
|
mainWindow.webContents.on(
|
||||||
"did-fail-load",
|
'did-fail-load',
|
||||||
(event, errorCode, errorDescription) => {
|
(event, errorCode, errorDescription) => {
|
||||||
logger.error("Page failed to load:", errorCode, errorDescription);
|
logger.error('Page failed to load:', errorCode, errorDescription)
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
mainWindow.webContents.on("preload-error", (event, preloadPath, error) => {
|
mainWindow.webContents.on('preload-error', (event, preloadPath, error) => {
|
||||||
logger.error("Preload script error:", preloadPath, error);
|
logger.error('Preload script error:', preloadPath, error)
|
||||||
});
|
})
|
||||||
|
|
||||||
mainWindow.webContents.on(
|
mainWindow.webContents.on(
|
||||||
"console-message",
|
'console-message',
|
||||||
(event, level, message, line, sourceId) => {
|
(event, level, message, line, sourceId) => {
|
||||||
logger.log("Renderer Console:", line, sourceId, message);
|
logger.log('Renderer Console:', line, sourceId, message)
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
// Enable remote debugging when in dev mode
|
// Enable remote debugging when in dev mode
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle app ready
|
// Handle app ready
|
||||||
app.whenReady().then(createWindow);
|
app.whenReady().then(createWindow)
|
||||||
|
|
||||||
// Handle all windows closed
|
// Handle all windows closed
|
||||||
app.on("window-all-closed", () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== "darwin") {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit();
|
app.quit()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
// Handle any errors
|
// Handle any errors
|
||||||
process.on("uncaughtException", (error) => {
|
process.on('uncaughtException', (error) => {
|
||||||
logger.error("Uncaught Exception:", error);
|
logger.error('Uncaught Exception:', error)
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -1,78 +1,78 @@
|
|||||||
const { contextBridge, ipcRenderer } = require("electron");
|
const { contextBridge, ipcRenderer } = require('electron')
|
||||||
|
|
||||||
const logger = {
|
const logger = {
|
||||||
log: (message, ...args) => {
|
log: (message, ...args) => {
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.log(message, ...args);
|
console.log(message, ...args)
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
warn: (message, ...args) => {
|
warn: (message, ...args) => {
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.warn(message, ...args);
|
console.warn(message, ...args)
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (message, ...args) => {
|
error: (message, ...args) => {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.error(message, ...args); // Errors should always be logged
|
console.error(message, ...args) // Errors should always be logged
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Use a more direct path resolution approach
|
// Use a more direct path resolution approach
|
||||||
const getPath = (pathType) => {
|
const getPath = (pathType) => {
|
||||||
switch (pathType) {
|
switch (pathType) {
|
||||||
case "userData":
|
case 'userData':
|
||||||
return (
|
return (
|
||||||
process.env.APPDATA ||
|
process.env.APPDATA ||
|
||||||
(process.platform === "darwin"
|
(process.platform === 'darwin'
|
||||||
? `${process.env.HOME}/Library/Application Support`
|
? `${process.env.HOME}/Library/Application Support`
|
||||||
: `${process.env.HOME}/.local/share`)
|
: `${process.env.HOME}/.local/share`)
|
||||||
);
|
)
|
||||||
case "home":
|
case 'home':
|
||||||
return process.env.HOME;
|
return process.env.HOME
|
||||||
case "appPath":
|
case 'appPath':
|
||||||
return process.resourcesPath;
|
return process.resourcesPath
|
||||||
default:
|
default:
|
||||||
return "";
|
return ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
logger.log("Preload script starting...");
|
logger.log('Preload script starting...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
// Path utilities
|
// Path utilities
|
||||||
getPath,
|
getPath,
|
||||||
|
|
||||||
// IPC functions
|
// IPC functions
|
||||||
send: (channel, data) => {
|
send: (channel, data) => {
|
||||||
const validChannels = ["toMain"];
|
const validChannels = ['toMain']
|
||||||
if (validChannels.includes(channel)) {
|
if (validChannels.includes(channel)) {
|
||||||
ipcRenderer.send(channel, data);
|
ipcRenderer.send(channel, data)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
receive: (channel, func) => {
|
receive: (channel, func) => {
|
||||||
const validChannels = ["fromMain"];
|
const validChannels = ['fromMain']
|
||||||
if (validChannels.includes(channel)) {
|
if (validChannels.includes(channel)) {
|
||||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
ipcRenderer.on(channel, (event, ...args) => func(...args))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Environment info
|
// Environment info
|
||||||
env: {
|
env: {
|
||||||
isElectron: true,
|
isElectron: true,
|
||||||
isDev: process.env.NODE_ENV === "development",
|
isDev: process.env.NODE_ENV === 'development'
|
||||||
},
|
},
|
||||||
// Path utilities
|
// Path utilities
|
||||||
getBasePath: () => {
|
getBasePath: () => {
|
||||||
return process.env.NODE_ENV === "development" ? "/" : "./";
|
return process.env.NODE_ENV === 'development' ? '/' : './'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
logger.log("Preload script completed successfully");
|
logger.log('Preload script completed successfully')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error in preload script:", error);
|
logger.error('Error in preload script:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,58 @@
|
|||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from 'axios'
|
||||||
import { GiverReceiverInputInfo } from "../libs/util";
|
import { GiverReceiverInputInfo } from '../libs/util'
|
||||||
import { ErrorResult, ResultWithType } from "./common";
|
import { ErrorResult, ResultWithType } from './common'
|
||||||
|
|
||||||
export interface GiverOutputInfo {
|
export interface GiverOutputInfo {
|
||||||
action: string;
|
action: string
|
||||||
giver?: GiverReceiverInputInfo;
|
giver?: GiverReceiverInputInfo
|
||||||
description?: string;
|
description?: string
|
||||||
amount?: number;
|
amount?: number
|
||||||
unitCode?: string;
|
unitCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaimResult {
|
export interface ClaimResult {
|
||||||
success: { claimId: string; handleId: string };
|
success: { claimId: string; handleId: string }
|
||||||
error: { code: string; message: string };
|
error: { code: string; message: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerifiableCredential {
|
export interface VerifiableCredential {
|
||||||
exp?: number;
|
exp?: number
|
||||||
iat: number;
|
iat: number
|
||||||
iss: string;
|
iss: string
|
||||||
vc: {
|
vc: {
|
||||||
"@context": string[];
|
'@context': string[]
|
||||||
type: string[];
|
type: string[]
|
||||||
credentialSubject: VerifiableCredentialSubject;
|
credentialSubject: VerifiableCredentialSubject
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerifiableCredentialSubject {
|
export interface VerifiableCredentialSubject {
|
||||||
"@context": string;
|
'@context': string
|
||||||
"@type": string;
|
'@type': string
|
||||||
[key: string]: unknown;
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorldProperties {
|
export interface WorldProperties {
|
||||||
startTime?: string;
|
startTime?: string
|
||||||
endTime?: string;
|
endTime?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderInfo {
|
export interface ProviderInfo {
|
||||||
/**
|
/**
|
||||||
* Could be a DID or a handleId that identifies the provider
|
* Could be a DID or a handleId that identifies the provider
|
||||||
*/
|
*/
|
||||||
identifier: string;
|
identifier: string
|
||||||
/**
|
/**
|
||||||
* Indicates if the provider link has been confirmed
|
* Indicates if the provider link has been confirmed
|
||||||
*/
|
*/
|
||||||
linkConfirmed: boolean;
|
linkConfirmed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type for createAndSubmitClaim result
|
// Type for createAndSubmitClaim result
|
||||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult
|
||||||
|
|
||||||
// Update SuccessResult to use ClaimResult
|
// Update SuccessResult to use ClaimResult
|
||||||
export interface SuccessResult extends ResultWithType {
|
export interface SuccessResult extends ResultWithType {
|
||||||
type: "success";
|
type: 'success'
|
||||||
response: AxiosResponse<ClaimResult>;
|
response: AxiosResponse<ClaimResult>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,68 @@
|
|||||||
import { GenericVerifiableCredential } from "./common";
|
import { GenericVerifiableCredential } from './common'
|
||||||
|
|
||||||
export interface AgreeVerifiableCredential {
|
export interface AgreeVerifiableCredential {
|
||||||
"@context": string;
|
'@context': string
|
||||||
"@type": string;
|
'@type': string
|
||||||
object: Record<string, unknown>;
|
object: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id4
|
// https://endorser.ch/doc/html/transactions.html#id4
|
||||||
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
||||||
"@context"?: string;
|
'@context'?: string
|
||||||
"@type": "GiveAction";
|
'@type': 'GiveAction'
|
||||||
agent?: { identifier: string };
|
agent?: { identifier: string }
|
||||||
description?: string;
|
description?: string
|
||||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
|
fulfills?: { '@type': string; identifier?: string; lastClaimId?: string }[]
|
||||||
identifier?: string;
|
identifier?: string
|
||||||
image?: string;
|
image?: string
|
||||||
object?: { amountOfThisGood: number; unitCode: string };
|
object?: { amountOfThisGood: number; unitCode: string }
|
||||||
provider?: GenericVerifiableCredential;
|
provider?: GenericVerifiableCredential
|
||||||
recipient?: { identifier: string };
|
recipient?: { identifier: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id8
|
// https://endorser.ch/doc/html/transactions.html#id8
|
||||||
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
||||||
"@context"?: string;
|
'@context'?: string
|
||||||
"@type": "Offer";
|
'@type': 'Offer'
|
||||||
description?: string;
|
description?: string
|
||||||
includesObject?: { amountOfThisGood: number; unitCode: string };
|
includesObject?: { amountOfThisGood: number; unitCode: string }
|
||||||
itemOffered?: {
|
itemOffered?: {
|
||||||
description?: string;
|
description?: string
|
||||||
isPartOf?: {
|
isPartOf?: {
|
||||||
identifier?: string;
|
identifier?: string
|
||||||
lastClaimId?: string;
|
lastClaimId?: string
|
||||||
"@type"?: string;
|
'@type'?: string
|
||||||
name?: string;
|
name?: string
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
offeredBy?: { identifier: string };
|
offeredBy?: { identifier: string }
|
||||||
recipient?: { identifier: string };
|
recipient?: { identifier: string }
|
||||||
validThrough?: string;
|
validThrough?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id7
|
// https://endorser.ch/doc/html/transactions.html#id7
|
||||||
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
||||||
"@context": "https://schema.org";
|
'@context': 'https://schema.org'
|
||||||
"@type": "PlanAction";
|
'@type': 'PlanAction'
|
||||||
name: string;
|
name: string
|
||||||
agent?: { identifier: string };
|
agent?: { identifier: string }
|
||||||
description?: string;
|
description?: string
|
||||||
identifier?: string;
|
identifier?: string
|
||||||
lastClaimId?: string;
|
lastClaimId?: string
|
||||||
location?: {
|
location?: {
|
||||||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
geo: { '@type': 'GeoCoordinates'; latitude: number; longitude: number }
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AKA Registration & RegisterAction
|
// AKA Registration & RegisterAction
|
||||||
export interface RegisterVerifiableCredential {
|
export interface RegisterVerifiableCredential {
|
||||||
"@context": string;
|
'@context': string
|
||||||
"@type": "RegisterAction";
|
'@type': 'RegisterAction'
|
||||||
agent: { identifier: string };
|
agent: { identifier: string }
|
||||||
identifier?: string;
|
identifier?: string
|
||||||
object: string;
|
object: string
|
||||||
participant?: { identifier: string };
|
participant?: { identifier: string }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,36 @@
|
|||||||
// similar to VerifiableCredentialSubject... maybe rename this
|
// similar to VerifiableCredentialSubject... maybe rename this
|
||||||
export interface GenericVerifiableCredential {
|
export interface GenericVerifiableCredential {
|
||||||
"@context"?: string;
|
'@context'?: string
|
||||||
"@type": string;
|
'@type': string
|
||||||
[key: string]: unknown;
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
||||||
claim: T;
|
claim: T
|
||||||
claimType?: string;
|
claimType?: string
|
||||||
handleId: string;
|
handleId: string
|
||||||
id: string;
|
id: string
|
||||||
issuedAt: string;
|
issuedAt: string
|
||||||
issuer: string;
|
issuer: string
|
||||||
publicUrls?: Record<string, string>;
|
publicUrls?: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultWithType {
|
export interface ResultWithType {
|
||||||
type: string;
|
type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorResponse {
|
export interface ErrorResponse {
|
||||||
error?: {
|
error?: {
|
||||||
message?: string;
|
message?: string
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternalError {
|
export interface InternalError {
|
||||||
error: string;
|
error: string
|
||||||
userMessage?: string;
|
userMessage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorResult extends ResultWithType {
|
export interface ErrorResult extends ResultWithType {
|
||||||
type: "error";
|
type: 'error'
|
||||||
error: InternalError;
|
error: InternalError
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface DeepLinkError extends Error {
|
export interface DeepLinkError extends Error {
|
||||||
code: string;
|
code: string
|
||||||
details?: unknown;
|
details?: unknown
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export * from "./claims";
|
export * from './claims'
|
||||||
export * from "./claims-result";
|
export * from './claims-result'
|
||||||
export * from "./common";
|
export * from './common'
|
||||||
export * from "./limits";
|
export * from './limits'
|
||||||
export * from "./records";
|
export * from './records'
|
||||||
export * from "./user";
|
export * from './user'
|
||||||
export * from "./deepLinks";
|
export * from './deepLinks'
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export interface EndorserRateLimits {
|
export interface EndorserRateLimits {
|
||||||
doneClaimsThisWeek: string;
|
doneClaimsThisWeek: string
|
||||||
doneRegistrationsThisMonth: string;
|
doneRegistrationsThisMonth: string
|
||||||
maxClaimsPerWeek: string;
|
maxClaimsPerWeek: string
|
||||||
maxRegistrationsPerMonth: string;
|
maxRegistrationsPerMonth: string
|
||||||
nextMonthBeginDateTime: string;
|
nextMonthBeginDateTime: string
|
||||||
nextWeekBeginDateTime: string;
|
nextWeekBeginDateTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImageRateLimits {
|
export interface ImageRateLimits {
|
||||||
doneImagesThisWeek: string;
|
doneImagesThisWeek: string
|
||||||
maxImagesPerWeek: string;
|
maxImagesPerWeek: string
|
||||||
nextWeekBeginDateTime: string;
|
nextWeekBeginDateTime: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,64 @@
|
|||||||
import { GiveVerifiableCredential, OfferVerifiableCredential } from "./claims";
|
import { GiveVerifiableCredential, OfferVerifiableCredential } from './claims'
|
||||||
|
|
||||||
// a summary record; the VC is found the fullClaim field
|
// a summary record; the VC is found the fullClaim field
|
||||||
export interface GiveSummaryRecord {
|
export interface GiveSummaryRecord {
|
||||||
[x: string]: PropertyKey | undefined | GiveVerifiableCredential;
|
[x: string]: PropertyKey | undefined | GiveVerifiableCredential
|
||||||
type?: string;
|
type?: string
|
||||||
agentDid: string;
|
agentDid: string
|
||||||
amount: number;
|
amount: number
|
||||||
amountConfirmed: number;
|
amountConfirmed: number
|
||||||
description: string;
|
description: string
|
||||||
fullClaim: GiveVerifiableCredential;
|
fullClaim: GiveVerifiableCredential
|
||||||
fulfillsHandleId: string;
|
fulfillsHandleId: string
|
||||||
fulfillsPlanHandleId?: string;
|
fulfillsPlanHandleId?: string
|
||||||
fulfillsType?: string;
|
fulfillsType?: string
|
||||||
handleId: string;
|
handleId: string
|
||||||
issuedAt: string;
|
issuedAt: string
|
||||||
issuerDid: string;
|
issuerDid: string
|
||||||
jwtId: string;
|
jwtId: string
|
||||||
providerPlanHandleId?: string;
|
providerPlanHandleId?: string
|
||||||
recipientDid: string;
|
recipientDid: string
|
||||||
unit: string;
|
unit: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// a summary record; the VC is found the fullClaim field
|
// a summary record; the VC is found the fullClaim field
|
||||||
export interface OfferSummaryRecord {
|
export interface OfferSummaryRecord {
|
||||||
amount: number;
|
amount: number
|
||||||
amountGiven: number;
|
amountGiven: number
|
||||||
amountGivenConfirmed: number;
|
amountGivenConfirmed: number
|
||||||
fullClaim: OfferVerifiableCredential;
|
fullClaim: OfferVerifiableCredential
|
||||||
fulfillsPlanHandleId: string;
|
fulfillsPlanHandleId: string
|
||||||
handleId: string;
|
handleId: string
|
||||||
issuerDid: string;
|
issuerDid: string
|
||||||
jwtId: string;
|
jwtId: string
|
||||||
nonAmountGivenConfirmed: number;
|
nonAmountGivenConfirmed: number
|
||||||
objectDescription: string;
|
objectDescription: string
|
||||||
offeredByDid: string;
|
offeredByDid: string
|
||||||
recipientDid: string;
|
recipientDid: string
|
||||||
requirementsMet: boolean;
|
requirementsMet: boolean
|
||||||
unit: string;
|
unit: string
|
||||||
validThrough: string;
|
validThrough: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
|
export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
|
||||||
planName: string;
|
planName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// a summary record; the VC is not currently part of this record
|
// a summary record; the VC is not currently part of this record
|
||||||
export interface PlanSummaryRecord {
|
export interface PlanSummaryRecord {
|
||||||
agentDid?: string;
|
agentDid?: string
|
||||||
description: string;
|
description: string
|
||||||
endTime?: string;
|
endTime?: string
|
||||||
fulfillsPlanHandleId: string;
|
fulfillsPlanHandleId: string
|
||||||
handleId: string;
|
handleId: string
|
||||||
image?: string;
|
image?: string
|
||||||
issuerDid: string;
|
issuerDid: string
|
||||||
locLat?: number;
|
locLat?: number
|
||||||
locLon?: number;
|
locLon?: number
|
||||||
name?: string;
|
name?: string
|
||||||
startTime?: string;
|
startTime?: string
|
||||||
url?: string;
|
url?: string
|
||||||
jwtId?: string;
|
jwtId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,23 +71,23 @@ export interface PlanData {
|
|||||||
/**
|
/**
|
||||||
* Description of the project
|
* Description of the project
|
||||||
**/
|
**/
|
||||||
description: string;
|
description: string
|
||||||
/**
|
/**
|
||||||
* URL referencing information about the project
|
* URL referencing information about the project
|
||||||
**/
|
**/
|
||||||
handleId: string;
|
handleId: string
|
||||||
image?: string;
|
image?: string
|
||||||
/**
|
/**
|
||||||
* The DID of the issuer
|
* The DID of the issuer
|
||||||
*/
|
*/
|
||||||
issuerDid: string;
|
issuerDid: string
|
||||||
/**
|
/**
|
||||||
* Name of the project
|
* Name of the project
|
||||||
**/
|
**/
|
||||||
name: string;
|
name: string
|
||||||
/**
|
/**
|
||||||
* The identifier of the project record -- different from jwtId
|
* The identifier of the project record -- different from jwtId
|
||||||
* (Maybe we should use the jwtId to iterate through the records instead.)
|
* (Maybe we should use the jwtId to iterate through the records instead.)
|
||||||
**/
|
**/
|
||||||
rowId?: string;
|
rowId?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export interface UserInfo {
|
export interface UserInfo {
|
||||||
did: string;
|
did: string
|
||||||
name: string;
|
name: string
|
||||||
publicEncKey: string;
|
publicEncKey: string
|
||||||
registered: boolean;
|
registered: boolean
|
||||||
profileImageUrl?: string;
|
profileImageUrl?: string
|
||||||
nextPublicEncKeyHash?: string;
|
nextPublicEncKeyHash?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
import {
|
import {
|
||||||
App as CapacitorApp,
|
App as CapacitorApp,
|
||||||
AppLaunchUrl,
|
AppLaunchUrl,
|
||||||
BackButtonListener,
|
BackButtonListener
|
||||||
} from "../../../node_modules/@capacitor/app";
|
} from '../../../node_modules/@capacitor/app'
|
||||||
import type { PluginListenerHandle } from "@capacitor/core";
|
import type { PluginListenerHandle } from '@capacitor/core'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface defining the app event listener functionality
|
* Interface defining the app event listener functionality
|
||||||
@@ -19,9 +19,9 @@ interface AppInterface {
|
|||||||
* @returns Promise that resolves with a removable listener handle
|
* @returns Promise that resolves with a removable listener handle
|
||||||
*/
|
*/
|
||||||
addListener(
|
addListener(
|
||||||
eventName: "backButton",
|
eventName: 'backButton',
|
||||||
listenerFunc: BackButtonListener,
|
listenerFunc: BackButtonListener
|
||||||
): Promise<PluginListenerHandle> & PluginListenerHandle;
|
): Promise<PluginListenerHandle> & PluginListenerHandle
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add listener for app URL open events
|
* Add listener for app URL open events
|
||||||
@@ -30,9 +30,9 @@ interface AppInterface {
|
|||||||
* @returns Promise that resolves with a removable listener handle
|
* @returns Promise that resolves with a removable listener handle
|
||||||
*/
|
*/
|
||||||
addListener(
|
addListener(
|
||||||
eventName: "appUrlOpen",
|
eventName: 'appUrlOpen',
|
||||||
listenerFunc: (data: AppLaunchUrl) => void,
|
listenerFunc: (data: AppLaunchUrl) => void
|
||||||
): Promise<PluginListenerHandle> & PluginListenerHandle;
|
): Promise<PluginListenerHandle> & PluginListenerHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,19 +41,19 @@ interface AppInterface {
|
|||||||
*/
|
*/
|
||||||
export const App: AppInterface = {
|
export const App: AppInterface = {
|
||||||
addListener(
|
addListener(
|
||||||
eventName: "backButton" | "appUrlOpen",
|
eventName: 'backButton' | 'appUrlOpen',
|
||||||
listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void),
|
listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void)
|
||||||
): Promise<PluginListenerHandle> & PluginListenerHandle {
|
): Promise<PluginListenerHandle> & PluginListenerHandle {
|
||||||
if (eventName === "backButton") {
|
if (eventName === 'backButton') {
|
||||||
return CapacitorApp.addListener(
|
return CapacitorApp.addListener(
|
||||||
eventName,
|
eventName,
|
||||||
listenerFunc as BackButtonListener,
|
listenerFunc as BackButtonListener
|
||||||
) as Promise<PluginListenerHandle> & PluginListenerHandle;
|
) as Promise<PluginListenerHandle> & PluginListenerHandle
|
||||||
} else {
|
} else {
|
||||||
return CapacitorApp.addListener(
|
return CapacitorApp.addListener(
|
||||||
eventName,
|
eventName,
|
||||||
listenerFunc as (data: AppLaunchUrl) => void,
|
listenerFunc as (data: AppLaunchUrl) => void
|
||||||
) as Promise<PluginListenerHandle> & PluginListenerHandle;
|
) as Promise<PluginListenerHandle> & PluginListenerHandle
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
@@ -81,8 +81,8 @@ import {
|
|||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
faXmark,
|
faXmark
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
// Initialize Font Awesome library with all required icons
|
// Initialize Font Awesome library with all required icons
|
||||||
library.add(
|
library.add(
|
||||||
@@ -161,8 +161,8 @@ library.add(
|
|||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
faXmark,
|
faXmark
|
||||||
);
|
)
|
||||||
|
|
||||||
// Export the FontAwesomeIcon component for use in other files
|
// Export the FontAwesomeIcon component for use in other files
|
||||||
export { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
export { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from '@veramo/core'
|
||||||
import { getRandomBytesSync } from "ethereum-cryptography/random";
|
import { getRandomBytesSync } from 'ethereum-cryptography/random'
|
||||||
import { entropyToMnemonic } from "ethereum-cryptography/bip39";
|
import { entropyToMnemonic } from 'ethereum-cryptography/bip39'
|
||||||
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
|
import { wordlist } from 'ethereum-cryptography/bip39/wordlists/english'
|
||||||
import { HDNode } from "@ethersproject/hdnode";
|
import { HDNode } from '@ethersproject/hdnode'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
createEndorserJwtForDid,
|
createEndorserJwtForDid,
|
||||||
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
||||||
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI
|
||||||
} from "../../libs/endorserServer";
|
} from '../../libs/endorserServer'
|
||||||
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
import { DEFAULT_DID_PROVIDER_NAME } from '../veramo/setup'
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from '../../utils/logger'
|
||||||
|
|
||||||
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
|
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'"
|
||||||
|
|
||||||
export const LOCAL_KMS_NAME = "local";
|
export const LOCAL_KMS_NAME = 'local'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -30,10 +30,10 @@ export const newIdentifier = (
|
|||||||
address: string,
|
address: string,
|
||||||
publicHex: string,
|
publicHex: string,
|
||||||
privateHex: string,
|
privateHex: string,
|
||||||
derivationPath: string,
|
derivationPath: string
|
||||||
): Omit<IIdentifier, keyof "provider"> => {
|
): Omit<IIdentifier, keyof 'provider'> => {
|
||||||
return {
|
return {
|
||||||
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
|
did: DEFAULT_DID_PROVIDER_NAME + ':' + address,
|
||||||
keys: [
|
keys: [
|
||||||
{
|
{
|
||||||
kid: publicHex,
|
kid: publicHex,
|
||||||
@@ -41,13 +41,13 @@ export const newIdentifier = (
|
|||||||
meta: { derivationPath: derivationPath },
|
meta: { derivationPath: derivationPath },
|
||||||
privateKeyHex: privateHex,
|
privateKeyHex: privateHex,
|
||||||
publicKeyHex: publicHex,
|
publicKeyHex: publicHex,
|
||||||
type: "Secp256k1",
|
type: 'Secp256k1'
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
provider: DEFAULT_DID_PROVIDER_NAME,
|
provider: DEFAULT_DID_PROVIDER_NAME,
|
||||||
services: [],
|
services: []
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -57,22 +57,22 @@ export const newIdentifier = (
|
|||||||
*/
|
*/
|
||||||
export const deriveAddress = (
|
export const deriveAddress = (
|
||||||
mnemonic: string,
|
mnemonic: string,
|
||||||
derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH,
|
derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH
|
||||||
): [string, string, string, string] => {
|
): [string, string, string, string] => {
|
||||||
mnemonic = mnemonic.trim().toLowerCase();
|
mnemonic = mnemonic.trim().toLowerCase()
|
||||||
|
|
||||||
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
|
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic)
|
||||||
const rootNode: HDNode = hdnode.derivePath(derivationPath);
|
const rootNode: HDNode = hdnode.derivePath(derivationPath)
|
||||||
const privateHex = rootNode.privateKey.substring(2); // original starts with '0x'
|
const privateHex = rootNode.privateKey.substring(2) // original starts with '0x'
|
||||||
const publicHex = rootNode.publicKey.substring(2); // original starts with '0x'
|
const publicHex = rootNode.publicKey.substring(2) // original starts with '0x'
|
||||||
const address = rootNode.address;
|
const address = rootNode.address
|
||||||
|
|
||||||
return [address, privateHex, publicHex, derivationPath];
|
return [address, privateHex, publicHex, derivationPath]
|
||||||
};
|
}
|
||||||
|
|
||||||
export const generateRandomBytes = (numBytes: number): Uint8Array => {
|
export const generateRandomBytes = (numBytes: number): Uint8Array => {
|
||||||
return getRandomBytesSync(numBytes);
|
return getRandomBytesSync(numBytes)
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -80,11 +80,11 @@ export const generateRandomBytes = (numBytes: number): Uint8Array => {
|
|||||||
* @return {*} {string}
|
* @return {*} {string}
|
||||||
*/
|
*/
|
||||||
export const generateSeed = (): string => {
|
export const generateSeed = (): string => {
|
||||||
const entropy: Uint8Array = getRandomBytesSync(32);
|
const entropy: Uint8Array = getRandomBytesSync(32)
|
||||||
const mnemonic = entropyToMnemonic(entropy, wordlist);
|
const mnemonic = entropyToMnemonic(entropy, wordlist)
|
||||||
|
|
||||||
return mnemonic;
|
return mnemonic
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve an access token, or "" if no DID is provided.
|
* Retrieve an access token, or "" if no DID is provided.
|
||||||
@@ -94,14 +94,14 @@ export const generateSeed = (): string => {
|
|||||||
*/
|
*/
|
||||||
export const accessToken = async (did?: string) => {
|
export const accessToken = async (did?: string) => {
|
||||||
if (did) {
|
if (did) {
|
||||||
const nowEpoch = Math.floor(Date.now() / 1000);
|
const nowEpoch = Math.floor(Date.now() / 1000)
|
||||||
const endEpoch = nowEpoch + 60; // add one minute
|
const endEpoch = nowEpoch + 60 // add one minute
|
||||||
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
|
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did }
|
||||||
return createEndorserJwtForDid(did, tokenPayload);
|
return createEndorserJwtForDid(did, tokenPayload)
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract JWT from various URL formats
|
* Extract JWT from various URL formats
|
||||||
@@ -109,204 +109,204 @@ export const accessToken = async (did?: string) => {
|
|||||||
* @return JWT string if found, original text otherwise
|
* @return JWT string if found, original text otherwise
|
||||||
*/
|
*/
|
||||||
export const getContactJwtFromJwtUrl = (jwtUrlText: string) => {
|
export const getContactJwtFromJwtUrl = (jwtUrlText: string) => {
|
||||||
let jwtText = jwtUrlText;
|
let jwtText = jwtUrlText
|
||||||
|
|
||||||
// Handle various URL patterns
|
// Handle various URL patterns
|
||||||
const URL_PATTERNS = [
|
const URL_PATTERNS = [
|
||||||
"/contact/confirm/",
|
'/contact/confirm/',
|
||||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
||||||
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
CONTACT_URL_PATH_ENDORSER_CH_OLD
|
||||||
];
|
]
|
||||||
|
|
||||||
// Try each pattern
|
// Try each pattern
|
||||||
for (const pattern of URL_PATTERNS) {
|
for (const pattern of URL_PATTERNS) {
|
||||||
const patternIndex = jwtText.indexOf(pattern);
|
const patternIndex = jwtText.indexOf(pattern)
|
||||||
if (patternIndex > -1) {
|
if (patternIndex > -1) {
|
||||||
jwtText = jwtText.substring(patternIndex + pattern.length);
|
jwtText = jwtText.substring(patternIndex + pattern.length)
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no patterns matched but text starts with 'ey', assume it's already a JWT
|
// If no patterns matched but text starts with 'ey', assume it's already a JWT
|
||||||
if (jwtText === jwtUrlText && jwtText.startsWith("ey")) {
|
if (jwtText === jwtUrlText && jwtText.startsWith('ey')) {
|
||||||
return jwtText;
|
return jwtText
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any trailing URL parameters or fragments
|
// Clean up any trailing URL parameters or fragments
|
||||||
const endIndex = jwtText.indexOf("?");
|
const endIndex = jwtText.indexOf('?')
|
||||||
if (endIndex > -1) {
|
if (endIndex > -1) {
|
||||||
jwtText = jwtText.substring(0, endIndex);
|
jwtText = jwtText.substring(0, endIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
return jwtText;
|
return jwtText
|
||||||
};
|
}
|
||||||
|
|
||||||
export const nextDerivationPath = (origDerivPath: string) => {
|
export const nextDerivationPath = (origDerivPath: string) => {
|
||||||
let lastStr = origDerivPath.split("/").slice(-1)[0];
|
let lastStr = origDerivPath.split('/').slice(-1)[0]
|
||||||
if (lastStr.endsWith("'")) {
|
if (lastStr.endsWith("'")) {
|
||||||
lastStr = lastStr.slice(0, -1);
|
lastStr = lastStr.slice(0, -1)
|
||||||
}
|
}
|
||||||
const lastNum = parseInt(lastStr, 10);
|
const lastNum = parseInt(lastStr, 10)
|
||||||
const newLastNum = lastNum + 1;
|
const newLastNum = lastNum + 1
|
||||||
const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : "");
|
const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : '')
|
||||||
const newDerivPath = origDerivPath
|
const newDerivPath = origDerivPath
|
||||||
.split("/")
|
.split('/')
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.concat([newLastStr])
|
.concat([newLastStr])
|
||||||
.join("/");
|
.join('/')
|
||||||
return newDerivPath;
|
return newDerivPath
|
||||||
};
|
}
|
||||||
|
|
||||||
// Base64 encoding/decoding utilities for browser
|
// Base64 encoding/decoding utilities for browser
|
||||||
function base64ToArrayBuffer(base64: string): Uint8Array {
|
function base64ToArrayBuffer(base64: string): Uint8Array {
|
||||||
const binaryString = atob(base64);
|
const binaryString = atob(base64)
|
||||||
const bytes = new Uint8Array(binaryString.length);
|
const bytes = new Uint8Array(binaryString.length)
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
bytes[i] = binaryString.charCodeAt(i)
|
||||||
}
|
}
|
||||||
return bytes;
|
return bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||||
const binary = String.fromCharCode(...new Uint8Array(buffer));
|
const binary = String.fromCharCode(...new Uint8Array(buffer))
|
||||||
return btoa(binary);
|
return btoa(binary)
|
||||||
}
|
}
|
||||||
|
|
||||||
const SALT_LENGTH = 16;
|
const SALT_LENGTH = 16
|
||||||
const IV_LENGTH = 12;
|
const IV_LENGTH = 12
|
||||||
const KEY_LENGTH = 256;
|
const KEY_LENGTH = 256
|
||||||
const ITERATIONS = 100000;
|
const ITERATIONS = 100000
|
||||||
|
|
||||||
// Encryption helper function
|
// Encryption helper function
|
||||||
export async function encryptMessage(message: string, password: string) {
|
export async function encryptMessage(message: string, password: string) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder()
|
||||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH))
|
||||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))
|
||||||
|
|
||||||
// Derive key from password using PBKDF2
|
// Derive key from password using PBKDF2
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
"raw",
|
'raw',
|
||||||
encoder.encode(password),
|
encoder.encode(password),
|
||||||
"PBKDF2",
|
'PBKDF2',
|
||||||
false,
|
false,
|
||||||
["deriveBits", "deriveKey"],
|
['deriveBits', 'deriveKey']
|
||||||
);
|
)
|
||||||
|
|
||||||
const key = await crypto.subtle.deriveKey(
|
const key = await crypto.subtle.deriveKey(
|
||||||
{
|
{
|
||||||
name: "PBKDF2",
|
name: 'PBKDF2',
|
||||||
salt,
|
salt,
|
||||||
iterations: ITERATIONS,
|
iterations: ITERATIONS,
|
||||||
hash: "SHA-256",
|
hash: 'SHA-256'
|
||||||
},
|
},
|
||||||
keyMaterial,
|
keyMaterial,
|
||||||
{ name: "AES-GCM", length: KEY_LENGTH },
|
{ name: 'AES-GCM', length: KEY_LENGTH },
|
||||||
false,
|
false,
|
||||||
["encrypt"],
|
['encrypt']
|
||||||
);
|
)
|
||||||
|
|
||||||
// Encrypt the message
|
// Encrypt the message
|
||||||
const encryptedContent = await crypto.subtle.encrypt(
|
const encryptedContent = await crypto.subtle.encrypt(
|
||||||
{
|
{
|
||||||
name: "AES-GCM",
|
name: 'AES-GCM',
|
||||||
iv,
|
iv
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
encoder.encode(message),
|
encoder.encode(message)
|
||||||
);
|
)
|
||||||
|
|
||||||
// Return a JSON structure with base64-encoded components
|
// Return a JSON structure with base64-encoded components
|
||||||
const result = {
|
const result = {
|
||||||
salt: arrayBufferToBase64(salt),
|
salt: arrayBufferToBase64(salt),
|
||||||
iv: arrayBufferToBase64(iv),
|
iv: arrayBufferToBase64(iv),
|
||||||
encrypted: arrayBufferToBase64(encryptedContent),
|
encrypted: arrayBufferToBase64(encryptedContent)
|
||||||
};
|
}
|
||||||
|
|
||||||
return btoa(JSON.stringify(result));
|
return btoa(JSON.stringify(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decryption helper function
|
// Decryption helper function
|
||||||
export async function decryptMessage(encryptedJson: string, password: string) {
|
export async function decryptMessage(encryptedJson: string, password: string) {
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder()
|
||||||
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));
|
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson))
|
||||||
|
|
||||||
// Convert base64 components back to Uint8Arrays
|
// Convert base64 components back to Uint8Arrays
|
||||||
const saltArray = base64ToArrayBuffer(salt);
|
const saltArray = base64ToArrayBuffer(salt)
|
||||||
const ivArray = base64ToArrayBuffer(iv);
|
const ivArray = base64ToArrayBuffer(iv)
|
||||||
const encryptedContent = base64ToArrayBuffer(encrypted);
|
const encryptedContent = base64ToArrayBuffer(encrypted)
|
||||||
|
|
||||||
// Derive the same key using PBKDF2 with the extracted salt
|
// Derive the same key using PBKDF2 with the extracted salt
|
||||||
const keyMaterial = await crypto.subtle.importKey(
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
"raw",
|
'raw',
|
||||||
new TextEncoder().encode(password),
|
new TextEncoder().encode(password),
|
||||||
"PBKDF2",
|
'PBKDF2',
|
||||||
false,
|
false,
|
||||||
["deriveBits", "deriveKey"],
|
['deriveBits', 'deriveKey']
|
||||||
);
|
)
|
||||||
|
|
||||||
const key = await crypto.subtle.deriveKey(
|
const key = await crypto.subtle.deriveKey(
|
||||||
{
|
{
|
||||||
name: "PBKDF2",
|
name: 'PBKDF2',
|
||||||
salt: saltArray,
|
salt: saltArray,
|
||||||
iterations: ITERATIONS,
|
iterations: ITERATIONS,
|
||||||
hash: "SHA-256",
|
hash: 'SHA-256'
|
||||||
},
|
},
|
||||||
keyMaterial,
|
keyMaterial,
|
||||||
{ name: "AES-GCM", length: KEY_LENGTH },
|
{ name: 'AES-GCM', length: KEY_LENGTH },
|
||||||
false,
|
false,
|
||||||
["decrypt"],
|
['decrypt']
|
||||||
);
|
)
|
||||||
|
|
||||||
// Decrypt the content
|
// Decrypt the content
|
||||||
const decryptedContent = await crypto.subtle.decrypt(
|
const decryptedContent = await crypto.subtle.decrypt(
|
||||||
{
|
{
|
||||||
name: "AES-GCM",
|
name: 'AES-GCM',
|
||||||
iv: ivArray,
|
iv: ivArray
|
||||||
},
|
},
|
||||||
key,
|
key,
|
||||||
encryptedContent,
|
encryptedContent
|
||||||
);
|
)
|
||||||
|
|
||||||
// Convert the decrypted content back to a string
|
// Convert the decrypted content back to a string
|
||||||
return decoder.decode(decryptedContent);
|
return decoder.decode(decryptedContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test function to verify encryption/decryption
|
// Test function to verify encryption/decryption
|
||||||
export async function testEncryptionDecryption() {
|
export async function testEncryptionDecryption() {
|
||||||
try {
|
try {
|
||||||
const testMessage = "Hello, this is a test message! 🚀";
|
const testMessage = 'Hello, this is a test message! 🚀'
|
||||||
const testPassword = "myTestPassword123";
|
const testPassword = 'myTestPassword123'
|
||||||
|
|
||||||
logger.log("Original message:", testMessage);
|
logger.log('Original message:', testMessage)
|
||||||
|
|
||||||
// Test encryption
|
// Test encryption
|
||||||
logger.log("Encrypting...");
|
logger.log('Encrypting...')
|
||||||
const encrypted = await encryptMessage(testMessage, testPassword);
|
const encrypted = await encryptMessage(testMessage, testPassword)
|
||||||
logger.log("Encrypted result:", encrypted);
|
logger.log('Encrypted result:', encrypted)
|
||||||
|
|
||||||
// Test decryption
|
// Test decryption
|
||||||
logger.log("Decrypting...");
|
logger.log('Decrypting...')
|
||||||
const decrypted = await decryptMessage(encrypted, testPassword);
|
const decrypted = await decryptMessage(encrypted, testPassword)
|
||||||
logger.log("Decrypted result:", decrypted);
|
logger.log('Decrypted result:', decrypted)
|
||||||
|
|
||||||
// Verify
|
// Verify
|
||||||
const success = testMessage === decrypted;
|
const success = testMessage === decrypted
|
||||||
logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
logger.log('Test ' + (success ? 'PASSED ✅' : 'FAILED ❌'))
|
||||||
logger.log("Messages match:", success);
|
logger.log('Messages match:', success)
|
||||||
|
|
||||||
// Test with wrong password
|
// Test with wrong password
|
||||||
logger.log("\nTesting with wrong password...");
|
logger.log('\nTesting with wrong password...')
|
||||||
try {
|
try {
|
||||||
await decryptMessage(encrypted, "wrongPassword");
|
await decryptMessage(encrypted, 'wrongPassword')
|
||||||
logger.log("Should not reach here");
|
logger.log('Should not reach here')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Correctly failed with wrong password ✅");
|
logger.log('Correctly failed with wrong password ✅')
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Test failed with error:", error);
|
logger.error('Test failed with error:', error)
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,37 +10,37 @@
|
|||||||
* Similar code resides in endorser-ch and image-api
|
* Similar code resides in endorser-ch and image-api
|
||||||
*/
|
*/
|
||||||
export const didEthLocalResolver = async (did: string) => {
|
export const didEthLocalResolver = async (did: string) => {
|
||||||
const didRegex = /^did:ethr:(0x[0-9a-fA-F]{40})$/;
|
const didRegex = /^did:ethr:(0x[0-9a-fA-F]{40})$/
|
||||||
const match = did.match(didRegex);
|
const match = did.match(didRegex)
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const address = match[1]; // Extract eth address: 0x...
|
const address = match[1] // Extract eth address: 0x...
|
||||||
const publicKeyHex = address; // Use the address directly as a public key placeholder
|
const publicKeyHex = address // Use the address directly as a public key placeholder
|
||||||
|
|
||||||
return {
|
return {
|
||||||
didDocumentMetadata: {},
|
didDocumentMetadata: {},
|
||||||
didResolutionMetadata: {
|
didResolutionMetadata: {
|
||||||
contentType: "application/did+ld+json",
|
contentType: 'application/did+ld+json'
|
||||||
},
|
},
|
||||||
didDocument: {
|
didDocument: {
|
||||||
"@context": [
|
'@context': [
|
||||||
"https://www.w3.org/ns/did/v1",
|
'https://www.w3.org/ns/did/v1',
|
||||||
"https://w3id.org/security/suites/secp256k1recovery-2020/v2",
|
'https://w3id.org/security/suites/secp256k1recovery-2020/v2'
|
||||||
],
|
],
|
||||||
id: did,
|
id: did,
|
||||||
verificationMethod: [
|
verificationMethod: [
|
||||||
{
|
{
|
||||||
id: `${did}#controller`,
|
id: `${did}#controller`,
|
||||||
type: "EcdsaSecp256k1RecoveryMethod2020",
|
type: 'EcdsaSecp256k1RecoveryMethod2020',
|
||||||
controller: did,
|
controller: did,
|
||||||
blockchainAccountId: "eip155:1:" + publicKeyHex,
|
blockchainAccountId: 'eip155:1:' + publicKeyHex
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
authentication: [`${did}#controller`],
|
authentication: [`${did}#controller`],
|
||||||
assertionMethod: [`${did}#controller`],
|
assertionMethod: [`${did}#controller`]
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unsupported DID format: ${did}`);
|
throw new Error(`Unsupported DID format: ${did}`)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Buffer } from "buffer/";
|
import { Buffer } from 'buffer/'
|
||||||
import { decode as cborDecode } from "cbor-x";
|
import { decode as cborDecode } from 'cbor-x'
|
||||||
import { bytesToMultibase, multibaseToBytes } from "did-jwt";
|
import { bytesToMultibase, multibaseToBytes } from 'did-jwt'
|
||||||
|
|
||||||
import { getWebCrypto } from "../../../libs/crypto/vc/passkeyHelpers";
|
import { getWebCrypto } from '../../../libs/crypto/vc/passkeyHelpers'
|
||||||
|
|
||||||
export const PEER_DID_PREFIX = "did:peer:";
|
export const PEER_DID_PREFIX = 'did:peer:'
|
||||||
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
|
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + '0'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -17,38 +17,38 @@ const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
|
|||||||
export async function verifyPeerSignature(
|
export async function verifyPeerSignature(
|
||||||
payloadBytes: Buffer,
|
payloadBytes: Buffer,
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
signatureBytes: Uint8Array,
|
signatureBytes: Uint8Array
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid)
|
||||||
|
|
||||||
const WebCrypto = await getWebCrypto();
|
const WebCrypto = await getWebCrypto()
|
||||||
const verifyAlgorithm = {
|
const verifyAlgorithm = {
|
||||||
name: "ECDSA",
|
name: 'ECDSA',
|
||||||
hash: { name: "SHA-256" },
|
hash: { name: 'SHA-256' }
|
||||||
};
|
}
|
||||||
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk;
|
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk
|
||||||
const keyAlgorithm = {
|
const keyAlgorithm = {
|
||||||
name: "ECDSA",
|
name: 'ECDSA',
|
||||||
namedCurve: publicKeyJwk.crv,
|
namedCurve: publicKeyJwk.crv
|
||||||
};
|
}
|
||||||
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
|
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
|
||||||
"jwk",
|
'jwk',
|
||||||
publicKeyJwk,
|
publicKeyJwk,
|
||||||
keyAlgorithm,
|
keyAlgorithm,
|
||||||
false,
|
false,
|
||||||
["verify"],
|
['verify']
|
||||||
);
|
)
|
||||||
const verified = await WebCrypto.subtle.verify(
|
const verified = await WebCrypto.subtle.verify(
|
||||||
verifyAlgorithm,
|
verifyAlgorithm,
|
||||||
publicKeyCryptoKey,
|
publicKeyCryptoKey,
|
||||||
signatureBytes,
|
signatureBytes,
|
||||||
payloadBytes,
|
payloadBytes
|
||||||
);
|
)
|
||||||
return verified;
|
return verified
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cborToKeys(publicKeyBytes: Uint8Array) {
|
export function cborToKeys(publicKeyBytes: Uint8Array) {
|
||||||
const jwkObj = cborDecode(publicKeyBytes);
|
const jwkObj = cborDecode(publicKeyBytes)
|
||||||
if (
|
if (
|
||||||
jwkObj[1] != 2 || // kty "EC"
|
jwkObj[1] != 2 || // kty "EC"
|
||||||
jwkObj[3] != -7 || // alg "ES256"
|
jwkObj[3] != -7 || // alg "ES256"
|
||||||
@@ -56,32 +56,32 @@ export function cborToKeys(publicKeyBytes: Uint8Array) {
|
|||||||
jwkObj[-2].length != 32 || // x
|
jwkObj[-2].length != 32 || // x
|
||||||
jwkObj[-3].length != 32 // y
|
jwkObj[-3].length != 32 // y
|
||||||
) {
|
) {
|
||||||
throw new Error("Unable to extract key.");
|
throw new Error('Unable to extract key.')
|
||||||
}
|
}
|
||||||
const publicKeyJwk = {
|
const publicKeyJwk = {
|
||||||
alg: "ES256",
|
alg: 'ES256',
|
||||||
crv: "P-256",
|
crv: 'P-256',
|
||||||
kty: "EC",
|
kty: 'EC',
|
||||||
x: arrayToBase64Url(jwkObj[-2]),
|
x: arrayToBase64Url(jwkObj[-2]),
|
||||||
y: arrayToBase64Url(jwkObj[-3]),
|
y: arrayToBase64Url(jwkObj[-3])
|
||||||
};
|
}
|
||||||
const publicKeyBuffer = Buffer.concat([
|
const publicKeyBuffer = Buffer.concat([
|
||||||
Buffer.from(jwkObj[-2]),
|
Buffer.from(jwkObj[-2]),
|
||||||
Buffer.from(jwkObj[-3]),
|
Buffer.from(jwkObj[-3])
|
||||||
]);
|
])
|
||||||
return { publicKeyJwk, publicKeyBuffer };
|
return { publicKeyJwk, publicKeyBuffer }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toBase64Url(anythingB64: string) {
|
export function toBase64Url(anythingB64: string) {
|
||||||
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
return anythingB64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function arrayToBase64Url(anything: Uint8Array) {
|
export function arrayToBase64Url(anything: Uint8Array) {
|
||||||
return toBase64Url(Buffer.from(anything).toString("base64"));
|
return toBase64Url(Buffer.from(anything).toString('base64'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function peerDidToPublicKeyBytes(did: string) {
|
export function peerDidToPublicKeyBytes(did: string) {
|
||||||
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length));
|
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPeerDid(publicKeyBytes: Uint8Array) {
|
export function createPeerDid(publicKeyBytes: Uint8Array) {
|
||||||
@@ -89,8 +89,8 @@ export function createPeerDid(publicKeyBytes: Uint8Array) {
|
|||||||
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
|
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
|
||||||
const methodSpecificId = bytesToMultibase(
|
const methodSpecificId = bytesToMultibase(
|
||||||
publicKeyBytes,
|
publicKeyBytes,
|
||||||
"base58btc",
|
'base58btc',
|
||||||
"p256-pub",
|
'p256-pub'
|
||||||
);
|
)
|
||||||
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId;
|
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,21 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Buffer } from "buffer/";
|
import { Buffer } from 'buffer/'
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from 'did-jwt'
|
||||||
import { JWTVerified } from "did-jwt";
|
import { JWTVerified } from 'did-jwt'
|
||||||
import { Resolver } from "did-resolver";
|
import { Resolver } from 'did-resolver'
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from '@veramo/core'
|
||||||
import * as u8a from "uint8arrays";
|
import * as u8a from 'uint8arrays'
|
||||||
|
|
||||||
import { didEthLocalResolver } from "./did-eth-local-resolver";
|
import { didEthLocalResolver } from './did-eth-local-resolver'
|
||||||
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
|
import { PEER_DID_PREFIX, verifyPeerSignature } from './didPeer'
|
||||||
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
|
import { base64urlDecodeString, createDidPeerJwt } from './passkeyDidPeer'
|
||||||
import { urlBase64ToUint8Array } from "./util";
|
import { urlBase64ToUint8Array } from './util'
|
||||||
|
|
||||||
export const ETHR_DID_PREFIX = "did:ethr:";
|
export const ETHR_DID_PREFIX = 'did:ethr:'
|
||||||
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
|
export const JWT_VERIFY_FAILED_CODE = 'JWT_VERIFY_FAILED'
|
||||||
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
|
export const UNSUPPORTED_DID_METHOD_CODE = 'UNSUPPORTED_DID_METHOD'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Meta info about a key
|
* Meta info about a key
|
||||||
@@ -29,51 +29,51 @@ export interface KeyMeta {
|
|||||||
/**
|
/**
|
||||||
* Decentralized ID for the key
|
* Decentralized ID for the key
|
||||||
*/
|
*/
|
||||||
did: string;
|
did: string
|
||||||
/**
|
/**
|
||||||
* Stringified IIDentifier object from Veramo
|
* Stringified IIDentifier object from Veramo
|
||||||
*/
|
*/
|
||||||
identity?: string;
|
identity?: string
|
||||||
/**
|
/**
|
||||||
* The Webauthn credential ID in hex, if this is from a passkey
|
* The Webauthn credential ID in hex, if this is from a passkey
|
||||||
*/
|
*/
|
||||||
passkeyCredIdHex?: string;
|
passkeyCredIdHex?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });
|
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tell whether a key is from a passkey
|
* Tell whether a key is from a passkey
|
||||||
* @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey
|
* @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey
|
||||||
*/
|
*/
|
||||||
export function isFromPasskey(keyMeta?: KeyMeta): boolean {
|
export function isFromPasskey(keyMeta?: KeyMeta): boolean {
|
||||||
return !!keyMeta?.passkeyCredIdHex;
|
return !!keyMeta?.passkeyCredIdHex
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createEndorserJwtForKey(
|
export async function createEndorserJwtForKey(
|
||||||
account: KeyMeta,
|
account: KeyMeta,
|
||||||
payload: object,
|
payload: object,
|
||||||
expiresIn?: number,
|
expiresIn?: number
|
||||||
) {
|
) {
|
||||||
if (account?.identity) {
|
if (account?.identity) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const identity: IIdentifier = JSON.parse(account.identity!);
|
const identity: IIdentifier = JSON.parse(account.identity!)
|
||||||
const privateKeyHex = identity.keys[0].privateKeyHex;
|
const privateKeyHex = identity.keys[0].privateKeyHex
|
||||||
const signer = await SimpleSigner(privateKeyHex as string);
|
const signer = await SimpleSigner(privateKeyHex as string)
|
||||||
const options = {
|
const options = {
|
||||||
// alg: "ES256K", // "K" is the default, "K-R" is used by the server in tests
|
// alg: "ES256K", // "K" is the default, "K-R" is used by the server in tests
|
||||||
issuer: account.did,
|
issuer: account.did,
|
||||||
signer: signer,
|
signer: signer,
|
||||||
expiresIn: undefined as number | undefined,
|
expiresIn: undefined as number | undefined
|
||||||
};
|
|
||||||
if (expiresIn) {
|
|
||||||
options.expiresIn = expiresIn;
|
|
||||||
}
|
}
|
||||||
return didJwt.createJWT(payload, options);
|
if (expiresIn) {
|
||||||
|
options.expiresIn = expiresIn
|
||||||
|
}
|
||||||
|
return didJwt.createJWT(payload, options)
|
||||||
} else if (account?.passkeyCredIdHex) {
|
} else if (account?.passkeyCredIdHex) {
|
||||||
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload);
|
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload)
|
||||||
} else {
|
} else {
|
||||||
throw new Error("No identity data found to sign for DID " + account.did);
|
throw new Error('No identity data found to sign for DID ' + account.did)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,35 +92,35 @@ export async function createEndorserJwtForKey(
|
|||||||
* @return {Function} a configured signer function
|
* @return {Function} a configured signer function
|
||||||
*/
|
*/
|
||||||
function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
|
function SimpleSigner(hexPrivateKey: string): didJwt.Signer {
|
||||||
const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true);
|
const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true)
|
||||||
return async (data) => {
|
return async (data) => {
|
||||||
const signature = (await signer(data)) as string;
|
const signature = (await signer(data)) as string
|
||||||
return fromJose(signature);
|
return fromJose(signature)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// from did-jwt/util; see SimpleSigner above
|
// from did-jwt/util; see SimpleSigner above
|
||||||
function fromJose(signature: string): {
|
function fromJose(signature: string): {
|
||||||
r: string;
|
r: string
|
||||||
s: string;
|
s: string
|
||||||
recoveryParam?: number;
|
recoveryParam?: number
|
||||||
} {
|
} {
|
||||||
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
|
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature)
|
||||||
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`,
|
`Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
const r = bytesToHex(signatureBytes.slice(0, 32));
|
const r = bytesToHex(signatureBytes.slice(0, 32))
|
||||||
const s = bytesToHex(signatureBytes.slice(32, 64));
|
const s = bytesToHex(signatureBytes.slice(32, 64))
|
||||||
const recoveryParam =
|
const recoveryParam =
|
||||||
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
|
signatureBytes.length === 65 ? signatureBytes[64] : undefined
|
||||||
return { r, s, recoveryParam };
|
return { r, s, recoveryParam }
|
||||||
}
|
}
|
||||||
|
|
||||||
// from did-jwt/util; see SimpleSigner above
|
// from did-jwt/util; see SimpleSigner above
|
||||||
function bytesToHex(b: Uint8Array): string {
|
function bytesToHex(b: Uint8Array): string {
|
||||||
return u8a.toString(b, "base16");
|
return u8a.toString(b, 'base16')
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should be calling 'verify' in more places, showing warnings if it fails.
|
// We should be calling 'verify' in more places, showing warnings if it fails.
|
||||||
@@ -128,21 +128,21 @@ function bytesToHex(b: Uint8Array): string {
|
|||||||
export function decodeEndorserJwt(jwt: string) {
|
export function decodeEndorserJwt(jwt: string) {
|
||||||
try {
|
try {
|
||||||
// First try the standard did-jwt decode
|
// First try the standard did-jwt decode
|
||||||
return didJwt.decodeJWT(jwt);
|
return didJwt.decodeJWT(jwt)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If that fails, try manual decoding
|
// If that fails, try manual decoding
|
||||||
try {
|
try {
|
||||||
const parts = jwt.split(".");
|
const parts = jwt.split('.')
|
||||||
if (parts.length !== 3) {
|
if (parts.length !== 3) {
|
||||||
throw new Error("JWT must have 3 parts");
|
throw new Error('JWT must have 3 parts')
|
||||||
}
|
}
|
||||||
|
|
||||||
const header = JSON.parse(Buffer.from(parts[0], "base64url").toString());
|
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString())
|
||||||
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString())
|
||||||
|
|
||||||
// Validate the header
|
// Validate the header
|
||||||
if (header.typ !== "JWT" || !header.alg) {
|
if (header.typ !== 'JWT' || !header.alg) {
|
||||||
throw new Error("Invalid JWT header format");
|
throw new Error('Invalid JWT header format')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return in the same format as didJwt.decodeJWT
|
// Return in the same format as didJwt.decodeJWT
|
||||||
@@ -150,10 +150,10 @@ export function decodeEndorserJwt(jwt: string) {
|
|||||||
header,
|
header,
|
||||||
payload,
|
payload,
|
||||||
signature: parts[2],
|
signature: parts[2],
|
||||||
data: parts[0] + "." + parts[1],
|
data: parts[0] + '.' + parts[1]
|
||||||
};
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(`invalid_argument: Incorrect format JWT - ${e.message}`);
|
throw new Error(`invalid_argument: Incorrect format JWT - ${e.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,70 +161,70 @@ export function decodeEndorserJwt(jwt: string) {
|
|||||||
// return Promise of at least { issuer, payload, verified boolean }
|
// return Promise of at least { issuer, payload, verified boolean }
|
||||||
// ... and also if successfully verified by did-jwt (not JWANT): data, doc, signature, signer
|
// ... and also if successfully verified by did-jwt (not JWANT): data, doc, signature, signer
|
||||||
export async function decodeAndVerifyJwt(
|
export async function decodeAndVerifyJwt(
|
||||||
jwt: string,
|
jwt: string
|
||||||
): Promise<Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt">> {
|
): Promise<Omit<JWTVerified, 'didResolutionResult' | 'signer' | 'jwt'>> {
|
||||||
const pieces = jwt.split(".");
|
const pieces = jwt.split('.')
|
||||||
const header = JSON.parse(base64urlDecodeString(pieces[0]));
|
const header = JSON.parse(base64urlDecodeString(pieces[0]))
|
||||||
const payload = JSON.parse(base64urlDecodeString(pieces[1]));
|
const payload = JSON.parse(base64urlDecodeString(pieces[1]))
|
||||||
const issuerDid = payload.iss;
|
const issuerDid = payload.iss
|
||||||
if (!issuerDid) {
|
if (!issuerDid) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
clientError: {
|
clientError: {
|
||||||
message: `Missing "iss" field in JWT.`,
|
message: `Missing "iss" field in JWT.`
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
|
if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
|
||||||
try {
|
try {
|
||||||
const verified = await didJwt.verifyJWT(jwt, {
|
const verified = await didJwt.verifyJWT(jwt, {
|
||||||
resolver: ethLocalResolver,
|
resolver: ethLocalResolver
|
||||||
});
|
})
|
||||||
return verified;
|
return verified
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
clientError: {
|
clientError: {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
message: `JWT failed verification: ` + e.toString(),
|
message: `JWT failed verification: ` + e.toString(),
|
||||||
code: JWT_VERIFY_FAILED_CODE,
|
code: JWT_VERIFY_FAILED_CODE
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (issuerDid.startsWith(PEER_DID_PREFIX) && header.typ === "JWANT") {
|
if (issuerDid.startsWith(PEER_DID_PREFIX) && header.typ === 'JWANT') {
|
||||||
const verified = await verifyPeerSignature(
|
const verified = await verifyPeerSignature(
|
||||||
Buffer.from(payload),
|
Buffer.from(payload),
|
||||||
issuerDid,
|
issuerDid,
|
||||||
urlBase64ToUint8Array(pieces[2]),
|
urlBase64ToUint8Array(pieces[2])
|
||||||
);
|
)
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
clientError: {
|
clientError: {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
message: `JWT failed verification: ` + e.toString(),
|
message: `JWT failed verification: ` + e.toString(),
|
||||||
code: JWT_VERIFY_FAILED_CODE,
|
code: JWT_VERIFY_FAILED_CODE
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
return { issuer: issuerDid, payload: payload, verified: true };
|
return { issuer: issuerDid, payload: payload, verified: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (issuerDid.startsWith(PEER_DID_PREFIX)) {
|
if (issuerDid.startsWith(PEER_DID_PREFIX)) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
clientError: {
|
clientError: {
|
||||||
message: `JWT with a PEER DID currently only supported with typ == JWANT. Contact us us for JWT suport since it should be straightforward.`,
|
message: `JWT with a PEER DID currently only supported with typ == JWANT. Contact us us for JWT suport since it should be straightforward.`
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
clientError: {
|
clientError: {
|
||||||
message: `Unsupported DID method ${issuerDid}`,
|
message: `Unsupported DID method ${issuerDid}`,
|
||||||
code: UNSUPPORTED_DID_METHOD_CODE,
|
code: UNSUPPORTED_DID_METHOD_CODE
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,36 @@
|
|||||||
import { Buffer } from "buffer/";
|
import { Buffer } from 'buffer/'
|
||||||
import { JWTPayload } from "did-jwt";
|
import { JWTPayload } from 'did-jwt'
|
||||||
import { DIDResolutionResult } from "did-resolver";
|
import { DIDResolutionResult } from 'did-resolver'
|
||||||
import { sha256 } from "ethereum-cryptography/sha256.js";
|
import { sha256 } from 'ethereum-cryptography/sha256.js'
|
||||||
import {
|
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
|
||||||
startAuthentication,
|
|
||||||
startRegistration,
|
|
||||||
} from "@simplewebauthn/browser";
|
|
||||||
import {
|
import {
|
||||||
generateAuthenticationOptions,
|
generateAuthenticationOptions,
|
||||||
generateRegistrationOptions,
|
generateRegistrationOptions,
|
||||||
verifyAuthenticationResponse,
|
verifyAuthenticationResponse,
|
||||||
verifyRegistrationResponse,
|
verifyRegistrationResponse
|
||||||
} from "@simplewebauthn/server";
|
} from '@simplewebauthn/server'
|
||||||
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse";
|
import { VerifyAuthenticationResponseOpts } from '@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse'
|
||||||
import {
|
import {
|
||||||
Base64URLString,
|
Base64URLString,
|
||||||
PublicKeyCredentialCreationOptionsJSON,
|
PublicKeyCredentialCreationOptionsJSON,
|
||||||
PublicKeyCredentialRequestOptionsJSON,
|
PublicKeyCredentialRequestOptionsJSON
|
||||||
} from "@simplewebauthn/types";
|
} from '@simplewebauthn/types'
|
||||||
|
|
||||||
import { AppString } from "../../../constants/app";
|
import { AppString } from '../../../constants/app'
|
||||||
import { unwrapEC2Signature } from "../../../libs/crypto/vc/passkeyHelpers";
|
import { unwrapEC2Signature } from '../../../libs/crypto/vc/passkeyHelpers'
|
||||||
import {
|
import {
|
||||||
arrayToBase64Url,
|
arrayToBase64Url,
|
||||||
cborToKeys,
|
cborToKeys,
|
||||||
peerDidToPublicKeyBytes,
|
peerDidToPublicKeyBytes,
|
||||||
verifyPeerSignature,
|
verifyPeerSignature
|
||||||
} from "../../../libs/crypto/vc/didPeer";
|
} from '../../../libs/crypto/vc/didPeer'
|
||||||
import { logger } from "../../../utils/logger";
|
import { logger } from '../../../utils/logger'
|
||||||
|
|
||||||
export interface JWK {
|
export interface JWK {
|
||||||
kty: string;
|
kty: string
|
||||||
crv: string;
|
crv: string
|
||||||
x: string;
|
x: string
|
||||||
y: string;
|
y: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerCredential(passkeyName?: string) {
|
export async function registerCredential(passkeyName?: string) {
|
||||||
@@ -41,203 +38,203 @@ export async function registerCredential(passkeyName?: string) {
|
|||||||
await generateRegistrationOptions({
|
await generateRegistrationOptions({
|
||||||
rpName: AppString.APP_NAME,
|
rpName: AppString.APP_NAME,
|
||||||
rpID: window.location.hostname,
|
rpID: window.location.hostname,
|
||||||
userName: passkeyName || AppString.APP_NAME + " User",
|
userName: passkeyName || AppString.APP_NAME + ' User',
|
||||||
// Don't prompt users for additional information about the authenticator
|
// Don't prompt users for additional information about the authenticator
|
||||||
// (Recommended for smoother UX)
|
// (Recommended for smoother UX)
|
||||||
attestationType: "none",
|
attestationType: 'none',
|
||||||
authenticatorSelection: {
|
authenticatorSelection: {
|
||||||
// Defaults
|
// Defaults
|
||||||
residentKey: "preferred",
|
residentKey: 'preferred',
|
||||||
userVerification: "preferred",
|
userVerification: 'preferred',
|
||||||
// Optional
|
// Optional
|
||||||
authenticatorAttachment: "platform",
|
authenticatorAttachment: 'platform'
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
// someday, instead of simplwebauthn, we'll go direct: navigator.credentials.create with PublicKeyCredentialCreationOptions
|
// someday, instead of simplwebauthn, we'll go direct: navigator.credentials.create with PublicKeyCredentialCreationOptions
|
||||||
// with pubKeyCredParams: { type: "public-key", alg: -7 }
|
// with pubKeyCredParams: { type: "public-key", alg: -7 }
|
||||||
const attResp = await startRegistration(options);
|
const attResp = await startRegistration(options)
|
||||||
const verification = await verifyRegistrationResponse({
|
const verification = await verifyRegistrationResponse({
|
||||||
response: attResp,
|
response: attResp,
|
||||||
expectedChallenge: options.challenge,
|
expectedChallenge: options.challenge,
|
||||||
expectedOrigin: window.location.origin,
|
expectedOrigin: window.location.origin,
|
||||||
expectedRPID: window.location.hostname,
|
expectedRPID: window.location.hostname
|
||||||
});
|
})
|
||||||
|
|
||||||
// references for parsing auth data and getting the public key
|
// references for parsing auth data and getting the public key
|
||||||
// https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11
|
// https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11
|
||||||
// https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
|
// https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
|
||||||
// https://chatgpt.com/share/3c13f061-6031-45bc-a2d7-3347c1e7a2d7
|
// https://chatgpt.com/share/3c13f061-6031-45bc-a2d7-3347c1e7a2d7
|
||||||
|
|
||||||
const credIdBase64Url = verification.registrationInfo?.credentialID as string;
|
const credIdBase64Url = verification.registrationInfo?.credentialID as string
|
||||||
if (attResp.rawId !== credIdBase64Url) {
|
if (attResp.rawId !== credIdBase64Url) {
|
||||||
logger.warn("Warning! The raw ID does not match the credential ID.");
|
logger.warn('Warning! The raw ID does not match the credential ID.')
|
||||||
}
|
}
|
||||||
const credIdHex = Buffer.from(
|
const credIdHex = Buffer.from(
|
||||||
base64URLStringToArrayBuffer(credIdBase64Url),
|
base64URLStringToArrayBuffer(credIdBase64Url)
|
||||||
).toString("hex");
|
).toString('hex')
|
||||||
const { publicKeyJwk } = cborToKeys(
|
const { publicKeyJwk } = cborToKeys(
|
||||||
verification.registrationInfo?.credentialPublicKey as Uint8Array,
|
verification.registrationInfo?.credentialPublicKey as Uint8Array
|
||||||
);
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authData: verification.registrationInfo?.attestationObject,
|
authData: verification.registrationInfo?.attestationObject,
|
||||||
credIdHex: credIdHex,
|
credIdHex: credIdHex,
|
||||||
publicKeyJwk: publicKeyJwk,
|
publicKeyJwk: publicKeyJwk,
|
||||||
publicKeyBytes: verification.registrationInfo
|
publicKeyBytes: verification.registrationInfo
|
||||||
?.credentialPublicKey as Uint8Array,
|
?.credentialPublicKey as Uint8Array
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PeerSetup {
|
export class PeerSetup {
|
||||||
public authenticatorData?: ArrayBuffer;
|
public authenticatorData?: ArrayBuffer
|
||||||
public challenge?: Uint8Array;
|
public challenge?: Uint8Array
|
||||||
public clientDataJsonBase64Url?: Base64URLString;
|
public clientDataJsonBase64Url?: Base64URLString
|
||||||
public signature?: Base64URLString;
|
public signature?: Base64URLString
|
||||||
|
|
||||||
public async createJwtSimplewebauthn(
|
public async createJwtSimplewebauthn(
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
payload: object,
|
payload: object,
|
||||||
credIdHex: string,
|
credIdHex: string,
|
||||||
expMinutes: number = 1,
|
expMinutes: number = 1
|
||||||
) {
|
) {
|
||||||
const credentialId = arrayBufferToBase64URLString(
|
const credentialId = arrayBufferToBase64URLString(
|
||||||
Buffer.from(credIdHex, "hex").buffer,
|
Buffer.from(credIdHex, 'hex').buffer
|
||||||
);
|
)
|
||||||
const issuedAt = Math.floor(Date.now() / 1000);
|
const issuedAt = Math.floor(Date.now() / 1000)
|
||||||
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
|
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60 // some minutes from now
|
||||||
const fullPayload = {
|
const fullPayload = {
|
||||||
...payload,
|
...payload,
|
||||||
exp: expiryTime,
|
exp: expiryTime,
|
||||||
iat: issuedAt,
|
iat: issuedAt,
|
||||||
iss: issuerDid,
|
iss: issuerDid
|
||||||
};
|
}
|
||||||
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
|
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)))
|
||||||
// const payloadHash: Uint8Array = sha256(this.challenge);
|
// const payloadHash: Uint8Array = sha256(this.challenge);
|
||||||
const options: PublicKeyCredentialRequestOptionsJSON =
|
const options: PublicKeyCredentialRequestOptionsJSON =
|
||||||
await generateAuthenticationOptions({
|
await generateAuthenticationOptions({
|
||||||
challenge: this.challenge,
|
challenge: this.challenge,
|
||||||
rpID: window.location.hostname,
|
rpID: window.location.hostname,
|
||||||
allowCredentials: [{ id: credentialId }],
|
allowCredentials: [{ id: credentialId }]
|
||||||
});
|
})
|
||||||
// console.log("simple authentication options", options);
|
// console.log("simple authentication options", options);
|
||||||
|
|
||||||
const clientAuth = await startAuthentication(options);
|
const clientAuth = await startAuthentication(options)
|
||||||
// console.log("simple credential get", clientAuth);
|
// console.log("simple credential get", clientAuth);
|
||||||
|
|
||||||
const authenticatorDataBase64Url = clientAuth.response.authenticatorData;
|
const authenticatorDataBase64Url = clientAuth.response.authenticatorData
|
||||||
this.authenticatorData = Buffer.from(
|
this.authenticatorData = Buffer.from(
|
||||||
clientAuth.response.authenticatorData,
|
clientAuth.response.authenticatorData,
|
||||||
"base64",
|
'base64'
|
||||||
).buffer;
|
).buffer
|
||||||
this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON;
|
this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON
|
||||||
// console.log("simple authenticatorData for signing", this.authenticatorData);
|
// console.log("simple authenticatorData for signing", this.authenticatorData);
|
||||||
this.signature = clientAuth.response.signature;
|
this.signature = clientAuth.response.signature
|
||||||
|
|
||||||
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
||||||
const header: JWTPayload = { typ: "JWANT", alg: "ES256" };
|
const header: JWTPayload = { typ: 'JWANT', alg: 'ES256' }
|
||||||
const headerBase64 = Buffer.from(JSON.stringify(header))
|
const headerBase64 = Buffer.from(JSON.stringify(header))
|
||||||
.toString("base64")
|
.toString('base64')
|
||||||
.replace(/\+/g, "-")
|
.replace(/\+/g, '-')
|
||||||
.replace(/\//g, "_")
|
.replace(/\//g, '_')
|
||||||
.replace(/=+$/, "");
|
.replace(/=+$/, '')
|
||||||
|
|
||||||
const dataInJwt = {
|
const dataInJwt = {
|
||||||
AuthenticationDataB64URL: authenticatorDataBase64Url,
|
AuthenticationDataB64URL: authenticatorDataBase64Url,
|
||||||
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
|
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
|
||||||
exp: expiryTime,
|
exp: expiryTime,
|
||||||
iat: issuedAt,
|
iat: issuedAt,
|
||||||
iss: issuerDid,
|
iss: issuerDid
|
||||||
};
|
}
|
||||||
const dataInJwtString = JSON.stringify(dataInJwt);
|
const dataInJwtString = JSON.stringify(dataInJwt)
|
||||||
const payloadBase64 = Buffer.from(dataInJwtString)
|
const payloadBase64 = Buffer.from(dataInJwtString)
|
||||||
.toString("base64")
|
.toString('base64')
|
||||||
.replace(/\+/g, "-")
|
.replace(/\+/g, '-')
|
||||||
.replace(/\//g, "_")
|
.replace(/\//g, '_')
|
||||||
.replace(/=+$/, "");
|
.replace(/=+$/, '')
|
||||||
|
|
||||||
const signature = clientAuth.response.signature;
|
const signature = clientAuth.response.signature
|
||||||
|
|
||||||
return headerBase64 + "." + payloadBase64 + "." + signature;
|
return headerBase64 + '.' + payloadBase64 + '.' + signature
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createJwtNavigator(
|
public async createJwtNavigator(
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
payload: object,
|
payload: object,
|
||||||
credIdHex: string,
|
credIdHex: string,
|
||||||
expMinutes: number = 1,
|
expMinutes: number = 1
|
||||||
) {
|
) {
|
||||||
const issuedAt = Math.floor(Date.now() / 1000);
|
const issuedAt = Math.floor(Date.now() / 1000)
|
||||||
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
|
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60 // some minutes from now
|
||||||
const fullPayload = {
|
const fullPayload = {
|
||||||
...payload,
|
...payload,
|
||||||
exp: expiryTime,
|
exp: expiryTime,
|
||||||
iat: issuedAt,
|
iat: issuedAt,
|
||||||
iss: issuerDid,
|
iss: issuerDid
|
||||||
};
|
}
|
||||||
const dataToSignString = JSON.stringify(fullPayload);
|
const dataToSignString = JSON.stringify(fullPayload)
|
||||||
const dataToSignBuffer = Buffer.from(dataToSignString);
|
const dataToSignBuffer = Buffer.from(dataToSignString)
|
||||||
const credentialId = Buffer.from(credIdHex, "hex");
|
const credentialId = Buffer.from(credIdHex, 'hex')
|
||||||
|
|
||||||
// console.log("lower credentialId", credentialId);
|
// console.log("lower credentialId", credentialId);
|
||||||
this.challenge = new Uint8Array(dataToSignBuffer);
|
this.challenge = new Uint8Array(dataToSignBuffer)
|
||||||
const options = {
|
const options = {
|
||||||
publicKey: {
|
publicKey: {
|
||||||
allowCredentials: [
|
allowCredentials: [
|
||||||
{
|
{
|
||||||
id: credentialId,
|
id: credentialId,
|
||||||
type: "public-key" as const,
|
type: 'public-key' as const
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
challenge: this.challenge.buffer,
|
challenge: this.challenge.buffer,
|
||||||
rpID: window.location.hostname,
|
rpID: window.location.hostname,
|
||||||
userVerification: "preferred" as const,
|
userVerification: 'preferred' as const
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const credential = await navigator.credentials.get(options);
|
const credential = await navigator.credentials.get(options)
|
||||||
// console.log("nav credential get", credential);
|
// console.log("nav credential get", credential);
|
||||||
|
|
||||||
this.authenticatorData = credential?.response.authenticatorData;
|
this.authenticatorData = credential?.response.authenticatorData
|
||||||
const authenticatorDataBase64Url = arrayBufferToBase64URLString(
|
const authenticatorDataBase64Url = arrayBufferToBase64URLString(
|
||||||
this.authenticatorData as ArrayBuffer,
|
this.authenticatorData as ArrayBuffer
|
||||||
);
|
)
|
||||||
|
|
||||||
this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
|
this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
|
||||||
credential?.response.clientDataJSON,
|
credential?.response.clientDataJSON
|
||||||
);
|
)
|
||||||
|
|
||||||
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
||||||
const header: JWTPayload = { typ: "JWANT", alg: "ES256" };
|
const header: JWTPayload = { typ: 'JWANT', alg: 'ES256' }
|
||||||
const headerBase64 = Buffer.from(JSON.stringify(header))
|
const headerBase64 = Buffer.from(JSON.stringify(header))
|
||||||
.toString("base64")
|
.toString('base64')
|
||||||
.replace(/\+/g, "-")
|
.replace(/\+/g, '-')
|
||||||
.replace(/\//g, "_")
|
.replace(/\//g, '_')
|
||||||
.replace(/=+$/, "");
|
.replace(/=+$/, '')
|
||||||
|
|
||||||
const dataInJwt = {
|
const dataInJwt = {
|
||||||
AuthenticationDataB64URL: authenticatorDataBase64Url,
|
AuthenticationDataB64URL: authenticatorDataBase64Url,
|
||||||
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
|
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
|
||||||
exp: expiryTime,
|
exp: expiryTime,
|
||||||
iat: issuedAt,
|
iat: issuedAt,
|
||||||
iss: issuerDid,
|
iss: issuerDid
|
||||||
};
|
}
|
||||||
const dataInJwtString = JSON.stringify(dataInJwt);
|
const dataInJwtString = JSON.stringify(dataInJwt)
|
||||||
const payloadBase64 = Buffer.from(dataInJwtString)
|
const payloadBase64 = Buffer.from(dataInJwtString)
|
||||||
.toString("base64")
|
.toString('base64')
|
||||||
.replace(/\+/g, "-")
|
.replace(/\+/g, '-')
|
||||||
.replace(/\//g, "_")
|
.replace(/\//g, '_')
|
||||||
.replace(/=+$/, "");
|
.replace(/=+$/, '')
|
||||||
|
|
||||||
const origSignature = Buffer.from(credential?.response.signature).toString(
|
const origSignature = Buffer.from(credential?.response.signature).toString(
|
||||||
"base64",
|
'base64'
|
||||||
);
|
)
|
||||||
this.signature = origSignature
|
this.signature = origSignature
|
||||||
.replace(/\+/g, "-")
|
.replace(/\+/g, '-')
|
||||||
.replace(/\//g, "_")
|
.replace(/\//g, '_')
|
||||||
.replace(/=+$/, "");
|
.replace(/=+$/, '')
|
||||||
|
|
||||||
const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature;
|
const jwt = headerBase64 + '.' + payloadBase64 + '.' + this.signature
|
||||||
return jwt;
|
return jwt
|
||||||
}
|
}
|
||||||
|
|
||||||
// To use this, add the asn1-ber library and add this import:
|
// To use this, add the asn1-ber library and add this import:
|
||||||
@@ -302,11 +299,11 @@ export class PeerSetup {
|
|||||||
export async function createDidPeerJwt(
|
export async function createDidPeerJwt(
|
||||||
did: string,
|
did: string,
|
||||||
credIdHex: string,
|
credIdHex: string,
|
||||||
payload: object,
|
payload: object
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const peerSetup = new PeerSetup();
|
const peerSetup = new PeerSetup()
|
||||||
const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex);
|
const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex)
|
||||||
return jwt;
|
return jwt
|
||||||
}
|
}
|
||||||
|
|
||||||
// I'd love to use this but it doesn't verify.
|
// I'd love to use this but it doesn't verify.
|
||||||
@@ -320,26 +317,26 @@ export async function verifyJwtP256(
|
|||||||
authenticatorData: ArrayBuffer,
|
authenticatorData: ArrayBuffer,
|
||||||
challenge: Uint8Array,
|
challenge: Uint8Array,
|
||||||
clientDataJsonBase64Url: Base64URLString,
|
clientDataJsonBase64Url: Base64URLString,
|
||||||
signature: Base64URLString,
|
signature: Base64URLString
|
||||||
) {
|
) {
|
||||||
const authDataFromBase = Buffer.from(authenticatorData);
|
const authDataFromBase = Buffer.from(authenticatorData)
|
||||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, 'base64')
|
||||||
const sigBuffer = Buffer.from(signature, "base64");
|
const sigBuffer = Buffer.from(signature, 'base64')
|
||||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
const finalSigBuffer = unwrapEC2Signature(sigBuffer)
|
||||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid)
|
||||||
|
|
||||||
// Hash the client data
|
// Hash the client data
|
||||||
const hash = sha256(clientDataFromBase);
|
const hash = sha256(clientDataFromBase)
|
||||||
|
|
||||||
// Construct the preimage
|
// Construct the preimage
|
||||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
const preimage = Buffer.concat([authDataFromBase, hash])
|
||||||
|
|
||||||
const isValid = p256.verify(
|
const isValid = p256.verify(
|
||||||
finalSigBuffer,
|
finalSigBuffer,
|
||||||
new Uint8Array(preimage),
|
new Uint8Array(preimage),
|
||||||
publicKeyBytes,
|
publicKeyBytes
|
||||||
);
|
)
|
||||||
return isValid;
|
return isValid
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function verifyJwtSimplewebauthn(
|
export async function verifyJwtSimplewebauthn(
|
||||||
@@ -348,37 +345,37 @@ export async function verifyJwtSimplewebauthn(
|
|||||||
authenticatorData: ArrayBuffer,
|
authenticatorData: ArrayBuffer,
|
||||||
challenge: Uint8Array,
|
challenge: Uint8Array,
|
||||||
clientDataJsonBase64Url: Base64URLString,
|
clientDataJsonBase64Url: Base64URLString,
|
||||||
signature: Base64URLString,
|
signature: Base64URLString
|
||||||
) {
|
) {
|
||||||
const authData = arrayToBase64Url(Buffer.from(authenticatorData));
|
const authData = arrayToBase64Url(Buffer.from(authenticatorData))
|
||||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid)
|
||||||
const credId = arrayBufferToBase64URLString(
|
const credId = arrayBufferToBase64URLString(
|
||||||
Buffer.from(credIdHex, "hex").buffer,
|
Buffer.from(credIdHex, 'hex').buffer
|
||||||
);
|
)
|
||||||
const authOpts: VerifyAuthenticationResponseOpts = {
|
const authOpts: VerifyAuthenticationResponseOpts = {
|
||||||
authenticator: {
|
authenticator: {
|
||||||
credentialID: credId,
|
credentialID: credId,
|
||||||
credentialPublicKey: publicKeyBytes,
|
credentialPublicKey: publicKeyBytes,
|
||||||
counter: 0,
|
counter: 0
|
||||||
},
|
},
|
||||||
expectedChallenge: arrayToBase64Url(challenge),
|
expectedChallenge: arrayToBase64Url(challenge),
|
||||||
expectedOrigin: window.location.origin,
|
expectedOrigin: window.location.origin,
|
||||||
expectedRPID: window.location.hostname,
|
expectedRPID: window.location.hostname,
|
||||||
response: {
|
response: {
|
||||||
authenticatorAttachment: "platform",
|
authenticatorAttachment: 'platform',
|
||||||
clientExtensionResults: {},
|
clientExtensionResults: {},
|
||||||
id: credId,
|
id: credId,
|
||||||
rawId: credId,
|
rawId: credId,
|
||||||
response: {
|
response: {
|
||||||
authenticatorData: authData,
|
authenticatorData: authData,
|
||||||
clientDataJSON: clientDataJsonBase64Url,
|
clientDataJSON: clientDataJsonBase64Url,
|
||||||
signature: signature,
|
signature: signature
|
||||||
},
|
},
|
||||||
type: "public-key",
|
type: 'public-key'
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
const verification = await verifyAuthenticationResponse(authOpts);
|
const verification = await verifyAuthenticationResponse(authOpts)
|
||||||
return verification.verified;
|
return verification.verified
|
||||||
}
|
}
|
||||||
|
|
||||||
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
|
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
|
||||||
@@ -388,27 +385,27 @@ export async function verifyJwtWebCrypto(
|
|||||||
authenticatorData: ArrayBuffer,
|
authenticatorData: ArrayBuffer,
|
||||||
challenge: Uint8Array,
|
challenge: Uint8Array,
|
||||||
clientDataJsonBase64Url: Base64URLString,
|
clientDataJsonBase64Url: Base64URLString,
|
||||||
signature: Base64URLString,
|
signature: Base64URLString
|
||||||
) {
|
) {
|
||||||
const authDataFromBase = Buffer.from(authenticatorData);
|
const authDataFromBase = Buffer.from(authenticatorData)
|
||||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, 'base64')
|
||||||
const sigBuffer = Buffer.from(signature, "base64");
|
const sigBuffer = Buffer.from(signature, 'base64')
|
||||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
const finalSigBuffer = unwrapEC2Signature(sigBuffer)
|
||||||
|
|
||||||
// Hash the client data
|
// Hash the client data
|
||||||
const hash = sha256(clientDataFromBase);
|
const hash = sha256(clientDataFromBase)
|
||||||
|
|
||||||
// Construct the preimage
|
// Construct the preimage
|
||||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
const preimage = Buffer.concat([authDataFromBase, hash])
|
||||||
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
|
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
||||||
if (!did.startsWith("did:peer:0z")) {
|
if (!did.startsWith('did:peer:0z')) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"This only verifies a peer DID, method 0, encoded base58btc.",
|
'This only verifies a peer DID, method 0, encoded base58btc.'
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
// this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
|
// this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
|
||||||
// (another reference is the @aviarytech/did-peer resolver)
|
// (another reference is the @aviarytech/did-peer resolver)
|
||||||
@@ -420,131 +417,131 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
|||||||
* - change type to JsonWebKey2020
|
* - change type to JsonWebKey2020
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const id = did.split(":")[2];
|
const id = did.split(':')[2]
|
||||||
const multibase = id.slice(1);
|
const multibase = id.slice(1)
|
||||||
const encnumbasis = multibase.slice(1);
|
const encnumbasis = multibase.slice(1)
|
||||||
const didDocument = {
|
const didDocument = {
|
||||||
"@context": [
|
'@context': [
|
||||||
"https://www.w3.org/ns/did/v1",
|
'https://www.w3.org/ns/did/v1',
|
||||||
"https://w3id.org/security/suites/secp256k1-2019/v1",
|
'https://w3id.org/security/suites/secp256k1-2019/v1'
|
||||||
],
|
],
|
||||||
assertionMethod: [did + "#" + encnumbasis],
|
assertionMethod: [did + '#' + encnumbasis],
|
||||||
authentication: [did + "#" + encnumbasis],
|
authentication: [did + '#' + encnumbasis],
|
||||||
capabilityDelegation: [did + "#" + encnumbasis],
|
capabilityDelegation: [did + '#' + encnumbasis],
|
||||||
capabilityInvocation: [did + "#" + encnumbasis],
|
capabilityInvocation: [did + '#' + encnumbasis],
|
||||||
id: did,
|
id: did,
|
||||||
keyAgreement: undefined,
|
keyAgreement: undefined,
|
||||||
service: undefined,
|
service: undefined,
|
||||||
verificationMethod: [
|
verificationMethod: [
|
||||||
{
|
{
|
||||||
controller: did,
|
controller: did,
|
||||||
id: did + "#" + encnumbasis,
|
id: did + '#' + encnumbasis,
|
||||||
publicKeyMultibase: multibase,
|
publicKeyMultibase: multibase,
|
||||||
type: "EcdsaSecp256k1VerificationKey2019",
|
type: 'EcdsaSecp256k1VerificationKey2019'
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
};
|
}
|
||||||
return {
|
return {
|
||||||
didDocument,
|
didDocument,
|
||||||
didDocumentMetadata: {},
|
didDocumentMetadata: {},
|
||||||
didResolutionMetadata: { contentType: "application/did+ld+json" },
|
didResolutionMetadata: { contentType: 'application/did+ld+json' }
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert COSE public key to PEM format
|
// convert COSE public key to PEM format
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
function COSEtoPEM(cose: Buffer) {
|
function COSEtoPEM(cose: Buffer) {
|
||||||
// const alg = cose.get(3); // Algorithm
|
// const alg = cose.get(3); // Algorithm
|
||||||
const x = cose[-2]; // x-coordinate
|
const x = cose[-2] // x-coordinate
|
||||||
const y = cose[-3]; // y-coordinate
|
const y = cose[-3] // y-coordinate
|
||||||
|
|
||||||
// Ensure the coordinates are in the correct format
|
// Ensure the coordinates are in the correct format
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error because it complains about the type of x and y
|
// @ts-expect-error because it complains about the type of x and y
|
||||||
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
|
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y])
|
||||||
|
|
||||||
// Convert to PEM format
|
// Convert to PEM format
|
||||||
const pem = `-----BEGIN PUBLIC KEY-----
|
const pem = `-----BEGIN PUBLIC KEY-----
|
||||||
${pubKeyBuffer.toString("base64")}
|
${pubKeyBuffer.toString('base64')}
|
||||||
-----END PUBLIC KEY-----`;
|
-----END PUBLIC KEY-----`
|
||||||
|
|
||||||
return pem;
|
return pem
|
||||||
}
|
}
|
||||||
|
|
||||||
// tried the base64url library but got an error using their Buffer
|
// tried the base64url library but got an error using their Buffer
|
||||||
export function base64urlDecodeString(input: string) {
|
export function base64urlDecodeString(input: string) {
|
||||||
return atob(input.replace(/-/g, "+").replace(/_/g, "/"));
|
return atob(input.replace(/-/g, '+').replace(/_/g, '/'))
|
||||||
}
|
}
|
||||||
|
|
||||||
// tried the base64url library but got an error using their Buffer
|
// tried the base64url library but got an error using their Buffer
|
||||||
export function base64urlEncodeString(input: string) {
|
export function base64urlEncodeString(input: string) {
|
||||||
return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
return btoa(input).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
function base64urlDecodeArrayBuffer(input: string) {
|
function base64urlDecodeArrayBuffer(input: string) {
|
||||||
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
input = input.replace(/-/g, '+').replace(/_/g, '/')
|
||||||
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
|
const pad = input.length % 4 === 0 ? '' : '===='.slice(input.length % 4)
|
||||||
const str = atob(input + pad);
|
const str = atob(input + pad)
|
||||||
const bytes = new Uint8Array(str.length);
|
const bytes = new Uint8Array(str.length)
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (let i = 0; i < str.length; i++) {
|
||||||
bytes[i] = str.charCodeAt(i);
|
bytes[i] = str.charCodeAt(i)
|
||||||
}
|
}
|
||||||
return bytes.buffer;
|
return bytes.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) {
|
function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) {
|
||||||
const str = String.fromCharCode(...new Uint8Array(buffer));
|
const str = String.fromCharCode(...new Uint8Array(buffer))
|
||||||
return base64urlEncodeString(str);
|
return base64urlEncodeString(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// from @simplewebauthn/browser
|
// from @simplewebauthn/browser
|
||||||
function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
|
function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer)
|
||||||
let str = "";
|
let str = ''
|
||||||
for (const charCode of bytes) {
|
for (const charCode of bytes) {
|
||||||
str += String.fromCharCode(charCode);
|
str += String.fromCharCode(charCode)
|
||||||
}
|
}
|
||||||
const base64String = btoa(str);
|
const base64String = btoa(str)
|
||||||
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
// from @simplewebauthn/browser
|
// from @simplewebauthn/browser
|
||||||
function base64URLStringToArrayBuffer(base64URLString: string) {
|
function base64URLStringToArrayBuffer(base64URLString: string) {
|
||||||
const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/");
|
const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/')
|
||||||
const padLength = (4 - (base64.length % 4)) % 4;
|
const padLength = (4 - (base64.length % 4)) % 4
|
||||||
const padded = base64.padEnd(base64.length + padLength, "=");
|
const padded = base64.padEnd(base64.length + padLength, '=')
|
||||||
const binary = atob(padded);
|
const binary = atob(padded)
|
||||||
const buffer = new ArrayBuffer(binary.length);
|
const buffer = new ArrayBuffer(binary.length)
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer)
|
||||||
for (let i = 0; i < binary.length; i++) {
|
for (let i = 0; i < binary.length; i++) {
|
||||||
bytes[i] = binary.charCodeAt(i);
|
bytes[i] = binary.charCodeAt(i)
|
||||||
}
|
}
|
||||||
return buffer;
|
return buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
async function pemToCryptoKey(pem: string) {
|
async function pemToCryptoKey(pem: string) {
|
||||||
const binaryDerString = atob(
|
const binaryDerString = atob(
|
||||||
pem
|
pem
|
||||||
.split("\n")
|
.split('\n')
|
||||||
.filter((x) => !x.includes("-----"))
|
.filter((x) => !x.includes('-----'))
|
||||||
.join(""),
|
.join('')
|
||||||
);
|
)
|
||||||
const binaryDer = new Uint8Array(binaryDerString.length);
|
const binaryDer = new Uint8Array(binaryDerString.length)
|
||||||
for (let i = 0; i < binaryDerString.length; i++) {
|
for (let i = 0; i < binaryDerString.length; i++) {
|
||||||
binaryDer[i] = binaryDerString.charCodeAt(i);
|
binaryDer[i] = binaryDerString.charCodeAt(i)
|
||||||
}
|
}
|
||||||
// console.log("binaryDer", binaryDer.buffer);
|
// console.log("binaryDer", binaryDer.buffer);
|
||||||
return await window.crypto.subtle.importKey(
|
return await window.crypto.subtle.importKey(
|
||||||
"spki",
|
'spki',
|
||||||
binaryDer.buffer,
|
binaryDer.buffer,
|
||||||
{
|
{
|
||||||
name: "RSASSA-PKCS1-v1_5",
|
name: 'RSASSA-PKCS1-v1_5',
|
||||||
hash: "SHA-256",
|
hash: 'SHA-256'
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
["verify"],
|
['verify']
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts
|
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts
|
||||||
import { AsnParser } from "@peculiar/asn1-schema";
|
import { AsnParser } from '@peculiar/asn1-schema'
|
||||||
import { ECDSASigValue } from "@peculiar/asn1-ecc";
|
import { ECDSASigValue } from '@peculiar/asn1-ecc'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
|
* In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
|
||||||
@@ -8,21 +8,21 @@ import { ECDSASigValue } from "@peculiar/asn1-ecc";
|
|||||||
* See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
|
* See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
|
||||||
*/
|
*/
|
||||||
export function unwrapEC2Signature(signature: Uint8Array): Uint8Array {
|
export function unwrapEC2Signature(signature: Uint8Array): Uint8Array {
|
||||||
const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
|
const parsedSignature = AsnParser.parse(signature, ECDSASigValue)
|
||||||
let rBytes = new Uint8Array(parsedSignature.r);
|
let rBytes = new Uint8Array(parsedSignature.r)
|
||||||
let sBytes = new Uint8Array(parsedSignature.s);
|
let sBytes = new Uint8Array(parsedSignature.s)
|
||||||
|
|
||||||
if (shouldRemoveLeadingZero(rBytes)) {
|
if (shouldRemoveLeadingZero(rBytes)) {
|
||||||
rBytes = rBytes.slice(1);
|
rBytes = rBytes.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldRemoveLeadingZero(sBytes)) {
|
if (shouldRemoveLeadingZero(sBytes)) {
|
||||||
sBytes = sBytes.slice(1);
|
sBytes = sBytes.slice(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalSignature = isoUint8ArrayConcat([rBytes, sBytes]);
|
const finalSignature = isoUint8ArrayConcat([rBytes, sBytes])
|
||||||
|
|
||||||
return finalSignature;
|
return finalSignature
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +33,7 @@ export function unwrapEC2Signature(signature: Uint8Array): Uint8Array {
|
|||||||
* then remove the leading 0x0 byte"
|
* then remove the leading 0x0 byte"
|
||||||
*/
|
*/
|
||||||
function shouldRemoveLeadingZero(bytes: Uint8Array): boolean {
|
function shouldRemoveLeadingZero(bytes: Uint8Array): boolean {
|
||||||
return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0;
|
return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoUint8Array.ts#L49
|
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoUint8Array.ts#L49
|
||||||
@@ -41,21 +41,21 @@ function shouldRemoveLeadingZero(bytes: Uint8Array): boolean {
|
|||||||
* Combine multiple Uint8Arrays into a single Uint8Array
|
* Combine multiple Uint8Arrays into a single Uint8Array
|
||||||
*/
|
*/
|
||||||
export function isoUint8ArrayConcat(arrays: Uint8Array[]): Uint8Array {
|
export function isoUint8ArrayConcat(arrays: Uint8Array[]): Uint8Array {
|
||||||
let pointer = 0;
|
let pointer = 0
|
||||||
const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0);
|
const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0)
|
||||||
|
|
||||||
const toReturn = new Uint8Array(totalLength);
|
const toReturn = new Uint8Array(totalLength)
|
||||||
|
|
||||||
arrays.forEach((arr) => {
|
arrays.forEach((arr) => {
|
||||||
toReturn.set(arr, pointer);
|
toReturn.set(arr, pointer)
|
||||||
pointer += arr.length;
|
pointer += arr.length
|
||||||
});
|
})
|
||||||
|
|
||||||
return toReturn;
|
return toReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts
|
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts
|
||||||
let webCrypto: { subtle: SubtleCrypto } | undefined = undefined;
|
let webCrypto: { subtle: SubtleCrypto } | undefined = undefined
|
||||||
export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> {
|
export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> {
|
||||||
/**
|
/**
|
||||||
* Hello there! If you came here wondering why this method is asynchronous when use of
|
* Hello there! If you came here wondering why this method is asynchronous when use of
|
||||||
@@ -70,29 +70,29 @@ export function getWebCrypto(): Promise<{ subtle: SubtleCrypto }> {
|
|||||||
const toResolve: Promise<{ subtle: SubtleCrypto }> = new Promise(
|
const toResolve: Promise<{ subtle: SubtleCrypto }> = new Promise(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
if (webCrypto) {
|
if (webCrypto) {
|
||||||
return resolve(webCrypto);
|
return resolve(webCrypto)
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
|
* Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
|
||||||
* support (and Node v20+)
|
* support (and Node v20+)
|
||||||
*/
|
*/
|
||||||
const _globalThisCrypto =
|
const _globalThisCrypto =
|
||||||
_getWebCryptoInternals.stubThisGlobalThisCrypto();
|
_getWebCryptoInternals.stubThisGlobalThisCrypto()
|
||||||
if (_globalThisCrypto) {
|
if (_globalThisCrypto) {
|
||||||
webCrypto = _globalThisCrypto;
|
webCrypto = _globalThisCrypto
|
||||||
return resolve(webCrypto);
|
return resolve(webCrypto)
|
||||||
}
|
}
|
||||||
// We tried to access it both in Node and globally, so bail out
|
// We tried to access it both in Node and globally, so bail out
|
||||||
return reject(new MissingWebCrypto());
|
return reject(new MissingWebCrypto())
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
return toResolve;
|
return toResolve
|
||||||
}
|
}
|
||||||
class MissingWebCrypto extends Error {
|
class MissingWebCrypto extends Error {
|
||||||
constructor() {
|
constructor() {
|
||||||
const message = "An instance of the Crypto API could not be located";
|
const message = 'An instance of the Crypto API could not be located'
|
||||||
super(message);
|
super(message)
|
||||||
this.name = "MissingWebCrypto";
|
this.name = 'MissingWebCrypto'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Make it possible to stub return values during testing
|
// Make it possible to stub return values during testing
|
||||||
@@ -100,6 +100,6 @@ const _getWebCryptoInternals = {
|
|||||||
stubThisGlobalThisCrypto: () => globalThis.crypto,
|
stubThisGlobalThisCrypto: () => globalThis.crypto,
|
||||||
// Make it possible to reset the `webCrypto` at the top of the file
|
// Make it possible to reset the `webCrypto` at the top of the file
|
||||||
setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => {
|
setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => {
|
||||||
webCrypto = newCrypto;
|
webCrypto = newCrypto
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
export function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
||||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||||
const rawData = window.atob(base64);
|
const rawData = window.atob(base64)
|
||||||
const outputArray = new Uint8Array(rawData.length);
|
const outputArray = new Uint8Array(rawData.length)
|
||||||
|
|
||||||
for (let i = 0; i < rawData.length; ++i) {
|
for (let i = 0; i < rawData.length; ++i) {
|
||||||
outputArray[i] = rawData.charCodeAt(i);
|
outputArray[i] = rawData.charCodeAt(i)
|
||||||
}
|
}
|
||||||
return outputArray;
|
return outputArray
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
|||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
description: string;
|
description: string
|
||||||
locLat?: number;
|
locLat?: number
|
||||||
locLon?: number;
|
locLon?: number
|
||||||
locLat2?: number;
|
locLat2?: number
|
||||||
locLon2?: number;
|
locLon2?: number
|
||||||
issuerDid: string;
|
issuerDid: string
|
||||||
rowId?: string; // set on profile retrieved from server
|
rowId?: string // set on profile retrieved from server
|
||||||
}
|
}
|
||||||
|
|||||||
510
src/libs/util.ts
510
src/libs/util.ts
@@ -1,50 +1,50 @@
|
|||||||
// many of these are also found in endorser-mobile utility.ts
|
// many of these are also found in endorser-mobile utility.ts
|
||||||
|
|
||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from 'axios'
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from 'buffer'
|
||||||
import * as R from "ramda";
|
import * as R from 'ramda'
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from '@vueuse/core'
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
import { DEFAULT_PUSH_SERVER, NotificationIface } from '../constants/app'
|
||||||
import {
|
import {
|
||||||
accountsDBPromise,
|
accountsDBPromise,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
updateAccountSettings,
|
updateAccountSettings,
|
||||||
updateDefaultSettings,
|
updateDefaultSettings
|
||||||
} from "../db/index";
|
} from '../db/index'
|
||||||
import { Account } from "../db/tables/accounts";
|
import { Account } from '../db/tables/accounts'
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from '../db/tables/contacts'
|
||||||
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from '../db/tables/settings'
|
||||||
import { deriveAddress, generateSeed, newIdentifier } from "../libs/crypto";
|
import { deriveAddress, generateSeed, newIdentifier } from '../libs/crypto'
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from '../libs/endorserServer'
|
||||||
import {
|
import {
|
||||||
containsHiddenDid,
|
containsHiddenDid,
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
GenericVerifiableCredential,
|
GenericVerifiableCredential,
|
||||||
GiveSummaryRecord,
|
GiveSummaryRecord,
|
||||||
OfferVerifiableCredential,
|
OfferVerifiableCredential
|
||||||
} from "../libs/endorserServer";
|
} from '../libs/endorserServer'
|
||||||
import { KeyMeta } from "../libs/crypto/vc";
|
import { KeyMeta } from '../libs/crypto/vc'
|
||||||
import { createPeerDid } from "../libs/crypto/vc/didPeer";
|
import { createPeerDid } from '../libs/crypto/vc/didPeer'
|
||||||
import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer";
|
import { registerCredential } from '../libs/crypto/vc/passkeyDidPeer'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../utils/logger'
|
||||||
|
|
||||||
export interface GiverReceiverInputInfo {
|
export interface GiverReceiverInputInfo {
|
||||||
did?: string;
|
did?: string
|
||||||
name?: string;
|
name?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OnboardPage {
|
export enum OnboardPage {
|
||||||
Home = "HOME",
|
Home = 'HOME',
|
||||||
Discover = "DISCOVER",
|
Discover = 'DISCOVER',
|
||||||
Create = "CREATE",
|
Create = 'CREATE',
|
||||||
Contact = "CONTACT",
|
Contact = 'CONTACT',
|
||||||
Account = "ACCOUNT",
|
Account = 'ACCOUNT'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PRIVACY_MESSAGE =
|
export const PRIVACY_MESSAGE =
|
||||||
"The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
|
'The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.'
|
||||||
export const SHARED_PHOTO_BASE64_KEY = "shared-photo-base64";
|
export const SHARED_PHOTO_BASE64_KEY = 'shared-photo-base64'
|
||||||
|
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
export const UNIT_SHORT: Record<string, string> = {
|
export const UNIT_SHORT: Record<string, string> = {
|
||||||
@@ -68,21 +68,21 @@ export const UNIT_LONG: Record<string, string> = {
|
|||||||
|
|
||||||
const UNIT_CODES: Record<string, Record<string, string>> = {
|
const UNIT_CODES: Record<string, Record<string, string>> = {
|
||||||
BTC: {
|
BTC: {
|
||||||
name: "Bitcoin",
|
name: 'Bitcoin',
|
||||||
faIcon: "bitcoin-sign",
|
faIcon: 'bitcoin-sign'
|
||||||
},
|
},
|
||||||
HUR: {
|
HUR: {
|
||||||
name: "hours",
|
name: 'hours',
|
||||||
faIcon: "clock",
|
faIcon: 'clock'
|
||||||
},
|
},
|
||||||
USD: {
|
USD: {
|
||||||
name: "US Dollars",
|
name: 'US Dollars',
|
||||||
faIcon: "dollar",
|
faIcon: 'dollar'
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export function iconForUnitCode(unitCode: string) {
|
export function iconForUnitCode(unitCode: string) {
|
||||||
return UNIT_CODES[unitCode]?.faIcon || "question";
|
return UNIT_CODES[unitCode]?.faIcon || 'question'
|
||||||
}
|
}
|
||||||
|
|
||||||
// from https://stackoverflow.com/a/175787/845494
|
// from https://stackoverflow.com/a/175787/845494
|
||||||
@@ -92,11 +92,11 @@ export function isNumeric(str: string): boolean {
|
|||||||
// This ignore commentary is because typescript complains when you pass a string to isNaN.
|
// This ignore commentary is because typescript complains when you pass a string to isNaN.
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return !isNaN(str) && !isNaN(parseFloat(str));
|
return !isNaN(str) && !isNaN(parseFloat(str))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function numberOrZero(str: string): number {
|
export function numberOrZero(str: string): number {
|
||||||
return isNumeric(str) ? +str : 0;
|
return isNumeric(str) ? +str : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,67 +104,67 @@ export function numberOrZero(str: string): number {
|
|||||||
* also useful is https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Definition
|
* also useful is https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Definition
|
||||||
**/
|
**/
|
||||||
export const isGlobalUri = (uri: string) => {
|
export const isGlobalUri = (uri: string) => {
|
||||||
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
|
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/))
|
||||||
};
|
}
|
||||||
|
|
||||||
export const isGiveClaimType = (claimType?: string) => {
|
export const isGiveClaimType = (claimType?: string) => {
|
||||||
return claimType === "GiveAction";
|
return claimType === 'GiveAction'
|
||||||
};
|
}
|
||||||
|
|
||||||
export const isGiveAction = (
|
export const isGiveAction = (
|
||||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
veriClaim: GenericCredWrapper<GenericVerifiableCredential>
|
||||||
) => {
|
) => {
|
||||||
return isGiveClaimType(veriClaim.claimType);
|
return isGiveClaimType(veriClaim.claimType)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const shortDid = (did: string) => {
|
export const shortDid = (did: string) => {
|
||||||
if (did.startsWith("did:peer:")) {
|
if (did.startsWith('did:peer:')) {
|
||||||
return (
|
return (
|
||||||
did.substring(0, "did:peer:".length + 2) +
|
did.substring(0, 'did:peer:'.length + 2) +
|
||||||
"..." +
|
'...' +
|
||||||
did.substring("did:peer:".length + 18, "did:peer:".length + 25) +
|
did.substring('did:peer:'.length + 18, 'did:peer:'.length + 25) +
|
||||||
"..."
|
'...'
|
||||||
);
|
)
|
||||||
} else if (did.startsWith("did:ethr:")) {
|
} else if (did.startsWith('did:ethr:')) {
|
||||||
return did.substring(0, "did:ethr:".length + 9) + "...";
|
return did.substring(0, 'did:ethr:'.length + 9) + '...'
|
||||||
} else {
|
} else {
|
||||||
return did.substring(0, did.indexOf(":", 4) + 7) + "...";
|
return did.substring(0, did.indexOf(':', 4) + 7) + '...'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export const nameForDid = (
|
export const nameForDid = (
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
contacts: Array<Contact>,
|
contacts: Array<Contact>,
|
||||||
did: string,
|
did: string
|
||||||
): string => {
|
): string => {
|
||||||
if (did === activeDid) {
|
if (did === activeDid) {
|
||||||
return "you";
|
return 'you'
|
||||||
|
}
|
||||||
|
const contact = R.find((con) => con.did == did, contacts)
|
||||||
|
return nameForContact(contact)
|
||||||
}
|
}
|
||||||
const contact = R.find((con) => con.did == did, contacts);
|
|
||||||
return nameForContact(contact);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const nameForContact = (
|
export const nameForContact = (
|
||||||
contact?: Contact,
|
contact?: Contact,
|
||||||
capitalize?: boolean,
|
capitalize?: boolean
|
||||||
): string => {
|
): string => {
|
||||||
return (
|
return (
|
||||||
(contact?.name as string) ||
|
(contact?.name as string) ||
|
||||||
(capitalize ? "This" : "this") + " unnamed user"
|
(capitalize ? 'This' : 'this') + ' unnamed user'
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
|
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
|
||||||
fn();
|
fn()
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(text)
|
.copy(text)
|
||||||
.then(() => setTimeout(fn, 2000));
|
.then(() => setTimeout(fn, 2000))
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface ConfirmerData {
|
export interface ConfirmerData {
|
||||||
confirmerIdList: string[];
|
confirmerIdList: string[]
|
||||||
confsVisibleToIdList: string[];
|
confsVisibleToIdList: string[]
|
||||||
numConfsNotVisible: number;
|
numConfsNotVisible: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// // This is meant to be a second argument to JSON.stringify to avoid circular references.
|
// // This is meant to be a second argument to JSON.stringify to avoid circular references.
|
||||||
@@ -191,49 +191,49 @@ export async function retrieveConfirmerIdList(
|
|||||||
apiServer: string,
|
apiServer: string,
|
||||||
claimId: string,
|
claimId: string,
|
||||||
claimIssuerId: string,
|
claimIssuerId: string,
|
||||||
userDid: string,
|
userDid: string
|
||||||
): Promise<ConfirmerData | undefined> {
|
): Promise<ConfirmerData | undefined> {
|
||||||
const confirmUrl =
|
const confirmUrl =
|
||||||
apiServer +
|
apiServer +
|
||||||
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
|
'/api/report/issuersWhoClaimedOrConfirmed?claimId=' +
|
||||||
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
|
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId))
|
||||||
const confirmHeaders = await serverUtil.getHeaders(userDid);
|
const confirmHeaders = await serverUtil.getHeaders(userDid)
|
||||||
const response = await axios.get(confirmUrl, {
|
const response = await axios.get(confirmUrl, {
|
||||||
headers: confirmHeaders,
|
headers: confirmHeaders
|
||||||
});
|
})
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
const resultList1 = response.data.result || [];
|
const resultList1 = response.data.result || []
|
||||||
//const publicUrls = resultList.publicUrls || [];
|
//const publicUrls = resultList.publicUrls || [];
|
||||||
delete resultList1.publicUrls;
|
delete resultList1.publicUrls
|
||||||
// exclude hidden DIDs
|
// exclude hidden DIDs
|
||||||
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
|
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1)
|
||||||
// exclude the issuer
|
// exclude the issuer
|
||||||
const resultList3 = R.reject(
|
const resultList3 = R.reject(
|
||||||
(did: string) => did === claimIssuerId,
|
(did: string) => did === claimIssuerId,
|
||||||
resultList2,
|
resultList2
|
||||||
);
|
)
|
||||||
const confirmerIdList = resultList3;
|
const confirmerIdList = resultList3
|
||||||
let numConfsNotVisible = resultList1.length - resultList2.length;
|
let numConfsNotVisible = resultList1.length - resultList2.length
|
||||||
if (resultList3.length === resultList2.length) {
|
if (resultList3.length === resultList2.length) {
|
||||||
// the issuer was not in the "visible" list so they must be hidden
|
// the issuer was not in the "visible" list so they must be hidden
|
||||||
// so subtract them from the non-visible confirmers count
|
// so subtract them from the non-visible confirmers count
|
||||||
numConfsNotVisible = numConfsNotVisible - 1;
|
numConfsNotVisible = numConfsNotVisible - 1
|
||||||
}
|
}
|
||||||
const confsVisibleToIdList = response.data.result.resultVisibleToDids || [];
|
const confsVisibleToIdList = response.data.result.resultVisibleToDids || []
|
||||||
const result: ConfirmerData = {
|
const result: ConfirmerData = {
|
||||||
confirmerIdList,
|
confirmerIdList,
|
||||||
confsVisibleToIdList,
|
confsVisibleToIdList,
|
||||||
numConfsNotVisible,
|
numConfsNotVisible
|
||||||
};
|
}
|
||||||
return result;
|
return result
|
||||||
} else {
|
} else {
|
||||||
logger.error(
|
logger.error(
|
||||||
"Bad response status of",
|
'Bad response status of',
|
||||||
response.status,
|
response.status,
|
||||||
"for confirmers:",
|
'for confirmers:',
|
||||||
response,
|
response
|
||||||
);
|
)
|
||||||
return undefined;
|
return undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ export function isGiveRecordTheUserCanConfirm(
|
|||||||
isRegistered: boolean,
|
isRegistered: boolean,
|
||||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
confirmerIdList: string[] = [],
|
confirmerIdList: string[] = []
|
||||||
): boolean {
|
): boolean {
|
||||||
return (
|
return (
|
||||||
isRegistered &&
|
isRegistered &&
|
||||||
@@ -253,7 +253,7 @@ export function isGiveRecordTheUserCanConfirm(
|
|||||||
!confirmerIdList.includes(activeDid) &&
|
!confirmerIdList.includes(activeDid) &&
|
||||||
veriClaim.issuer !== activeDid &&
|
veriClaim.issuer !== activeDid &&
|
||||||
!containsHiddenDid(veriClaim.claim)
|
!containsHiddenDid(veriClaim.claim)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notifyWhyCannotConfirm(
|
export function notifyWhyCannotConfirm(
|
||||||
@@ -262,101 +262,101 @@ export function notifyWhyCannotConfirm(
|
|||||||
claimType: string | undefined,
|
claimType: string | undefined,
|
||||||
giveDetails: GiveSummaryRecord | undefined,
|
giveDetails: GiveSummaryRecord | undefined,
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
confirmerIdList: string[] = [],
|
confirmerIdList: string[] = []
|
||||||
) {
|
) {
|
||||||
if (!isRegistered) {
|
if (!isRegistered) {
|
||||||
notifyFun(
|
notifyFun(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Not Registered",
|
title: 'Not Registered',
|
||||||
text: "Someone needs to register you before you can confirm.",
|
text: 'Someone needs to register you before you can confirm.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
} else if (!isGiveClaimType(claimType)) {
|
} else if (!isGiveClaimType(claimType)) {
|
||||||
notifyFun(
|
notifyFun(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Not A Give",
|
title: 'Not A Give',
|
||||||
text: "This is not a giving action to confirm.",
|
text: 'This is not a giving action to confirm.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
} else if (confirmerIdList.includes(activeDid)) {
|
} else if (confirmerIdList.includes(activeDid)) {
|
||||||
notifyFun(
|
notifyFun(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Already Confirmed",
|
title: 'Already Confirmed',
|
||||||
text: "You already confirmed this claim.",
|
text: 'You already confirmed this claim.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
} else if (giveDetails?.issuerDid == activeDid) {
|
} else if (giveDetails?.issuerDid == activeDid) {
|
||||||
notifyFun(
|
notifyFun(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Cannot Confirm",
|
title: 'Cannot Confirm',
|
||||||
text: "You cannot confirm this because you issued this claim.",
|
text: 'You cannot confirm this because you issued this claim.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
} else if (serverUtil.containsHiddenDid(giveDetails?.fullClaim)) {
|
} else if (serverUtil.containsHiddenDid(giveDetails?.fullClaim)) {
|
||||||
notifyFun(
|
notifyFun(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Cannot Confirm",
|
title: 'Cannot Confirm',
|
||||||
text: "You cannot confirm this because some people are hidden.",
|
text: 'You cannot confirm this because some people are hidden.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
} else {
|
} else {
|
||||||
notifyFun(
|
notifyFun(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: 'alert',
|
||||||
type: "info",
|
type: 'info',
|
||||||
title: "Cannot Confirm",
|
title: 'Cannot Confirm',
|
||||||
text: "You cannot confirm this claim. There are no other details -- we can help more if you contact us and send us screenshots.",
|
text: 'You cannot confirm this claim. There are no other details -- we can help more if you contact us and send us screenshots.'
|
||||||
},
|
},
|
||||||
3000,
|
3000
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function blobToBase64(blob: Blob): Promise<string> {
|
export async function blobToBase64(blob: Blob): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader()
|
||||||
reader.onloadend = () => resolve(reader.result as string); // potential problem if it returns an ArrayBuffer?
|
reader.onloadend = () => resolve(reader.result as string) // potential problem if it returns an ArrayBuffer?
|
||||||
reader.onerror = reject;
|
reader.onerror = reject
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
|
export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
|
||||||
// Extract the content type and the Base64 data
|
// Extract the content type and the Base64 data
|
||||||
const [metadata, base64] = base64DataUrl.split(",");
|
const [metadata, base64] = base64DataUrl.split(',')
|
||||||
const contentTypeMatch = metadata.match(/data:(.*?);base64/);
|
const contentTypeMatch = metadata.match(/data:(.*?);base64/)
|
||||||
const contentType = contentTypeMatch ? contentTypeMatch[1] : "";
|
const contentType = contentTypeMatch ? contentTypeMatch[1] : ''
|
||||||
|
|
||||||
const byteCharacters = atob(base64);
|
const byteCharacters = atob(base64)
|
||||||
const byteArrays = [];
|
const byteArrays = []
|
||||||
|
|
||||||
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
||||||
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
const slice = byteCharacters.slice(offset, offset + sliceSize)
|
||||||
|
|
||||||
const byteNumbers = new Array(slice.length);
|
const byteNumbers = new Array(slice.length)
|
||||||
for (let i = 0; i < slice.length; i++) {
|
for (let i = 0; i < slice.length; i++) {
|
||||||
byteNumbers[i] = slice.charCodeAt(i);
|
byteNumbers[i] = slice.charCodeAt(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
const byteArray = new Uint8Array(byteNumbers);
|
const byteArray = new Uint8Array(byteNumbers)
|
||||||
byteArrays.push(byteArray);
|
byteArrays.push(byteArray)
|
||||||
}
|
}
|
||||||
return new Blob(byteArrays, { type: contentType });
|
return new Blob(byteArrays, { type: contentType })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -364,18 +364,18 @@ export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
|
|||||||
* @param veriClaim is expected to have fields: claim and issuer
|
* @param veriClaim is expected to have fields: claim and issuer
|
||||||
*/
|
*/
|
||||||
export function offerGiverDid(
|
export function offerGiverDid(
|
||||||
veriClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
veriClaim: GenericCredWrapper<OfferVerifiableCredential>
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
let giver;
|
let giver
|
||||||
if (
|
if (
|
||||||
veriClaim.claim.offeredBy?.identifier &&
|
veriClaim.claim.offeredBy?.identifier &&
|
||||||
!serverUtil.isHiddenDid(veriClaim.claim.offeredBy.identifier as string)
|
!serverUtil.isHiddenDid(veriClaim.claim.offeredBy.identifier as string)
|
||||||
) {
|
) {
|
||||||
giver = veriClaim.claim.offeredBy.identifier;
|
giver = veriClaim.claim.offeredBy.identifier
|
||||||
} else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) {
|
} else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) {
|
||||||
giver = veriClaim.issuer;
|
giver = veriClaim.issuer
|
||||||
}
|
}
|
||||||
return giver;
|
return giver
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -383,53 +383,53 @@ export function offerGiverDid(
|
|||||||
* @param veriClaim is expected to have fields: claim, claimType, and issuer
|
* @param veriClaim is expected to have fields: claim, claimType, and issuer
|
||||||
*/
|
*/
|
||||||
export const canFulfillOffer = (
|
export const canFulfillOffer = (
|
||||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
veriClaim: GenericCredWrapper<GenericVerifiableCredential>
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
veriClaim.claimType === "Offer" &&
|
veriClaim.claimType === 'Offer' &&
|
||||||
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferVerifiableCredential>)
|
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferVerifiableCredential>)
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
// return object with paths and arrays of DIDs for any keys ending in "VisibleToDid"
|
// return object with paths and arrays of DIDs for any keys ending in "VisibleToDid"
|
||||||
export function findAllVisibleToDids(
|
export function findAllVisibleToDids(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
input: any,
|
input: any,
|
||||||
humanReadable = false,
|
humanReadable = false
|
||||||
): Record<string, Array<string>> {
|
): Record<string, Array<string>> {
|
||||||
if (Array.isArray(input)) {
|
if (Array.isArray(input)) {
|
||||||
const result: Record<string, Array<string>> = {};
|
const result: Record<string, Array<string>> = {}
|
||||||
for (let i = 0; i < input.length; i++) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
const inside = findAllVisibleToDids(input[i], humanReadable);
|
const inside = findAllVisibleToDids(input[i], humanReadable)
|
||||||
for (const key in inside) {
|
for (const key in inside) {
|
||||||
const pathKey = humanReadable
|
const pathKey = humanReadable
|
||||||
? "#" + (i + 1) + " " + key
|
? '#' + (i + 1) + ' ' + key
|
||||||
: "[" + i + "]" + key;
|
: '[' + i + ']' + key
|
||||||
result[pathKey] = inside[key];
|
result[pathKey] = inside[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result
|
||||||
} else if (input instanceof Object) {
|
} else if (input instanceof Object) {
|
||||||
// regular map (non-array) object
|
// regular map (non-array) object
|
||||||
const result: Record<string, Array<string>> = {};
|
const result: Record<string, Array<string>> = {}
|
||||||
for (const key in input) {
|
for (const key in input) {
|
||||||
if (key.endsWith("VisibleToDids")) {
|
if (key.endsWith('VisibleToDids')) {
|
||||||
const newKey = key.slice(0, -"VisibleToDids".length);
|
const newKey = key.slice(0, -'VisibleToDids'.length)
|
||||||
const pathKey = humanReadable ? newKey : "." + newKey;
|
const pathKey = humanReadable ? newKey : '.' + newKey
|
||||||
result[pathKey] = input[key];
|
result[pathKey] = input[key]
|
||||||
} else {
|
} else {
|
||||||
const inside = findAllVisibleToDids(input[key], humanReadable);
|
const inside = findAllVisibleToDids(input[key], humanReadable)
|
||||||
for (const insideKey in inside) {
|
for (const insideKey in inside) {
|
||||||
const pathKey = humanReadable
|
const pathKey = humanReadable
|
||||||
? key + "'s " + insideKey
|
? key + "'s " + insideKey
|
||||||
: "." + key + insideKey;
|
: '.' + key + insideKey
|
||||||
result[pathKey] = inside[insideKey];
|
result[pathKey] = inside[insideKey]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result
|
||||||
} else {
|
} else {
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,151 +461,151 @@ export interface AccountKeyInfo extends Account, KeyMeta {}
|
|||||||
|
|
||||||
export const retrieveAccountCount = async (): Promise<number> => {
|
export const retrieveAccountCount = async (): Promise<number> => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise
|
||||||
return await accountsDB.accounts.count();
|
return await accountsDB.accounts.count()
|
||||||
};
|
}
|
||||||
|
|
||||||
export const retrieveAccountDids = async (): Promise<string[]> => {
|
export const retrieveAccountDids = async (): Promise<string[]> => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise
|
||||||
const allAccounts = await accountsDB.accounts.toArray();
|
const allAccounts = await accountsDB.accounts.toArray()
|
||||||
const allDids = allAccounts.map((acc) => acc.did);
|
const allDids = allAccounts.map((acc) => acc.did)
|
||||||
return allDids;
|
return allDids
|
||||||
};
|
}
|
||||||
|
|
||||||
// This is provided and recommended when the full key is not necessary so that
|
// This is provided and recommended when the full key is not necessary so that
|
||||||
// future work could separate this info from the sensitive key material.
|
// future work could separate this info from the sensitive key material.
|
||||||
export const retrieveAccountMetadata = async (
|
export const retrieveAccountMetadata = async (
|
||||||
activeDid: string,
|
activeDid: string
|
||||||
): Promise<AccountKeyInfo | undefined> => {
|
): Promise<AccountKeyInfo | undefined> => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise
|
||||||
const account = (await accountsDB.accounts
|
const account = (await accountsDB.accounts
|
||||||
.where("did")
|
.where('did')
|
||||||
.equals(activeDid)
|
.equals(activeDid)
|
||||||
.first()) as Account;
|
.first()) as Account
|
||||||
if (account) {
|
if (account) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { identity, mnemonic, ...metadata } = account;
|
const { identity, mnemonic, ...metadata } = account
|
||||||
return metadata;
|
return metadata
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise
|
||||||
const array = await accountsDB.accounts.toArray();
|
const array = await accountsDB.accounts.toArray()
|
||||||
return array.map((account) => {
|
return array.map((account) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { identity, mnemonic, ...metadata } = account;
|
const { identity, mnemonic, ...metadata } = account
|
||||||
return metadata;
|
return metadata
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export const retrieveFullyDecryptedAccount = async (
|
export const retrieveFullyDecryptedAccount = async (
|
||||||
activeDid: string,
|
activeDid: string
|
||||||
): Promise<AccountKeyInfo | undefined> => {
|
): Promise<AccountKeyInfo | undefined> => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise
|
||||||
const account = (await accountsDB.accounts
|
const account = (await accountsDB.accounts
|
||||||
.where("did")
|
.where('did')
|
||||||
.equals(activeDid)
|
.equals(activeDid)
|
||||||
.first()) as Account;
|
.first()) as Account
|
||||||
return account;
|
return account
|
||||||
};
|
}
|
||||||
|
|
||||||
// let's try and eliminate this
|
// let's try and eliminate this
|
||||||
export const retrieveAllFullyDecryptedAccounts = async (): Promise<
|
export const retrieveAllFullyDecryptedAccounts = async (): Promise<
|
||||||
Array<AccountKeyInfo>
|
Array<AccountKeyInfo>
|
||||||
> => {
|
> => {
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise
|
||||||
const allAccounts = await accountsDB.accounts.toArray();
|
const allAccounts = await accountsDB.accounts.toArray()
|
||||||
return allAccounts;
|
return allAccounts
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new identity, saves it to the database, and sets it as the active identity.
|
* Generates a new identity, saves it to the database, and sets it as the active identity.
|
||||||
* @return {Promise<string>} with the DID of the new identity
|
* @return {Promise<string>} with the DID of the new identity
|
||||||
*/
|
*/
|
||||||
export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
||||||
const mnemonic = generateSeed();
|
const mnemonic = generateSeed()
|
||||||
// address is 0x... ETH address, without "did:eth:"
|
// address is 0x... ETH address, without "did:eth:"
|
||||||
const [address, privateHex, publicHex, derivationPath] =
|
const [address, privateHex, publicHex, derivationPath] =
|
||||||
deriveAddress(mnemonic);
|
deriveAddress(mnemonic)
|
||||||
|
|
||||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
const newId = newIdentifier(address, publicHex, privateHex, derivationPath)
|
||||||
const identity = JSON.stringify(newId);
|
const identity = JSON.stringify(newId)
|
||||||
|
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise
|
||||||
await accountsDB.accounts.add({
|
await accountsDB.accounts.add({
|
||||||
dateCreated: new Date().toISOString(),
|
dateCreated: new Date().toISOString(),
|
||||||
derivationPath: derivationPath,
|
derivationPath: derivationPath,
|
||||||
did: newId.did,
|
did: newId.did,
|
||||||
identity: identity,
|
identity: identity,
|
||||||
mnemonic: mnemonic,
|
mnemonic: mnemonic,
|
||||||
publicKeyHex: newId.keys[0].publicKeyHex,
|
publicKeyHex: newId.keys[0].publicKeyHex
|
||||||
});
|
})
|
||||||
|
|
||||||
await updateDefaultSettings({ activeDid: newId.did });
|
await updateDefaultSettings({ activeDid: newId.did })
|
||||||
//console.log("Updated default settings in util");
|
//console.log("Updated default settings in util");
|
||||||
await updateAccountSettings(newId.did, { isRegistered: false });
|
await updateAccountSettings(newId.did, { isRegistered: false })
|
||||||
|
|
||||||
return newId.did;
|
return newId.did
|
||||||
};
|
}
|
||||||
|
|
||||||
export const registerAndSavePasskey = async (
|
export const registerAndSavePasskey = async (
|
||||||
keyName: string,
|
keyName: string
|
||||||
): Promise<Account> => {
|
): Promise<Account> => {
|
||||||
const cred = await registerCredential(keyName);
|
const cred = await registerCredential(keyName)
|
||||||
const publicKeyBytes = cred.publicKeyBytes;
|
const publicKeyBytes = cred.publicKeyBytes
|
||||||
const did = createPeerDid(publicKeyBytes as Uint8Array);
|
const did = createPeerDid(publicKeyBytes as Uint8Array)
|
||||||
const passkeyCredIdHex = cred.credIdHex as string;
|
const passkeyCredIdHex = cred.credIdHex as string
|
||||||
|
|
||||||
const account = {
|
const account = {
|
||||||
dateCreated: new Date().toISOString(),
|
dateCreated: new Date().toISOString(),
|
||||||
did,
|
did,
|
||||||
passkeyCredIdHex,
|
passkeyCredIdHex,
|
||||||
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
|
publicKeyHex: Buffer.from(publicKeyBytes).toString('hex')
|
||||||
};
|
}
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise
|
||||||
await accountsDB.accounts.add(account);
|
await accountsDB.accounts.add(account)
|
||||||
return account;
|
return account
|
||||||
};
|
}
|
||||||
|
|
||||||
export const registerSaveAndActivatePasskey = async (
|
export const registerSaveAndActivatePasskey = async (
|
||||||
keyName: string,
|
keyName: string
|
||||||
): Promise<Account> => {
|
): Promise<Account> => {
|
||||||
const account = await registerAndSavePasskey(keyName);
|
const account = await registerAndSavePasskey(keyName)
|
||||||
await updateDefaultSettings({ activeDid: account.did });
|
await updateDefaultSettings({ activeDid: account.did })
|
||||||
await updateAccountSettings(account.did, { isRegistered: false });
|
await updateAccountSettings(account.did, { isRegistered: false })
|
||||||
return account;
|
return account
|
||||||
};
|
}
|
||||||
|
|
||||||
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
|
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
return (
|
return (
|
||||||
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
|
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
|
||||||
60
|
60
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
// These are shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
|
// These are shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
|
||||||
export const DAILY_CHECK_TITLE = "DAILY_CHECK";
|
export const DAILY_CHECK_TITLE = 'DAILY_CHECK'
|
||||||
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
|
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
|
||||||
export const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
|
export const DIRECT_PUSH_TITLE = 'DIRECT_NOTIFICATION'
|
||||||
|
|
||||||
export const sendTestThroughPushServer = async (
|
export const sendTestThroughPushServer = async (
|
||||||
subscriptionJSON: PushSubscriptionJSON,
|
subscriptionJSON: PushSubscriptionJSON,
|
||||||
skipFilter: boolean,
|
skipFilter: boolean
|
||||||
): Promise<AxiosResponse> => {
|
): Promise<AxiosResponse> => {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
|
let pushUrl: string = DEFAULT_PUSH_SERVER as string
|
||||||
if (settings?.webPushServer) {
|
if (settings?.webPushServer) {
|
||||||
pushUrl = settings.webPushServer;
|
pushUrl = settings.webPushServer
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPayload = {
|
const newPayload = {
|
||||||
@@ -613,20 +613,20 @@ export const sendTestThroughPushServer = async (
|
|||||||
// ... overridden with the following
|
// ... overridden with the following
|
||||||
// eslint-disable-next-line prettier/prettier
|
// eslint-disable-next-line prettier/prettier
|
||||||
message: `Test, where you will see this message ${ skipFilter ? "un" : "" }filtered.`,
|
message: `Test, where you will see this message ${ skipFilter ? "un" : "" }filtered.`,
|
||||||
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
|
title: skipFilter ? DIRECT_PUSH_TITLE : 'Your Web Push'
|
||||||
};
|
}
|
||||||
logger.log("Sending a test web push message:", newPayload);
|
logger.log('Sending a test web push message:', newPayload)
|
||||||
const payloadStr = JSON.stringify(newPayload);
|
const payloadStr = JSON.stringify(newPayload)
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
pushUrl + "/web-push/send-test",
|
pushUrl + '/web-push/send-test',
|
||||||
payloadStr,
|
payloadStr,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json'
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
logger.log("Got response from web push server:", response);
|
logger.log('Got response from web push server:', response)
|
||||||
return response;
|
return response
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// see also ../constants/app.ts and
|
// see also ../constants/app.ts and
|
||||||
|
|
||||||
function didProviderName(netName: string) {
|
function didProviderName(netName: string) {
|
||||||
return "did:ethr" + (netName === "mainnet" ? "" : ":" + netName);
|
return 'did:ethr' + (netName === 'mainnet' ? '' : ':' + netName)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_DID_PROVIDER_NAME = didProviderName("mainnet");
|
export const DEFAULT_DID_PROVIDER_NAME = didProviderName('mainnet')
|
||||||
|
|||||||
@@ -28,28 +28,28 @@
|
|||||||
* // Processed and routed to appropriate view with type-safe parameters
|
* // Processed and routed to appropriate view with type-safe parameters
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from './main.common'
|
||||||
import { App } from "./lib/capacitor/app";
|
import { App } from './lib/capacitor/app'
|
||||||
import router from "./router";
|
import router from './router'
|
||||||
import { handleApiError } from "./services/api";
|
import { handleApiError } from './services/api'
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from 'axios'
|
||||||
import { DeepLinkHandler } from "./services/deepLinks";
|
import { DeepLinkHandler } from './services/deepLinks'
|
||||||
import { logConsoleAndDb } from "./db";
|
import { logConsoleAndDb } from './db'
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from './utils/logger'
|
||||||
|
|
||||||
logger.log("[Capacitor] Starting initialization");
|
logger.log('[Capacitor] Starting initialization')
|
||||||
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
logger.log('[Capacitor] Platform:', process.env.VITE_PLATFORM)
|
||||||
|
|
||||||
const app = initializeApp();
|
const app = initializeApp()
|
||||||
|
|
||||||
// Initialize API error handling for unhandled promise rejections
|
// Initialize API error handling for unhandled promise rejections
|
||||||
window.addEventListener("unhandledrejection", (event) => {
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
if (event.reason?.response) {
|
if (event.reason?.response) {
|
||||||
handleApiError(event.reason, event.reason.config?.url || "unknown");
|
handleApiError(event.reason, event.reason.config?.url || 'unknown')
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
const deepLinkHandler = new DeepLinkHandler(router);
|
const deepLinkHandler = new DeepLinkHandler(router)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles deep link routing for the application
|
* Handles deep link routing for the application
|
||||||
@@ -69,22 +69,22 @@ const deepLinkHandler = new DeepLinkHandler(router);
|
|||||||
*/
|
*/
|
||||||
const handleDeepLink = async (data: { url: string }) => {
|
const handleDeepLink = async (data: { url: string }) => {
|
||||||
try {
|
try {
|
||||||
await router.isReady();
|
await router.isReady()
|
||||||
await deepLinkHandler.handleDeepLink(data.url);
|
await deepLinkHandler.handleDeepLink(data.url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
|
logConsoleAndDb('[DeepLink] Error handling deep link: ' + error, true)
|
||||||
handleApiError(
|
handleApiError(
|
||||||
{
|
{
|
||||||
message: error instanceof Error ? error.message : String(error),
|
message: error instanceof Error ? error.message : String(error)
|
||||||
} as AxiosError,
|
} as AxiosError,
|
||||||
"deep-link",
|
'deep-link'
|
||||||
);
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Register deep link handler with Capacitor
|
// Register deep link handler with Capacitor
|
||||||
App.addListener("appUrlOpen", handleDeepLink);
|
App.addListener('appUrlOpen', handleDeepLink)
|
||||||
|
|
||||||
logger.log("[Capacitor] Mounting app");
|
logger.log('[Capacitor] Mounting app')
|
||||||
app.mount("#app");
|
app.mount('#app')
|
||||||
logger.log("[Capacitor] App mounted");
|
logger.log('[Capacitor] App mounted')
|
||||||
|
|||||||
@@ -1,61 +1,61 @@
|
|||||||
import { createPinia } from "pinia";
|
import { createPinia } from 'pinia'
|
||||||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
|
import { App as VueApp, ComponentPublicInstance, createApp } from 'vue'
|
||||||
import App from "./App.vue";
|
import App from './App.vue'
|
||||||
import router from "./router";
|
import router from './router'
|
||||||
import axios from "axios";
|
import axios from 'axios'
|
||||||
import VueAxios from "vue-axios";
|
import VueAxios from 'vue-axios'
|
||||||
import Notifications from "notiwind";
|
import Notifications from 'notiwind'
|
||||||
import "./assets/styles/tailwind.css";
|
import './assets/styles/tailwind.css'
|
||||||
import { FontAwesomeIcon } from "./lib/fontawesome";
|
import { FontAwesomeIcon } from './lib/fontawesome'
|
||||||
import Camera from "simple-vue-camera";
|
import Camera from 'simple-vue-camera'
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from './utils/logger'
|
||||||
|
|
||||||
// Global Error Handler
|
// Global Error Handler
|
||||||
function setupGlobalErrorHandler(app: VueApp) {
|
function setupGlobalErrorHandler(app: VueApp) {
|
||||||
logger.log("[App Init] Setting up global error handler");
|
logger.log('[App Init] Setting up global error handler')
|
||||||
app.config.errorHandler = (
|
app.config.errorHandler = (
|
||||||
err: unknown,
|
err: unknown,
|
||||||
instance: ComponentPublicInstance | null,
|
instance: ComponentPublicInstance | null,
|
||||||
info: string,
|
info: string
|
||||||
) => {
|
) => {
|
||||||
logger.error("[App Error] Global Error Handler:", {
|
logger.error('[App Error] Global Error Handler:', {
|
||||||
error: err,
|
error: err,
|
||||||
info,
|
info,
|
||||||
component: instance?.$options.name || "unknown",
|
component: instance?.$options.name || 'unknown'
|
||||||
});
|
})
|
||||||
alert(
|
alert(
|
||||||
(err instanceof Error ? err.message : "Something bad happened") +
|
(err instanceof Error ? err.message : 'Something bad happened') +
|
||||||
" - Try reloading or restarting the app.",
|
' - Try reloading or restarting the app.'
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to initialize the app
|
// Function to initialize the app
|
||||||
export function initializeApp() {
|
export function initializeApp() {
|
||||||
logger.log("[App Init] Starting app initialization");
|
logger.log('[App Init] Starting app initialization')
|
||||||
logger.log("[App Init] Platform:", process.env.VITE_PLATFORM);
|
logger.log('[App Init] Platform:', process.env.VITE_PLATFORM)
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App)
|
||||||
logger.log("[App Init] Vue app created");
|
logger.log('[App Init] Vue app created')
|
||||||
|
|
||||||
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera);
|
app.component('FontAwesome', FontAwesomeIcon).component('camera', Camera)
|
||||||
logger.log("[App Init] Components registered");
|
logger.log('[App Init] Components registered')
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia()
|
||||||
app.use(pinia);
|
app.use(pinia)
|
||||||
logger.log("[App Init] Pinia store initialized");
|
logger.log('[App Init] Pinia store initialized')
|
||||||
|
|
||||||
app.use(VueAxios, axios);
|
app.use(VueAxios, axios)
|
||||||
logger.log("[App Init] Axios initialized");
|
logger.log('[App Init] Axios initialized')
|
||||||
|
|
||||||
app.use(router);
|
app.use(router)
|
||||||
logger.log("[App Init] Router initialized");
|
logger.log('[App Init] Router initialized')
|
||||||
|
|
||||||
app.use(Notifications);
|
app.use(Notifications)
|
||||||
logger.log("[App Init] Notifications initialized");
|
logger.log('[App Init] Notifications initialized')
|
||||||
|
|
||||||
setupGlobalErrorHandler(app);
|
setupGlobalErrorHandler(app)
|
||||||
logger.log("[App Init] App initialization complete");
|
logger.log('[App Init] App initialization complete')
|
||||||
|
|
||||||
return app;
|
return app
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from './main.common'
|
||||||
|
|
||||||
const app = initializeApp();
|
const app = initializeApp()
|
||||||
app.mount("#app");
|
app.mount('#app')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from './main.common'
|
||||||
|
|
||||||
const app = initializeApp();
|
const app = initializeApp()
|
||||||
app.mount("#app");
|
app.mount('#app')
|
||||||
|
|||||||
68
src/main.ts
68
src/main.ts
@@ -1,14 +1,14 @@
|
|||||||
import { createPinia } from "pinia";
|
import { createPinia } from 'pinia'
|
||||||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
|
import { App as VueApp, ComponentPublicInstance, createApp } from 'vue'
|
||||||
import App from "./App.vue";
|
import App from './App.vue'
|
||||||
import "./registerServiceWorker";
|
import './registerServiceWorker'
|
||||||
import router from "./router";
|
import router from './router'
|
||||||
import axios from "axios";
|
import axios from 'axios'
|
||||||
import VueAxios from "vue-axios";
|
import VueAxios from 'vue-axios'
|
||||||
import Notifications from "notiwind";
|
import Notifications from 'notiwind'
|
||||||
import "./assets/styles/tailwind.css";
|
import './assets/styles/tailwind.css'
|
||||||
|
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
@@ -86,8 +86,8 @@ import {
|
|||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
faXmark,
|
faXmark
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
@@ -166,12 +166,12 @@ library.add(
|
|||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
faUser,
|
faUser,
|
||||||
faUsers,
|
faUsers,
|
||||||
faXmark,
|
faXmark
|
||||||
);
|
)
|
||||||
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
import Camera from "simple-vue-camera";
|
import Camera from 'simple-vue-camera'
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from './utils/logger'
|
||||||
|
|
||||||
// Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
|
// Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
|
||||||
function setupGlobalErrorHandler(app: VueApp) {
|
function setupGlobalErrorHandler(app: VueApp) {
|
||||||
@@ -179,35 +179,35 @@ function setupGlobalErrorHandler(app: VueApp) {
|
|||||||
app.config.errorHandler = (
|
app.config.errorHandler = (
|
||||||
err: Error,
|
err: Error,
|
||||||
instance: ComponentPublicInstance | null,
|
instance: ComponentPublicInstance | null,
|
||||||
info: string,
|
info: string
|
||||||
) => {
|
) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
"Ouch! Global Error Handler.",
|
'Ouch! Global Error Handler.',
|
||||||
"Error:",
|
'Error:',
|
||||||
err,
|
err,
|
||||||
"- Error toString:",
|
'- Error toString:',
|
||||||
err.toString(),
|
err.toString(),
|
||||||
"- Info:",
|
'- Info:',
|
||||||
info,
|
info,
|
||||||
"- Instance:",
|
'- Instance:',
|
||||||
instance,
|
instance
|
||||||
);
|
)
|
||||||
// Want to show a nice notiwind notification but can't figure out how.
|
// Want to show a nice notiwind notification but can't figure out how.
|
||||||
alert(
|
alert(
|
||||||
(err.message || "Something bad happened") +
|
(err.message || 'Something bad happened') +
|
||||||
" - Try reloading or restarting the app.",
|
' - Try reloading or restarting the app.'
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
.component("fa", FontAwesomeIcon)
|
.component('fa', FontAwesomeIcon)
|
||||||
.component("camera", Camera)
|
.component('camera', Camera)
|
||||||
.use(createPinia())
|
.use(createPinia())
|
||||||
.use(VueAxios, axios)
|
.use(VueAxios, axios)
|
||||||
.use(router)
|
.use(router)
|
||||||
.use(Notifications);
|
.use(Notifications)
|
||||||
|
|
||||||
setupGlobalErrorHandler(app);
|
setupGlobalErrorHandler(app)
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount('#app')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from './main.common'
|
||||||
import "./registerServiceWorker"; // Web PWA support
|
import './registerServiceWorker' // Web PWA support
|
||||||
|
|
||||||
const app = initializeApp();
|
const app = initializeApp()
|
||||||
app.mount("#app");
|
app.mount('#app')
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { register } from "register-service-worker";
|
import { register } from 'register-service-worker'
|
||||||
|
|
||||||
// Only register service worker if explicitly enabled and in production
|
// Only register service worker if explicitly enabled and in production
|
||||||
if (
|
if (
|
||||||
process.env.VITE_PWA_ENABLED === "true" &&
|
process.env.VITE_PWA_ENABLED === 'true' &&
|
||||||
process.env.NODE_ENV === "production"
|
process.env.NODE_ENV === 'production'
|
||||||
) {
|
) {
|
||||||
register(`${process.env.BASE_URL}sw.js`, {
|
register(`${process.env.BASE_URL}sw.js`, {
|
||||||
ready() {
|
ready() {
|
||||||
console.log("Service worker is active.");
|
console.log('Service worker is active.')
|
||||||
},
|
},
|
||||||
registered() {
|
registered() {
|
||||||
console.log("Service worker has been registered.");
|
console.log('Service worker has been registered.')
|
||||||
},
|
},
|
||||||
cached() {
|
cached() {
|
||||||
console.log("Content has been cached for offline use.");
|
console.log('Content has been cached for offline use.')
|
||||||
},
|
},
|
||||||
updatefound() {
|
updatefound() {
|
||||||
console.log("New content is downloading.");
|
console.log('New content is downloading.')
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
console.log("New content is available; please refresh.");
|
console.log('New content is available; please refresh.')
|
||||||
},
|
},
|
||||||
offline() {
|
offline() {
|
||||||
console.log(
|
console.log(
|
||||||
"No internet connection found. App is running in offline mode.",
|
'No internet connection found. App is running in offline mode.'
|
||||||
);
|
)
|
||||||
},
|
},
|
||||||
error(error) {
|
error(error) {
|
||||||
console.error("Error during service worker registration:", error);
|
console.error('Error during service worker registration:', error)
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
"Service worker registration skipped - not enabled or not in production",
|
'Service worker registration skipped - not enabled or not in production'
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import {
|
|||||||
createMemoryHistory,
|
createMemoryHistory,
|
||||||
NavigationGuardNext,
|
NavigationGuardNext,
|
||||||
RouteLocationNormalized,
|
RouteLocationNormalized,
|
||||||
RouteRecordRaw,
|
RouteRecordRaw
|
||||||
} from "vue-router";
|
} from 'vue-router'
|
||||||
import { accountsDBPromise } from "../db/index";
|
import { accountsDBPromise } from '../db/index'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../utils/logger'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -18,317 +18,317 @@ import { logger } from "../utils/logger";
|
|||||||
const enterOrStart = async (
|
const enterOrStart = async (
|
||||||
to: RouteLocationNormalized,
|
to: RouteLocationNormalized,
|
||||||
from: RouteLocationNormalized,
|
from: RouteLocationNormalized,
|
||||||
next: NavigationGuardNext,
|
next: NavigationGuardNext
|
||||||
) => {
|
) => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
const num_accounts = await accountsDB.accounts.count()
|
||||||
|
|
||||||
if (num_accounts > 0) {
|
if (num_accounts > 0) {
|
||||||
next();
|
next()
|
||||||
} else {
|
} else {
|
||||||
next({ name: "start" });
|
next({ name: 'start' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
path: "/account",
|
path: '/account',
|
||||||
name: "account",
|
name: 'account',
|
||||||
component: () => import("../views/AccountViewView.vue"),
|
component: () => import('../views/AccountViewView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/claim/:id?",
|
path: '/claim/:id?',
|
||||||
name: "claim",
|
name: 'claim',
|
||||||
component: () => import("../views/ClaimView.vue"),
|
component: () => import('../views/ClaimView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/claim-add-raw/:id?",
|
path: '/claim-add-raw/:id?',
|
||||||
name: "claim-add-raw",
|
name: 'claim-add-raw',
|
||||||
component: () => import("../views/ClaimAddRawView.vue"),
|
component: () => import('../views/ClaimAddRawView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/claim-cert/:id",
|
path: '/claim-cert/:id',
|
||||||
name: "claim-cert",
|
name: 'claim-cert',
|
||||||
component: () => import("../views/ClaimCertificateView.vue"),
|
component: () => import('../views/ClaimCertificateView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/confirm-contact",
|
path: '/confirm-contact',
|
||||||
name: "confirm-contact",
|
name: 'confirm-contact',
|
||||||
component: () => import("../views/ConfirmContactView.vue"),
|
component: () => import('../views/ConfirmContactView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/confirm-gift/:id?",
|
path: '/confirm-gift/:id?',
|
||||||
name: "confirm-gift",
|
name: 'confirm-gift',
|
||||||
component: () => import("../views/ConfirmGiftView.vue"),
|
component: () => import('../views/ConfirmGiftView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/contact-amounts",
|
path: '/contact-amounts',
|
||||||
name: "contact-amounts",
|
name: 'contact-amounts',
|
||||||
component: () => import("../views/ContactAmountsView.vue"),
|
component: () => import('../views/ContactAmountsView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/contact-edit/:did",
|
path: '/contact-edit/:did',
|
||||||
name: "contact-edit",
|
name: 'contact-edit',
|
||||||
component: () => import("../views/ContactEditView.vue"),
|
component: () => import('../views/ContactEditView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/contact-gift",
|
path: '/contact-gift',
|
||||||
name: "contact-gift",
|
name: 'contact-gift',
|
||||||
component: () => import("../views/ContactGiftingView.vue"),
|
component: () => import('../views/ContactGiftingView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/contact-import/:jwt?",
|
path: '/contact-import/:jwt?',
|
||||||
name: "contact-import",
|
name: 'contact-import',
|
||||||
component: () => import("../views/ContactImportView.vue"),
|
component: () => import('../views/ContactImportView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/contact-qr",
|
path: '/contact-qr',
|
||||||
name: "contact-qr",
|
name: 'contact-qr',
|
||||||
component: () => import("../views/ContactQRScanShowView.vue"),
|
component: () => import('../views/ContactQRScanShowView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/contacts",
|
path: '/contacts',
|
||||||
name: "contacts",
|
name: 'contacts',
|
||||||
component: () => import("../views/ContactsView.vue"),
|
component: () => import('../views/ContactsView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/did/:did?",
|
path: '/did/:did?',
|
||||||
name: "did",
|
name: 'did',
|
||||||
component: () => import("../views/DIDView.vue"),
|
component: () => import('../views/DIDView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/discover",
|
path: '/discover',
|
||||||
name: "discover",
|
name: 'discover',
|
||||||
component: () => import("../views/DiscoverView.vue"),
|
component: () => import('../views/DiscoverView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/gifted-details",
|
path: '/gifted-details',
|
||||||
name: "gifted-details",
|
name: 'gifted-details',
|
||||||
component: () => import("../views/GiftedDetailsView.vue"),
|
component: () => import('../views/GiftedDetailsView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/help",
|
path: '/help',
|
||||||
name: "help",
|
name: 'help',
|
||||||
component: () => import("../views/HelpView.vue"),
|
component: () => import('../views/HelpView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/help-notifications",
|
path: '/help-notifications',
|
||||||
name: "help-notifications",
|
name: 'help-notifications',
|
||||||
component: () => import("../views/HelpNotificationsView.vue"),
|
component: () => import('../views/HelpNotificationsView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/help-notification-types",
|
path: '/help-notification-types',
|
||||||
name: "help-notification-types",
|
name: 'help-notification-types',
|
||||||
component: () => import("../views/HelpNotificationTypesView.vue"),
|
component: () => import('../views/HelpNotificationTypesView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/help-onboarding",
|
path: '/help-onboarding',
|
||||||
name: "help-onboarding",
|
name: 'help-onboarding',
|
||||||
component: () => import("../views/HelpOnboardingView.vue"),
|
component: () => import('../views/HelpOnboardingView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: '/',
|
||||||
name: "home",
|
name: 'home',
|
||||||
component: () => import("../views/HomeView.vue"),
|
component: () => import('../views/HomeView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/identity-switcher",
|
path: '/identity-switcher',
|
||||||
name: "identity-switcher",
|
name: 'identity-switcher',
|
||||||
component: () => import("../views/IdentitySwitcherView.vue"),
|
component: () => import('../views/IdentitySwitcherView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/import-account",
|
path: '/import-account',
|
||||||
name: "import-account",
|
name: 'import-account',
|
||||||
component: () => import("../views/ImportAccountView.vue"),
|
component: () => import('../views/ImportAccountView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/import-derive",
|
path: '/import-derive',
|
||||||
name: "import-derive",
|
name: 'import-derive',
|
||||||
component: () => import("../views/ImportDerivedAccountView.vue"),
|
component: () => import('../views/ImportDerivedAccountView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/invite-one",
|
path: '/invite-one',
|
||||||
name: "invite-one",
|
name: 'invite-one',
|
||||||
component: () => import("../views/InviteOneView.vue"),
|
component: () => import('../views/InviteOneView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/invite-one-accept/:jwt?",
|
path: '/invite-one-accept/:jwt?',
|
||||||
name: "InviteOneAcceptView",
|
name: 'InviteOneAcceptView',
|
||||||
component: () => import("../views/InviteOneAcceptView.vue"),
|
component: () => import('../views/InviteOneAcceptView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/logs",
|
path: '/logs',
|
||||||
name: "logs",
|
name: 'logs',
|
||||||
component: () => import("../views/LogView.vue"),
|
component: () => import('../views/LogView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/new-activity",
|
path: '/new-activity',
|
||||||
name: "new-activity",
|
name: 'new-activity',
|
||||||
component: () => import("../views/NewActivityView.vue"),
|
component: () => import('../views/NewActivityView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/new-edit-account",
|
path: '/new-edit-account',
|
||||||
name: "new-edit-account",
|
name: 'new-edit-account',
|
||||||
component: () => import("../views/NewEditAccountView.vue"),
|
component: () => import('../views/NewEditAccountView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/new-edit-project",
|
path: '/new-edit-project',
|
||||||
name: "new-edit-project",
|
name: 'new-edit-project',
|
||||||
component: () => import("../views/NewEditProjectView.vue"),
|
component: () => import('../views/NewEditProjectView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/new-identifier",
|
path: '/new-identifier',
|
||||||
name: "new-identifier",
|
name: 'new-identifier',
|
||||||
component: () => import("../views/NewIdentifierView.vue"),
|
component: () => import('../views/NewIdentifierView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/offer-details/:id?",
|
path: '/offer-details/:id?',
|
||||||
name: "offer-details",
|
name: 'offer-details',
|
||||||
component: () => import("../views/OfferDetailsView.vue"),
|
component: () => import('../views/OfferDetailsView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/onboard-meeting-list",
|
path: '/onboard-meeting-list',
|
||||||
name: "onboard-meeting-list",
|
name: 'onboard-meeting-list',
|
||||||
component: () => import("../views/OnboardMeetingListView.vue"),
|
component: () => import('../views/OnboardMeetingListView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/onboard-meeting-members/:groupId",
|
path: '/onboard-meeting-members/:groupId',
|
||||||
name: "onboard-meeting-members",
|
name: 'onboard-meeting-members',
|
||||||
component: () => import("../views/OnboardMeetingMembersView.vue"),
|
component: () => import('../views/OnboardMeetingMembersView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/onboard-meeting-setup",
|
path: '/onboard-meeting-setup',
|
||||||
name: "onboard-meeting-setup",
|
name: 'onboard-meeting-setup',
|
||||||
component: () => import("../views/OnboardMeetingSetupView.vue"),
|
component: () => import('../views/OnboardMeetingSetupView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/project/:id?",
|
path: '/project/:id?',
|
||||||
name: "project",
|
name: 'project',
|
||||||
component: () => import("../views/ProjectViewView.vue"),
|
component: () => import('../views/ProjectViewView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/projects",
|
path: '/projects',
|
||||||
name: "projects",
|
name: 'projects',
|
||||||
component: () => import("../views/ProjectsView.vue"),
|
component: () => import('../views/ProjectsView.vue'),
|
||||||
beforeEnter: enterOrStart,
|
beforeEnter: enterOrStart
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/quick-action-bvc",
|
path: '/quick-action-bvc',
|
||||||
name: "quick-action-bvc",
|
name: 'quick-action-bvc',
|
||||||
component: () => import("../views/QuickActionBvcView.vue"),
|
component: () => import('../views/QuickActionBvcView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/quick-action-bvc-begin",
|
path: '/quick-action-bvc-begin',
|
||||||
name: "quick-action-bvc-begin",
|
name: 'quick-action-bvc-begin',
|
||||||
component: () => import("../views/QuickActionBvcBeginView.vue"),
|
component: () => import('../views/QuickActionBvcBeginView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/quick-action-bvc-end",
|
path: '/quick-action-bvc-end',
|
||||||
name: "quick-action-bvc-end",
|
name: 'quick-action-bvc-end',
|
||||||
component: () => import("../views/QuickActionBvcEndView.vue"),
|
component: () => import('../views/QuickActionBvcEndView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/recent-offers-to-user",
|
path: '/recent-offers-to-user',
|
||||||
name: "recent-offers-to-user",
|
name: 'recent-offers-to-user',
|
||||||
component: () => import("../views/RecentOffersToUserView.vue"),
|
component: () => import('../views/RecentOffersToUserView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/recent-offers-to-user-projects",
|
path: '/recent-offers-to-user-projects',
|
||||||
name: "recent-offers-to-user-projects",
|
name: 'recent-offers-to-user-projects',
|
||||||
component: () => import("../views/RecentOffersToUserProjectsView.vue"),
|
component: () => import('../views/RecentOffersToUserProjectsView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/scan-contact",
|
path: '/scan-contact',
|
||||||
name: "scan-contact",
|
name: 'scan-contact',
|
||||||
component: () => import("../views/ContactScanView.vue"),
|
component: () => import('../views/ContactScanView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/search-area",
|
path: '/search-area',
|
||||||
name: "search-area",
|
name: 'search-area',
|
||||||
component: () => import("../views/SearchAreaView.vue"),
|
component: () => import('../views/SearchAreaView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/seed-backup",
|
path: '/seed-backup',
|
||||||
name: "seed-backup",
|
name: 'seed-backup',
|
||||||
component: () => import("../views/SeedBackupView.vue"),
|
component: () => import('../views/SeedBackupView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/share-my-contact-info",
|
path: '/share-my-contact-info',
|
||||||
name: "share-my-contact-info",
|
name: 'share-my-contact-info',
|
||||||
component: () => import("../views/ShareMyContactInfoView.vue"),
|
component: () => import('../views/ShareMyContactInfoView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/shared-photo",
|
path: '/shared-photo',
|
||||||
name: "shared-photo",
|
name: 'shared-photo',
|
||||||
component: () => import("../views/SharedPhotoView.vue"),
|
component: () => import('../views/SharedPhotoView.vue')
|
||||||
},
|
},
|
||||||
|
|
||||||
// /share-target is also an endpoint in the service worker
|
// /share-target is also an endpoint in the service worker
|
||||||
|
|
||||||
{
|
{
|
||||||
path: "/start",
|
path: '/start',
|
||||||
name: "start",
|
name: 'start',
|
||||||
component: () => import("../views/StartView.vue"),
|
component: () => import('../views/StartView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/statistics",
|
path: '/statistics',
|
||||||
name: "statistics",
|
name: 'statistics',
|
||||||
component: () => import("../views/StatisticsView.vue"),
|
component: () => import('../views/StatisticsView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/test",
|
path: '/test',
|
||||||
name: "test",
|
name: 'test',
|
||||||
component: () => import("../views/TestView.vue"),
|
component: () => import('../views/TestView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/user-profile/:id?",
|
path: '/user-profile/:id?',
|
||||||
name: "user-profile",
|
name: 'user-profile',
|
||||||
component: () => import("../views/UserProfileView.vue"),
|
component: () => import('../views/UserProfileView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/deep-link-error",
|
path: '/deep-link-error',
|
||||||
name: "deep-link-error",
|
name: 'deep-link-error',
|
||||||
component: () => import("../views/DeepLinkErrorView.vue"),
|
component: () => import('../views/DeepLinkErrorView.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: "Invalid Deep Link",
|
title: 'Invalid Deep Link',
|
||||||
requiresAuth: false,
|
requiresAuth: false
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
];
|
]
|
||||||
|
|
||||||
const isElectron = window.location.protocol === "file:";
|
const isElectron = window.location.protocol === 'file:'
|
||||||
const initialPath = isElectron
|
const initialPath = isElectron
|
||||||
? window.location.pathname.split("/dist-electron/www/")[1] || "/"
|
? window.location.pathname.split('/dist-electron/www/')[1] || '/'
|
||||||
: window.location.pathname;
|
: window.location.pathname
|
||||||
|
|
||||||
const history = isElectron
|
const history = isElectron
|
||||||
? createMemoryHistory() // Memory history for Electron
|
? createMemoryHistory() // Memory history for Electron
|
||||||
: createWebHistory("/"); // Add base path for web apps
|
: createWebHistory('/') // Add base path for web apps
|
||||||
|
|
||||||
/** @type {*} */
|
/** @type {*} */
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history,
|
history,
|
||||||
routes,
|
routes
|
||||||
});
|
})
|
||||||
|
|
||||||
// Replace initial URL to start at `/` if necessary
|
// Replace initial URL to start at `/` if necessary
|
||||||
router.replace(initialPath || "/");
|
router.replace(initialPath || '/')
|
||||||
|
|
||||||
const errorHandler = (
|
const errorHandler = (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
error: any,
|
error: any,
|
||||||
to: RouteLocationNormalized,
|
to: RouteLocationNormalized,
|
||||||
from: RouteLocationNormalized,
|
from: RouteLocationNormalized
|
||||||
) => {
|
) => {
|
||||||
// Handle the error here
|
// Handle the error here
|
||||||
logger.error("Caught in top level error handler:", error, to, from);
|
logger.error('Caught in top level error handler:', error, to, from)
|
||||||
alert("Something is very wrong. Try reloading or restarting the app.");
|
alert('Something is very wrong. Try reloading or restarting the app.')
|
||||||
|
|
||||||
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
||||||
};
|
}
|
||||||
|
|
||||||
router.onError(errorHandler); // Assign the error handler to the router instance
|
router.onError(errorHandler) // Assign the error handler to the router instance
|
||||||
|
|
||||||
// router.beforeEach((to, from, next) => {
|
// router.beforeEach((to, from, next) => {
|
||||||
// console.log("Navigating to view:", to.name);
|
// console.log("Navigating to view:", to.name);
|
||||||
@@ -336,4 +336,4 @@ router.onError(errorHandler); // Assign the error handler to the router instance
|
|||||||
// next();
|
// next();
|
||||||
// });
|
// });
|
||||||
|
|
||||||
export default router;
|
export default router
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
export interface ImageResult {
|
export interface ImageResult {
|
||||||
/** The image data as a Blob object */
|
/** The image data as a Blob object */
|
||||||
blob: Blob;
|
blob: Blob
|
||||||
/** The filename associated with the image */
|
/** The filename associated with the image */
|
||||||
fileName: string;
|
fileName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,17 +15,17 @@ export interface ImageResult {
|
|||||||
*/
|
*/
|
||||||
export interface PlatformCapabilities {
|
export interface PlatformCapabilities {
|
||||||
/** Whether the platform supports native file system access */
|
/** Whether the platform supports native file system access */
|
||||||
hasFileSystem: boolean;
|
hasFileSystem: boolean
|
||||||
/** Whether the platform supports native camera access */
|
/** Whether the platform supports native camera access */
|
||||||
hasCamera: boolean;
|
hasCamera: boolean
|
||||||
/** Whether the platform is a mobile device */
|
/** Whether the platform is a mobile device */
|
||||||
isMobile: boolean;
|
isMobile: boolean
|
||||||
/** Whether the platform is iOS specifically */
|
/** Whether the platform is iOS specifically */
|
||||||
isIOS: boolean;
|
isIOS: boolean
|
||||||
/** Whether the platform supports native file download */
|
/** Whether the platform supports native file download */
|
||||||
hasFileDownload: boolean;
|
hasFileDownload: boolean
|
||||||
/** Whether the platform requires special file handling instructions */
|
/** Whether the platform requires special file handling instructions */
|
||||||
needsFileHandlingInstructions: boolean;
|
needsFileHandlingInstructions: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,7 +39,7 @@ export interface PlatformService {
|
|||||||
* Gets the current platform's capabilities
|
* Gets the current platform's capabilities
|
||||||
* @returns Object describing what features are available on this platform
|
* @returns Object describing what features are available on this platform
|
||||||
*/
|
*/
|
||||||
getCapabilities(): PlatformCapabilities;
|
getCapabilities(): PlatformCapabilities
|
||||||
|
|
||||||
// File system operations
|
// File system operations
|
||||||
/**
|
/**
|
||||||
@@ -47,7 +47,7 @@ export interface PlatformService {
|
|||||||
* @param path - The path to the file to read
|
* @param path - The path to the file to read
|
||||||
* @returns Promise resolving to the file contents as a string
|
* @returns Promise resolving to the file contents as a string
|
||||||
*/
|
*/
|
||||||
readFile(path: string): Promise<string>;
|
readFile(path: string): Promise<string>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes content to a file at the specified path.
|
* Writes content to a file at the specified path.
|
||||||
@@ -55,7 +55,7 @@ export interface PlatformService {
|
|||||||
* @param content - The content to write to the file
|
* @param content - The content to write to the file
|
||||||
* @returns Promise that resolves when the write is complete
|
* @returns Promise that resolves when the write is complete
|
||||||
*/
|
*/
|
||||||
writeFile(path: string, content: string): Promise<void>;
|
writeFile(path: string, content: string): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes content to a file at the specified path and shares it.
|
* Writes content to a file at the specified path and shares it.
|
||||||
@@ -63,41 +63,41 @@ export interface PlatformService {
|
|||||||
* @param content - The content to write to the file
|
* @param content - The content to write to the file
|
||||||
* @returns Promise that resolves when the write is complete
|
* @returns Promise that resolves when the write is complete
|
||||||
*/
|
*/
|
||||||
writeAndShareFile(fileName: string, content: string): Promise<void>;
|
writeAndShareFile(fileName: string, content: string): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a file at the specified path.
|
* Deletes a file at the specified path.
|
||||||
* @param path - The path to the file to delete
|
* @param path - The path to the file to delete
|
||||||
* @returns Promise that resolves when the deletion is complete
|
* @returns Promise that resolves when the deletion is complete
|
||||||
*/
|
*/
|
||||||
deleteFile(path: string): Promise<void>;
|
deleteFile(path: string): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists all files in the specified directory.
|
* Lists all files in the specified directory.
|
||||||
* @param directory - The directory path to list
|
* @param directory - The directory path to list
|
||||||
* @returns Promise resolving to an array of filenames
|
* @returns Promise resolving to an array of filenames
|
||||||
*/
|
*/
|
||||||
listFiles(directory: string): Promise<string[]>;
|
listFiles(directory: string): Promise<string[]>
|
||||||
|
|
||||||
// Camera operations
|
// Camera operations
|
||||||
/**
|
/**
|
||||||
* Activates the device camera to take a picture.
|
* Activates the device camera to take a picture.
|
||||||
* @returns Promise resolving to the captured image result
|
* @returns Promise resolving to the captured image result
|
||||||
*/
|
*/
|
||||||
takePicture(): Promise<ImageResult>;
|
takePicture(): Promise<ImageResult>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a file picker to select an existing image.
|
* Opens a file picker to select an existing image.
|
||||||
* @returns Promise resolving to the selected image result
|
* @returns Promise resolving to the selected image result
|
||||||
*/
|
*/
|
||||||
pickImage(): Promise<ImageResult>;
|
pickImage(): Promise<ImageResult>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles deep link URLs for the application.
|
* Handles deep link URLs for the application.
|
||||||
* @param url - The deep link URL to handle
|
* @param url - The deep link URL to handle
|
||||||
* @returns Promise that resolves when the deep link has been handled
|
* @returns Promise that resolves when the deep link has been handled
|
||||||
*/
|
*/
|
||||||
handleDeepLink(url: string): Promise<void>;
|
handleDeepLink(url: string): Promise<void>
|
||||||
|
|
||||||
// Clipboard operations
|
// Clipboard operations
|
||||||
/**
|
/**
|
||||||
@@ -105,11 +105,11 @@ export interface PlatformService {
|
|||||||
* @param text - The text to write to the clipboard
|
* @param text - The text to write to the clipboard
|
||||||
* @returns Promise that resolves when the write is complete
|
* @returns Promise that resolves when the write is complete
|
||||||
*/
|
*/
|
||||||
writeToClipboard(text: string): Promise<void>;
|
writeToClipboard(text: string): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads text from the system clipboard.
|
* Reads text from the system clipboard.
|
||||||
* @returns Promise resolving to the clipboard text
|
* @returns Promise resolving to the clipboard text
|
||||||
*/
|
*/
|
||||||
readFromClipboard(): Promise<string>;
|
readFromClipboard(): Promise<string>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { PlatformService } from "./PlatformService";
|
import { PlatformService } from './PlatformService'
|
||||||
import { WebPlatformService } from "./platforms/WebPlatformService";
|
import { WebPlatformService } from './platforms/WebPlatformService'
|
||||||
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
import { CapacitorPlatformService } from './platforms/CapacitorPlatformService'
|
||||||
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
import { ElectronPlatformService } from './platforms/ElectronPlatformService'
|
||||||
import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
|
import { PyWebViewPlatformService } from './platforms/PyWebViewPlatformService'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory class for creating platform-specific service implementations.
|
* Factory class for creating platform-specific service implementations.
|
||||||
@@ -22,7 +22,7 @@ import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class PlatformServiceFactory {
|
export class PlatformServiceFactory {
|
||||||
private static instance: PlatformService | null = null;
|
private static instance: PlatformService | null = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets or creates the singleton instance of PlatformService.
|
* Gets or creates the singleton instance of PlatformService.
|
||||||
@@ -32,27 +32,27 @@ export class PlatformServiceFactory {
|
|||||||
*/
|
*/
|
||||||
public static getInstance(): PlatformService {
|
public static getInstance(): PlatformService {
|
||||||
if (PlatformServiceFactory.instance) {
|
if (PlatformServiceFactory.instance) {
|
||||||
return PlatformServiceFactory.instance;
|
return PlatformServiceFactory.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
const platform = process.env.VITE_PLATFORM || "web";
|
const platform = process.env.VITE_PLATFORM || 'web'
|
||||||
|
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case "capacitor":
|
case 'capacitor':
|
||||||
PlatformServiceFactory.instance = new CapacitorPlatformService();
|
PlatformServiceFactory.instance = new CapacitorPlatformService()
|
||||||
break;
|
break
|
||||||
case "electron":
|
case 'electron':
|
||||||
PlatformServiceFactory.instance = new ElectronPlatformService();
|
PlatformServiceFactory.instance = new ElectronPlatformService()
|
||||||
break;
|
break
|
||||||
case "pywebview":
|
case 'pywebview':
|
||||||
PlatformServiceFactory.instance = new PyWebViewPlatformService();
|
PlatformServiceFactory.instance = new PyWebViewPlatformService()
|
||||||
break;
|
break
|
||||||
case "web":
|
case 'web':
|
||||||
default:
|
default:
|
||||||
PlatformServiceFactory.instance = new WebPlatformService();
|
PlatformServiceFactory.instance = new WebPlatformService()
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return PlatformServiceFactory.instance;
|
return PlatformServiceFactory.instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,154 +2,154 @@ import {
|
|||||||
BarcodeScanner,
|
BarcodeScanner,
|
||||||
BarcodeFormat,
|
BarcodeFormat,
|
||||||
LensFacing,
|
LensFacing,
|
||||||
ScanResult,
|
ScanResult
|
||||||
} from "@capacitor-mlkit/barcode-scanning";
|
} from '@capacitor-mlkit/barcode-scanning'
|
||||||
import type { QRScannerService, ScanListener } from "./types";
|
import type { QRScannerService, ScanListener } from './types'
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from '../../utils/logger'
|
||||||
|
|
||||||
export class CapacitorQRScanner implements QRScannerService {
|
export class CapacitorQRScanner implements QRScannerService {
|
||||||
private scanListener: ScanListener | null = null;
|
private scanListener: ScanListener | null = null
|
||||||
private isScanning = false;
|
private isScanning = false
|
||||||
private listenerHandles: Array<() => Promise<void>> = [];
|
private listenerHandles: Array<() => Promise<void>> = []
|
||||||
|
|
||||||
async checkPermissions() {
|
async checkPermissions() {
|
||||||
try {
|
try {
|
||||||
const { camera } = await BarcodeScanner.checkPermissions();
|
const { camera } = await BarcodeScanner.checkPermissions()
|
||||||
return camera === "granted";
|
return camera === 'granted'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error checking camera permissions:", error);
|
logger.error('Error checking camera permissions:', error)
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestPermissions() {
|
async requestPermissions() {
|
||||||
try {
|
try {
|
||||||
const { camera } = await BarcodeScanner.requestPermissions();
|
const { camera } = await BarcodeScanner.requestPermissions()
|
||||||
return camera === "granted";
|
return camera === 'granted'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error requesting camera permissions:", error);
|
logger.error('Error requesting camera permissions:', error)
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async isSupported() {
|
async isSupported() {
|
||||||
try {
|
try {
|
||||||
const { supported } = await BarcodeScanner.isSupported();
|
const { supported } = await BarcodeScanner.isSupported()
|
||||||
return supported;
|
return supported
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error checking barcode scanner support:", error);
|
logger.error('Error checking barcode scanner support:', error)
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startScan() {
|
async startScan() {
|
||||||
if (this.isScanning) {
|
if (this.isScanning) {
|
||||||
logger.warn("Scanner is already active");
|
logger.warn('Scanner is already active')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First register listeners before starting scan
|
// First register listeners before starting scan
|
||||||
await this.registerListeners();
|
await this.registerListeners()
|
||||||
|
|
||||||
this.isScanning = true;
|
this.isScanning = true
|
||||||
await BarcodeScanner.startScan({
|
await BarcodeScanner.startScan({
|
||||||
formats: [BarcodeFormat.QrCode],
|
formats: [BarcodeFormat.QrCode],
|
||||||
lensFacing: LensFacing.Back,
|
lensFacing: LensFacing.Back
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ensure cleanup on error
|
// Ensure cleanup on error
|
||||||
this.isScanning = false;
|
this.isScanning = false
|
||||||
await this.removeListeners();
|
await this.removeListeners()
|
||||||
logger.error("Error starting barcode scan:", error);
|
logger.error('Error starting barcode scan:', error)
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopScan() {
|
async stopScan() {
|
||||||
if (!this.isScanning) {
|
if (!this.isScanning) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First stop the scan
|
// First stop the scan
|
||||||
await BarcodeScanner.stopScan();
|
await BarcodeScanner.stopScan()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error stopping barcode scan:", error);
|
logger.error('Error stopping barcode scan:', error)
|
||||||
} finally {
|
} finally {
|
||||||
// Always cleanup state even if stop fails
|
// Always cleanup state even if stop fails
|
||||||
this.isScanning = false;
|
this.isScanning = false
|
||||||
await this.removeListeners();
|
await this.removeListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async registerListeners() {
|
private async registerListeners() {
|
||||||
// Clear any existing listeners first
|
// Clear any existing listeners first
|
||||||
await this.removeListeners();
|
await this.removeListeners()
|
||||||
|
|
||||||
const scanHandle = await BarcodeScanner.addListener(
|
const scanHandle = await BarcodeScanner.addListener(
|
||||||
"barcodesScanned",
|
'barcodesScanned',
|
||||||
(result: ScanResult) => {
|
(result: ScanResult) => {
|
||||||
if (result.barcodes.length > 0) {
|
if (result.barcodes.length > 0) {
|
||||||
const barcode = result.barcodes[0];
|
const barcode = result.barcodes[0]
|
||||||
if (barcode.rawValue && this.scanListener) {
|
if (barcode.rawValue && this.scanListener) {
|
||||||
this.scanListener.onScan(barcode.rawValue);
|
this.scanListener.onScan(barcode.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
const errorHandle = await BarcodeScanner.addListener(
|
const errorHandle = await BarcodeScanner.addListener(
|
||||||
"scanError",
|
'scanError',
|
||||||
(error) => {
|
(error) => {
|
||||||
logger.error("Scan error:", error);
|
logger.error('Scan error:', error)
|
||||||
if (this.scanListener?.onError) {
|
if (this.scanListener?.onError) {
|
||||||
this.scanListener.onError(
|
this.scanListener.onError(
|
||||||
new Error(error.message || "Unknown scan error"),
|
new Error(error.message || 'Unknown scan error')
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
|
||||||
this.listenerHandles.push(
|
this.listenerHandles.push(
|
||||||
async () => await scanHandle.remove(),
|
async () => await scanHandle.remove(),
|
||||||
async () => await errorHandle.remove(),
|
async () => await errorHandle.remove()
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async removeListeners() {
|
private async removeListeners() {
|
||||||
try {
|
try {
|
||||||
// Remove all registered listener handles
|
// Remove all registered listener handles
|
||||||
await Promise.all(this.listenerHandles.map((handle) => handle()));
|
await Promise.all(this.listenerHandles.map((handle) => handle()))
|
||||||
this.listenerHandles = [];
|
this.listenerHandles = []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error removing listeners:", error);
|
logger.error('Error removing listeners:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addListener(listener: ScanListener) {
|
addListener(listener: ScanListener) {
|
||||||
if (this.scanListener) {
|
if (this.scanListener) {
|
||||||
logger.warn("Scanner listener already exists, removing old listener");
|
logger.warn('Scanner listener already exists, removing old listener')
|
||||||
this.cleanup();
|
this.cleanup()
|
||||||
}
|
}
|
||||||
this.scanListener = listener;
|
this.scanListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup() {
|
async cleanup() {
|
||||||
try {
|
try {
|
||||||
// Stop scan first if active
|
// Stop scan first if active
|
||||||
if (this.isScanning) {
|
if (this.isScanning) {
|
||||||
await this.stopScan();
|
await this.stopScan()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove listeners
|
// Remove listeners
|
||||||
await this.removeListeners();
|
await this.removeListeners()
|
||||||
|
|
||||||
// Clear state
|
// Clear state
|
||||||
this.scanListener = null;
|
this.scanListener = null
|
||||||
this.isScanning = false;
|
this.isScanning = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error during cleanup:", error);
|
logger.error('Error during cleanup:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,88 +1,88 @@
|
|||||||
import {
|
import {
|
||||||
BarcodeScanner,
|
BarcodeScanner,
|
||||||
BarcodeFormat,
|
BarcodeFormat,
|
||||||
LensFacing,
|
LensFacing
|
||||||
} from "@capacitor-mlkit/barcode-scanning";
|
} from '@capacitor-mlkit/barcode-scanning'
|
||||||
import type { PluginListenerHandle } from "@capacitor/core";
|
import type { PluginListenerHandle } from '@capacitor/core'
|
||||||
import { QRScannerService, ScanListener } from "./types";
|
import { QRScannerService, ScanListener } from './types'
|
||||||
|
|
||||||
export class NativeQRScanner implements QRScannerService {
|
export class NativeQRScanner implements QRScannerService {
|
||||||
private scanListener: ScanListener | null = null;
|
private scanListener: ScanListener | null = null
|
||||||
private isScanning = false;
|
private isScanning = false
|
||||||
private listenerHandle: PluginListenerHandle | null = null;
|
private listenerHandle: PluginListenerHandle | null = null
|
||||||
|
|
||||||
async checkPermissions(): Promise<boolean> {
|
async checkPermissions(): Promise<boolean> {
|
||||||
const { camera } = await BarcodeScanner.checkPermissions();
|
const { camera } = await BarcodeScanner.checkPermissions()
|
||||||
return camera === "granted";
|
return camera === 'granted'
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestPermissions(): Promise<boolean> {
|
async requestPermissions(): Promise<boolean> {
|
||||||
const { camera } = await BarcodeScanner.requestPermissions();
|
const { camera } = await BarcodeScanner.requestPermissions()
|
||||||
return camera === "granted";
|
return camera === 'granted'
|
||||||
}
|
}
|
||||||
|
|
||||||
async isSupported(): Promise<boolean> {
|
async isSupported(): Promise<boolean> {
|
||||||
const { supported } = await BarcodeScanner.isSupported();
|
const { supported } = await BarcodeScanner.isSupported()
|
||||||
return supported;
|
return supported
|
||||||
}
|
}
|
||||||
|
|
||||||
async startScan(): Promise<void> {
|
async startScan(): Promise<void> {
|
||||||
if (this.isScanning) {
|
if (this.isScanning) {
|
||||||
throw new Error("Scanner is already running");
|
throw new Error('Scanner is already running')
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.isScanning = true;
|
this.isScanning = true
|
||||||
await BarcodeScanner.startScan({
|
await BarcodeScanner.startScan({
|
||||||
formats: [BarcodeFormat.QrCode],
|
formats: [BarcodeFormat.QrCode],
|
||||||
lensFacing: LensFacing.Back,
|
lensFacing: LensFacing.Back
|
||||||
});
|
})
|
||||||
|
|
||||||
this.listenerHandle = await BarcodeScanner.addListener(
|
this.listenerHandle = await BarcodeScanner.addListener(
|
||||||
"barcodesScanned",
|
'barcodesScanned',
|
||||||
async (result) => {
|
async (result) => {
|
||||||
if (result.barcodes.length > 0 && this.scanListener) {
|
if (result.barcodes.length > 0 && this.scanListener) {
|
||||||
const barcode = result.barcodes[0];
|
const barcode = result.barcodes[0]
|
||||||
this.scanListener.onScan(barcode.rawValue);
|
this.scanListener.onScan(barcode.rawValue)
|
||||||
await this.stopScan();
|
await this.stopScan()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.isScanning = false;
|
this.isScanning = false
|
||||||
if (this.scanListener?.onError) {
|
if (this.scanListener?.onError) {
|
||||||
this.scanListener.onError(new Error(String(error)));
|
this.scanListener.onError(new Error(String(error)))
|
||||||
}
|
}
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopScan(): Promise<void> {
|
async stopScan(): Promise<void> {
|
||||||
if (!this.isScanning) {
|
if (!this.isScanning) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await BarcodeScanner.stopScan();
|
await BarcodeScanner.stopScan()
|
||||||
this.isScanning = false;
|
this.isScanning = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.scanListener?.onError) {
|
if (this.scanListener?.onError) {
|
||||||
this.scanListener.onError(new Error(String(error)));
|
this.scanListener.onError(new Error(String(error)))
|
||||||
}
|
}
|
||||||
throw error;
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addListener(listener: ScanListener): void {
|
addListener(listener: ScanListener): void {
|
||||||
this.scanListener = listener;
|
this.scanListener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
await this.stopScan();
|
await this.stopScan()
|
||||||
if (this.listenerHandle) {
|
if (this.listenerHandle) {
|
||||||
await this.listenerHandle.remove();
|
await this.listenerHandle.remove()
|
||||||
this.listenerHandle = null;
|
this.listenerHandle = null
|
||||||
}
|
}
|
||||||
this.scanListener = null;
|
this.scanListener = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from '@capacitor/core'
|
||||||
import { CapacitorQRScanner } from "./CapacitorQRScanner";
|
import { CapacitorQRScanner } from './CapacitorQRScanner'
|
||||||
import { WebQRScanner } from "./WebQRScanner";
|
import type { QRScannerService } from './types'
|
||||||
import type { QRScannerService } from "./types";
|
import { logger } from '../../utils/logger'
|
||||||
import { logger } from "../../utils/logger";
|
import { WebDialogQRScanner } from './WebDialogQRScanner'
|
||||||
|
|
||||||
|
// Import platform-specific flags from Vite config
|
||||||
|
declare const __USE_QR_READER__: boolean
|
||||||
|
declare const __IS_MOBILE__: boolean
|
||||||
|
|
||||||
export class QRScannerFactory {
|
export class QRScannerFactory {
|
||||||
private static instance: QRScannerService | null = null;
|
private static instance: QRScannerService | null = null
|
||||||
|
|
||||||
static getInstance(): QRScannerService {
|
static getInstance(): QRScannerService {
|
||||||
if (!this.instance) {
|
if (!this.instance) {
|
||||||
if (Capacitor.isNativePlatform()) {
|
// Use platform-specific flags for more accurate detection
|
||||||
logger.log("Creating native QR scanner instance");
|
if (__IS_MOBILE__ || Capacitor.isNativePlatform()) {
|
||||||
this.instance = new CapacitorQRScanner();
|
logger.log('Creating native QR scanner instance')
|
||||||
|
this.instance = new CapacitorQRScanner()
|
||||||
|
} else if (__USE_QR_READER__) {
|
||||||
|
logger.log('Creating web QR scanner instance')
|
||||||
|
this.instance = new WebDialogQRScanner()
|
||||||
} else {
|
} else {
|
||||||
logger.log("Creating web QR scanner instance");
|
throw new Error(
|
||||||
this.instance = new WebQRScanner();
|
'No QR scanner implementation available for this platform'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.instance;
|
return this.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
static async cleanup() {
|
static async cleanup() {
|
||||||
if (this.instance) {
|
if (this.instance) {
|
||||||
await this.instance.cleanup();
|
await this.instance.cleanup()
|
||||||
this.instance = null;
|
this.instance = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
src/services/QRScanner/WebDialogQRScanner.ts
Normal file
90
src/services/QRScanner/WebDialogQRScanner.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import type { QRScannerService, ScanListener } from './types'
|
||||||
|
import QRScannerDialog from '../../components/QRScannerDialog.vue'
|
||||||
|
import { createApp, type App } from 'vue'
|
||||||
|
import { logger } from '../../utils/logger'
|
||||||
|
|
||||||
|
// Import platform-specific flags from Vite config
|
||||||
|
declare const __USE_QR_READER__: boolean
|
||||||
|
|
||||||
|
export class WebDialogQRScanner implements QRScannerService {
|
||||||
|
private dialogInstance: App | null = null
|
||||||
|
private dialogComponent: InstanceType<typeof QRScannerDialog> | null = null
|
||||||
|
private scanListener: ScanListener | null = null
|
||||||
|
|
||||||
|
async checkPermissions(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const permissions = await navigator.permissions.query({
|
||||||
|
name: 'camera' as PermissionName
|
||||||
|
})
|
||||||
|
return permissions.state === 'granted'
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error checking camera permissions:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestPermissions(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: { facingMode: 'environment' }
|
||||||
|
})
|
||||||
|
stream.getTracks().forEach((track) => track.stop())
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error requesting camera permissions:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isSupported(): Promise<boolean> {
|
||||||
|
if (!__USE_QR_READER__) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)
|
||||||
|
}
|
||||||
|
|
||||||
|
async startScan(): Promise<void> {
|
||||||
|
if (!__USE_QR_READER__) {
|
||||||
|
throw new Error('Web QR scanner is not supported on this platform')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.dialogInstance) {
|
||||||
|
const div = document.createElement('div')
|
||||||
|
document.body.appendChild(div)
|
||||||
|
|
||||||
|
this.dialogInstance = createApp(QRScannerDialog)
|
||||||
|
this.dialogComponent = this.dialogInstance.mount(div) as InstanceType<
|
||||||
|
typeof QRScannerDialog
|
||||||
|
>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dialogComponent && this.scanListener) {
|
||||||
|
this.dialogComponent.open((result: string) => {
|
||||||
|
if (this.scanListener) {
|
||||||
|
this.scanListener.onScan(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopScan(): Promise<void> {
|
||||||
|
if (this.dialogComponent) {
|
||||||
|
this.dialogComponent.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addListener(listener: ScanListener): void {
|
||||||
|
this.scanListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanup(): Promise<void> {
|
||||||
|
if (this.dialogComponent) {
|
||||||
|
this.dialogComponent.close()
|
||||||
|
}
|
||||||
|
if (this.dialogInstance) {
|
||||||
|
this.dialogInstance.unmount()
|
||||||
|
this.dialogInstance = null
|
||||||
|
this.dialogComponent = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import jsQR from "jsqr";
|
|
||||||
import { QRScannerService, ScanListener } from "./types";
|
|
||||||
import { logger } from "../../utils/logger";
|
|
||||||
|
|
||||||
export class WebQRScanner implements QRScannerService {
|
|
||||||
private video: HTMLVideoElement | null = null;
|
|
||||||
private canvas: HTMLCanvasElement | null = null;
|
|
||||||
private context: CanvasRenderingContext2D | null = null;
|
|
||||||
private animationFrameId: number | null = null;
|
|
||||||
private scanListener: ScanListener | null = null;
|
|
||||||
private isScanning = false;
|
|
||||||
private mediaStream: MediaStream | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.video = document.createElement("video");
|
|
||||||
this.canvas = document.createElement("canvas");
|
|
||||||
this.context = this.canvas.getContext("2d");
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkPermissions(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const permissions = await navigator.permissions.query({
|
|
||||||
name: "camera" as PermissionName,
|
|
||||||
});
|
|
||||||
return permissions.state === "granted";
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking camera permissions:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async requestPermissions(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: { facingMode: "environment" },
|
|
||||||
});
|
|
||||||
stream.getTracks().forEach((track) => track.stop());
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error requesting camera permissions:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async isSupported(): Promise<boolean> {
|
|
||||||
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
|
|
||||||
}
|
|
||||||
|
|
||||||
async startScan(): Promise<void> {
|
|
||||||
if (this.isScanning || !this.video || !this.canvas || !this.context) {
|
|
||||||
throw new Error("Scanner is already running or not properly initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
this.mediaStream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
video: { facingMode: "environment" },
|
|
||||||
});
|
|
||||||
|
|
||||||
this.video.srcObject = this.mediaStream;
|
|
||||||
this.video.setAttribute("playsinline", "true");
|
|
||||||
await this.video.play();
|
|
||||||
|
|
||||||
this.canvas.width = this.video.videoWidth;
|
|
||||||
this.canvas.height = this.video.videoHeight;
|
|
||||||
|
|
||||||
this.isScanning = true;
|
|
||||||
this.scanFrame();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error starting scan:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private scanFrame = () => {
|
|
||||||
if (
|
|
||||||
!this.isScanning ||
|
|
||||||
!this.video ||
|
|
||||||
!this.canvas ||
|
|
||||||
!this.context ||
|
|
||||||
!this.scanListener
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
|
|
||||||
this.canvas.width = this.video.videoWidth;
|
|
||||||
this.canvas.height = this.video.videoHeight;
|
|
||||||
this.context.drawImage(
|
|
||||||
this.video,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
this.canvas.width,
|
|
||||||
this.canvas.height,
|
|
||||||
);
|
|
||||||
|
|
||||||
const imageData = this.context.getImageData(
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
this.canvas.width,
|
|
||||||
this.canvas.height,
|
|
||||||
);
|
|
||||||
const code = jsQR(imageData.data, imageData.width, imageData.height, {
|
|
||||||
inversionAttempts: "dontInvert",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (code) {
|
|
||||||
this.scanListener.onScan(code.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.animationFrameId = requestAnimationFrame(this.scanFrame);
|
|
||||||
};
|
|
||||||
|
|
||||||
async stopScan(): Promise<void> {
|
|
||||||
this.isScanning = false;
|
|
||||||
if (this.animationFrameId) {
|
|
||||||
cancelAnimationFrame(this.animationFrameId);
|
|
||||||
this.animationFrameId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mediaStream) {
|
|
||||||
this.mediaStream.getTracks().forEach((track) => track.stop());
|
|
||||||
this.mediaStream = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.video) {
|
|
||||||
this.video.srcObject = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addListener(listener: ScanListener): void {
|
|
||||||
this.scanListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
async cleanup(): Promise<void> {
|
|
||||||
await this.stopScan();
|
|
||||||
this.scanListener = null;
|
|
||||||
this.video = null;
|
|
||||||
this.canvas = null;
|
|
||||||
this.context = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,29 @@
|
|||||||
export interface ScanResult {
|
export interface ScanResult {
|
||||||
rawValue: string;
|
rawValue: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QRScannerState {
|
export interface QRScannerState {
|
||||||
isSupported: boolean;
|
isSupported: boolean
|
||||||
granted: boolean;
|
granted: boolean
|
||||||
denied: boolean;
|
denied: boolean
|
||||||
isProcessing: boolean;
|
isProcessing: boolean
|
||||||
processingStatus: string;
|
processingStatus: string
|
||||||
processingDetails: string;
|
processingDetails: string
|
||||||
error: string;
|
error: string
|
||||||
status: string;
|
status: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScanListener {
|
export interface ScanListener {
|
||||||
onScan: (result: string) => void;
|
onScan: (result: string) => void
|
||||||
onError?: (error: Error) => void;
|
onError?: (error: Error) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QRScannerService {
|
export interface QRScannerService {
|
||||||
checkPermissions(): Promise<boolean>;
|
checkPermissions(): Promise<boolean>
|
||||||
requestPermissions(): Promise<boolean>;
|
requestPermissions(): Promise<boolean>
|
||||||
isSupported(): Promise<boolean>;
|
isSupported(): Promise<boolean>
|
||||||
startScan(): Promise<void>;
|
startScan(): Promise<void>
|
||||||
stopScan(): Promise<void>;
|
stopScan(): Promise<void>
|
||||||
addListener(listener: ScanListener): void;
|
addListener(listener: ScanListener): void
|
||||||
cleanup(): Promise<void>;
|
cleanup(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
* @module api
|
* @module api
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from 'axios'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../utils/logger'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles API errors with platform-specific logging and error processing.
|
* Handles API errors with platform-specific logging and error processing.
|
||||||
@@ -36,7 +36,7 @@ import { logger } from "../utils/logger";
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||||
if (process.env.VITE_PLATFORM === "capacitor") {
|
if (process.env.VITE_PLATFORM === 'capacitor') {
|
||||||
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
@@ -44,16 +44,16 @@ export const handleApiError = (error: AxiosError, endpoint: string) => {
|
|||||||
config: {
|
config: {
|
||||||
url: error.config?.url,
|
url: error.config?.url,
|
||||||
method: error.config?.method,
|
method: error.config?.method,
|
||||||
headers: error.config?.headers,
|
headers: error.config?.headers
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Specific handling for rate limits
|
// Specific handling for rate limits
|
||||||
if (error.response?.status === 400) {
|
if (error.response?.status === 400) {
|
||||||
logger.warn(`[Rate Limit] ${endpoint}`);
|
logger.warn(`[Rate Limit] ${endpoint}`)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -45,29 +45,29 @@
|
|||||||
* await handler.handleDeepLink("timesafari://claim/123?view=details");
|
* await handler.handleDeepLink("timesafari://claim/123?view=details");
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "vue-router";
|
import { Router } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
deepLinkSchemas,
|
deepLinkSchemas,
|
||||||
baseUrlSchema,
|
baseUrlSchema,
|
||||||
routeSchema,
|
routeSchema,
|
||||||
DeepLinkRoute,
|
DeepLinkRoute
|
||||||
} from "../types/deepLinks";
|
} from '../types/deepLinks'
|
||||||
import { logConsoleAndDb } from "../db";
|
import { logConsoleAndDb } from '../db'
|
||||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
import type { DeepLinkError } from '../interfaces/deepLinks'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles processing and routing of deep links in the application.
|
* Handles processing and routing of deep links in the application.
|
||||||
* Provides validation, error handling, and routing for deep link URLs.
|
* Provides validation, error handling, and routing for deep link URLs.
|
||||||
*/
|
*/
|
||||||
export class DeepLinkHandler {
|
export class DeepLinkHandler {
|
||||||
private router: Router;
|
private router: Router
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new DeepLinkHandler instance.
|
* Creates a new DeepLinkHandler instance.
|
||||||
* @param router - Vue Router instance for navigation
|
* @param router - Vue Router instance for navigation
|
||||||
*/
|
*/
|
||||||
constructor(router: Router) {
|
constructor(router: Router) {
|
||||||
this.router = router;
|
this.router = router
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,33 +79,33 @@ export class DeepLinkHandler {
|
|||||||
* @returns Parsed URL components (path, params, query)
|
* @returns Parsed URL components (path, params, query)
|
||||||
*/
|
*/
|
||||||
private parseDeepLink(url: string) {
|
private parseDeepLink(url: string) {
|
||||||
const parts = url.split("://");
|
const parts = url.split('://')
|
||||||
if (parts.length !== 2) {
|
if (parts.length !== 2) {
|
||||||
throw { code: "INVALID_URL", message: "Invalid URL format" };
|
throw { code: 'INVALID_URL', message: 'Invalid URL format' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate base URL structure
|
// Validate base URL structure
|
||||||
baseUrlSchema.parse({
|
baseUrlSchema.parse({
|
||||||
scheme: parts[0],
|
scheme: parts[0],
|
||||||
path: parts[1],
|
path: parts[1],
|
||||||
queryParams: {}, // Will be populated below
|
queryParams: {} // Will be populated below
|
||||||
});
|
})
|
||||||
|
|
||||||
const [path, queryString] = parts[1].split("?");
|
const [path, queryString] = parts[1].split('?')
|
||||||
const [routePath, param] = path.split("/");
|
const [routePath, param] = path.split('/')
|
||||||
|
|
||||||
const query: Record<string, string> = {};
|
const query: Record<string, string> = {}
|
||||||
if (queryString) {
|
if (queryString) {
|
||||||
new URLSearchParams(queryString).forEach((value, key) => {
|
new URLSearchParams(queryString).forEach((value, key) => {
|
||||||
query[key] = value;
|
query[key] = value
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: routePath,
|
path: routePath,
|
||||||
params: param ? { id: param } : {},
|
params: param ? { id: param } : {},
|
||||||
query,
|
query
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,25 +117,25 @@ export class DeepLinkHandler {
|
|||||||
*/
|
*/
|
||||||
async handleDeepLink(url: string): Promise<void> {
|
async handleDeepLink(url: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
logConsoleAndDb('[DeepLink] Processing URL: ' + url, false)
|
||||||
const { path, params, query } = this.parseDeepLink(url);
|
const { path, params, query } = this.parseDeepLink(url)
|
||||||
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
||||||
const sanitizedParams = Object.fromEntries(
|
const sanitizedParams = Object.fromEntries(
|
||||||
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
Object.entries(params).map(([key, value]) => [key, value ?? ''])
|
||||||
);
|
)
|
||||||
await this.validateAndRoute(path, sanitizedParams, query);
|
await this.validateAndRoute(path, sanitizedParams, query)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const deepLinkError = error as DeepLinkError;
|
const deepLinkError = error as DeepLinkError
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
||||||
true,
|
true
|
||||||
);
|
)
|
||||||
|
|
||||||
throw {
|
throw {
|
||||||
code: deepLinkError.code || "UNKNOWN_ERROR",
|
code: deepLinkError.code || 'UNKNOWN_ERROR',
|
||||||
message: deepLinkError.message,
|
message: deepLinkError.message,
|
||||||
details: deepLinkError.details,
|
details: deepLinkError.details
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,80 +151,80 @@ export class DeepLinkHandler {
|
|||||||
private async validateAndRoute(
|
private async validateAndRoute(
|
||||||
path: string,
|
path: string,
|
||||||
params: Record<string, string>,
|
params: Record<string, string>,
|
||||||
query: Record<string, string>,
|
query: Record<string, string>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const routeMap: Record<string, string> = {
|
const routeMap: Record<string, string> = {
|
||||||
"user-profile": "user-profile",
|
'user-profile': 'user-profile',
|
||||||
"project-details": "project-details",
|
'project-details': 'project-details',
|
||||||
"onboard-meeting-setup": "onboard-meeting-setup",
|
'onboard-meeting-setup': 'onboard-meeting-setup',
|
||||||
"invite-one-accept": "invite-one-accept",
|
'invite-one-accept': 'invite-one-accept',
|
||||||
"contact-import": "contact-import",
|
'contact-import': 'contact-import',
|
||||||
"confirm-gift": "confirm-gift",
|
'confirm-gift': 'confirm-gift',
|
||||||
claim: "claim",
|
claim: 'claim',
|
||||||
"claim-cert": "claim-cert",
|
'claim-cert': 'claim-cert',
|
||||||
"claim-add-raw": "claim-add-raw",
|
'claim-add-raw': 'claim-add-raw',
|
||||||
"contact-edit": "contact-edit",
|
'contact-edit': 'contact-edit',
|
||||||
contacts: "contacts",
|
contacts: 'contacts',
|
||||||
did: "did",
|
did: 'did'
|
||||||
};
|
}
|
||||||
|
|
||||||
// First try to validate the route path
|
// First try to validate the route path
|
||||||
let routeName: string;
|
let routeName: string
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate route exists
|
// Validate route exists
|
||||||
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
|
const validRoute = routeSchema.parse(path) as DeepLinkRoute
|
||||||
routeName = routeMap[validRoute];
|
routeName = routeMap[validRoute]
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log the invalid route attempt
|
// Log the invalid route attempt
|
||||||
logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true);
|
logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true)
|
||||||
|
|
||||||
// Redirect to error page with information about the invalid link
|
// Redirect to error page with information about the invalid link
|
||||||
await this.router.replace({
|
await this.router.replace({
|
||||||
name: "deep-link-error",
|
name: 'deep-link-error',
|
||||||
query: {
|
query: {
|
||||||
originalPath: path,
|
originalPath: path,
|
||||||
errorCode: "INVALID_ROUTE",
|
errorCode: 'INVALID_ROUTE',
|
||||||
message: `The link you followed (${path}) is not supported`,
|
message: `The link you followed (${path}) is not supported`
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
throw {
|
throw {
|
||||||
code: "INVALID_ROUTE",
|
code: 'INVALID_ROUTE',
|
||||||
message: `Unsupported route: ${path}`,
|
message: `Unsupported route: ${path}`
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with parameter validation as before...
|
// Continue with parameter validation as before...
|
||||||
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
|
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const validatedParams = await schema.parseAsync({
|
const validatedParams = await schema.parseAsync({
|
||||||
...params,
|
...params,
|
||||||
...query,
|
...query
|
||||||
});
|
})
|
||||||
|
|
||||||
await this.router.replace({
|
await this.router.replace({
|
||||||
name: routeName,
|
name: routeName,
|
||||||
params: validatedParams,
|
params: validatedParams,
|
||||||
query,
|
query
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// For parameter validation errors, provide specific error feedback
|
// For parameter validation errors, provide specific error feedback
|
||||||
await this.router.replace({
|
await this.router.replace({
|
||||||
name: "deep-link-error",
|
name: 'deep-link-error',
|
||||||
query: {
|
query: {
|
||||||
originalPath: path,
|
originalPath: path,
|
||||||
errorCode: "INVALID_PARAMETERS",
|
errorCode: 'INVALID_PARAMETERS',
|
||||||
message: `The link parameters are invalid: ${(error as Error).message}`,
|
message: `The link parameters are invalid: ${(error as Error).message}`
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
throw {
|
throw {
|
||||||
code: "INVALID_PARAMETERS",
|
code: 'INVALID_PARAMETERS',
|
||||||
message: (error as Error).message,
|
message: (error as Error).message,
|
||||||
details: error,
|
details: error
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
* @module plan
|
* @module plan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from 'axios'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../utils/logger'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response interface for plan loading operations.
|
* Response interface for plan loading operations.
|
||||||
@@ -14,13 +14,13 @@ import { logger } from "../utils/logger";
|
|||||||
*/
|
*/
|
||||||
interface PlanResponse {
|
interface PlanResponse {
|
||||||
/** The response data payload */
|
/** The response data payload */
|
||||||
data?: unknown;
|
data?: unknown
|
||||||
/** HTTP status code of the response */
|
/** HTTP status code of the response */
|
||||||
status?: number;
|
status?: number
|
||||||
/** Error message in case of failure */
|
/** Error message in case of failure */
|
||||||
error?: string;
|
error?: string
|
||||||
/** Response headers */
|
/** Response headers */
|
||||||
headers?: unknown;
|
headers?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,23 +52,23 @@ interface PlanResponse {
|
|||||||
*/
|
*/
|
||||||
export const loadPlanWithRetry = async (
|
export const loadPlanWithRetry = async (
|
||||||
handle: string,
|
handle: string,
|
||||||
retries = 3,
|
retries = 3
|
||||||
): Promise<PlanResponse> => {
|
): Promise<PlanResponse> => {
|
||||||
try {
|
try {
|
||||||
logger.log(`[Plan Service] Loading plan ${handle}, attempt 1/${retries}`);
|
logger.log(`[Plan Service] Loading plan ${handle}, attempt 1/${retries}`)
|
||||||
logger.log(
|
logger.log(
|
||||||
`[Plan Service] Context: Deep link handle=${handle}, isClaimFlow=${handle.includes("claim")}`,
|
`[Plan Service] Context: Deep link handle=${handle}, isClaimFlow=${handle.includes('claim')}`
|
||||||
);
|
)
|
||||||
|
|
||||||
// Different endpoint if this is a claim flow
|
// Different endpoint if this is a claim flow
|
||||||
const response = await loadPlan(handle);
|
const response = await loadPlan(handle)
|
||||||
logger.log(`[Plan Service] Plan ${handle} loaded successfully:`, {
|
logger.log(`[Plan Service] Plan ${handle} loaded successfully:`, {
|
||||||
status: response?.status,
|
status: response?.status,
|
||||||
headers: response?.headers,
|
headers: response?.headers,
|
||||||
data: response?.data,
|
data: response?.data
|
||||||
});
|
})
|
||||||
|
|
||||||
return response;
|
return response
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error(`[Plan Service] Error loading plan ${handle}:`, {
|
logger.error(`[Plan Service] Error loading plan ${handle}:`, {
|
||||||
message: (error as Error).message,
|
message: (error as Error).message,
|
||||||
@@ -82,24 +82,24 @@ export const loadPlanWithRetry = async (
|
|||||||
url: (error as { config?: { url?: string } })?.config?.url,
|
url: (error as { config?: { url?: string } })?.config?.url,
|
||||||
method: (error as { config?: { method?: string } })?.config?.method,
|
method: (error as { config?: { method?: string } })?.config?.method,
|
||||||
baseURL: (error as { config?: { baseURL?: string } })?.config?.baseURL,
|
baseURL: (error as { config?: { baseURL?: string } })?.config?.baseURL,
|
||||||
headers: (error as { config?: { headers?: unknown } })?.config?.headers,
|
headers: (error as { config?: { headers?: unknown } })?.config?.headers
|
||||||
},
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
if (retries > 1) {
|
if (retries > 1) {
|
||||||
logger.log(
|
logger.log(
|
||||||
`[Plan Service] Retrying plan ${handle}, ${retries - 1} attempts remaining`,
|
`[Plan Service] Retrying plan ${handle}, ${retries - 1} attempts remaining`
|
||||||
);
|
)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
return loadPlanWithRetry(handle, retries - 1);
|
return loadPlanWithRetry(handle, retries - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error: `Failed to load plan ${handle} after ${4 - retries} attempts: ${(error as Error).message}`,
|
error: `Failed to load plan ${handle} after ${4 - retries} attempts: ${(error as Error).message}`,
|
||||||
status: (error as { response?: { status?: number } })?.response?.status,
|
status: (error as { response?: { status?: number } })?.response?.status
|
||||||
};
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a single API request to load a plan or claim.
|
* Makes a single API request to load a plan or claim.
|
||||||
@@ -118,23 +118,23 @@ export const loadPlanWithRetry = async (
|
|||||||
* - Plans: /api/plans/{handle}
|
* - Plans: /api/plans/{handle}
|
||||||
*/
|
*/
|
||||||
export const loadPlan = async (handle: string): Promise<PlanResponse> => {
|
export const loadPlan = async (handle: string): Promise<PlanResponse> => {
|
||||||
logger.log(`[Plan Service] Making API request for plan ${handle}`);
|
logger.log(`[Plan Service] Making API request for plan ${handle}`)
|
||||||
|
|
||||||
const endpoint = handle.includes("claim")
|
const endpoint = handle.includes('claim')
|
||||||
? `/api/claims/${handle}`
|
? `/api/claims/${handle}`
|
||||||
: `/api/plans/${handle}`;
|
: `/api/plans/${handle}`
|
||||||
|
|
||||||
logger.log(`[Plan Service] Using endpoint: ${endpoint}`);
|
logger.log(`[Plan Service] Using endpoint: ${endpoint}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(endpoint);
|
const response = await axios.get(endpoint)
|
||||||
return response;
|
return response
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error(`[Plan Service] API request failed for ${handle}:`, {
|
logger.error(`[Plan Service] API request failed for ${handle}:`, {
|
||||||
endpoint,
|
endpoint,
|
||||||
error: (error as Error).message,
|
error: (error as Error).message,
|
||||||
response: (error as { response?: { data?: unknown } })?.response?.data,
|
response: (error as { response?: { data?: unknown } })?.response?.data
|
||||||
});
|
})
|
||||||
throw error;
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
ImageResult,
|
ImageResult,
|
||||||
PlatformService,
|
PlatformService,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities
|
||||||
} from "../PlatformService";
|
} from '../PlatformService'
|
||||||
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem'
|
||||||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'
|
||||||
import { Share } from "@capacitor/share";
|
import { Share } from '@capacitor/share'
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from '../../utils/logger'
|
||||||
import { Clipboard } from "@capacitor/clipboard";
|
import { Clipboard } from '@capacitor/clipboard'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service implementation for Capacitor (mobile) platform.
|
* Platform service implementation for Capacitor (mobile) platform.
|
||||||
@@ -28,8 +28,8 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
isMobile: true,
|
isMobile: true,
|
||||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||||
hasFileDownload: false,
|
hasFileDownload: false,
|
||||||
needsFileHandlingInstructions: true,
|
needsFileHandlingInstructions: true
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,102 +40,102 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
private async checkStoragePermissions(): Promise<void> {
|
private async checkStoragePermissions(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const logData = {
|
const logData = {
|
||||||
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
|
platform: this.getCapabilities().isIOS ? 'iOS' : 'Android',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
};
|
}
|
||||||
logger.log(
|
logger.log(
|
||||||
"Checking storage permissions",
|
'Checking storage permissions',
|
||||||
JSON.stringify(logData, null, 2),
|
JSON.stringify(logData, null, 2)
|
||||||
);
|
)
|
||||||
|
|
||||||
if (this.getCapabilities().isIOS) {
|
if (this.getCapabilities().isIOS) {
|
||||||
// iOS uses different permission model
|
// iOS uses different permission model
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to access a test directory to check permissions
|
// Try to access a test directory to check permissions
|
||||||
try {
|
try {
|
||||||
await Filesystem.stat({
|
await Filesystem.stat({
|
||||||
path: "/storage/emulated/0/Download",
|
path: '/storage/emulated/0/Download',
|
||||||
directory: Directory.Documents,
|
directory: Directory.Documents
|
||||||
});
|
})
|
||||||
logger.log(
|
logger.log(
|
||||||
"Storage permissions already granted",
|
'Storage permissions already granted',
|
||||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2)
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const err = error as Error;
|
const err = error as Error
|
||||||
const errorLogData = {
|
const errorLogData = {
|
||||||
error: {
|
error: {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
name: err.name,
|
name: err.name,
|
||||||
stack: err.stack,
|
stack: err.stack
|
||||||
},
|
},
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
};
|
}
|
||||||
|
|
||||||
// "File does not exist" is expected and not a permission error
|
// "File does not exist" is expected and not a permission error
|
||||||
if (err.message === "File does not exist") {
|
if (err.message === 'File does not exist') {
|
||||||
logger.log(
|
logger.log(
|
||||||
"Directory does not exist (expected), proceeding with write",
|
'Directory does not exist (expected), proceeding with write',
|
||||||
JSON.stringify(errorLogData, null, 2),
|
JSON.stringify(errorLogData, null, 2)
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for actual permission errors
|
// Check for actual permission errors
|
||||||
if (
|
if (
|
||||||
err.message.includes("permission") ||
|
err.message.includes('permission') ||
|
||||||
err.message.includes("access")
|
err.message.includes('access')
|
||||||
) {
|
) {
|
||||||
logger.log(
|
logger.log(
|
||||||
"Permission check failed, requesting permissions",
|
'Permission check failed, requesting permissions',
|
||||||
JSON.stringify(errorLogData, null, 2),
|
JSON.stringify(errorLogData, null, 2)
|
||||||
);
|
)
|
||||||
|
|
||||||
// The Filesystem plugin will automatically request permissions when needed
|
// The Filesystem plugin will automatically request permissions when needed
|
||||||
// We just need to try the operation again
|
// We just need to try the operation again
|
||||||
try {
|
try {
|
||||||
await Filesystem.stat({
|
await Filesystem.stat({
|
||||||
path: "/storage/emulated/0/Download",
|
path: '/storage/emulated/0/Download',
|
||||||
directory: Directory.Documents,
|
directory: Directory.Documents
|
||||||
});
|
})
|
||||||
logger.log(
|
logger.log(
|
||||||
"Storage permissions granted after request",
|
'Storage permissions granted after request',
|
||||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2)
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
} catch (retryError: unknown) {
|
} catch (retryError: unknown) {
|
||||||
const retryErr = retryError as Error;
|
const retryErr = retryError as Error
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to obtain storage permissions: ${retryErr.message}`,
|
`Failed to obtain storage permissions: ${retryErr.message}`
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For any other error, log it but don't treat as permission error
|
// For any other error, log it but don't treat as permission error
|
||||||
logger.log(
|
logger.log(
|
||||||
"Unexpected error during permission check",
|
'Unexpected error during permission check',
|
||||||
JSON.stringify(errorLogData, null, 2),
|
JSON.stringify(errorLogData, null, 2)
|
||||||
);
|
)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const err = error as Error;
|
const err = error as Error
|
||||||
const errorLogData = {
|
const errorLogData = {
|
||||||
error: {
|
error: {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
name: err.name,
|
name: err.name,
|
||||||
stack: err.stack,
|
stack: err.stack
|
||||||
},
|
},
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
};
|
}
|
||||||
logger.error(
|
logger.error(
|
||||||
"Error checking/requesting permissions",
|
'Error checking/requesting permissions',
|
||||||
JSON.stringify(errorLogData, null, 2),
|
JSON.stringify(errorLogData, null, 2)
|
||||||
);
|
)
|
||||||
throw new Error(`Failed to obtain storage permissions: ${err.message}`);
|
throw new Error(`Failed to obtain storage permissions: ${err.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,12 +148,12 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
async readFile(path: string): Promise<string> {
|
async readFile(path: string): Promise<string> {
|
||||||
const file = await Filesystem.readFile({
|
const file = await Filesystem.readFile({
|
||||||
path,
|
path,
|
||||||
directory: Directory.Data,
|
directory: Directory.Data
|
||||||
});
|
})
|
||||||
if (file.data instanceof Blob) {
|
if (file.data instanceof Blob) {
|
||||||
return await file.data.text();
|
return await file.data.text()
|
||||||
}
|
}
|
||||||
return file.data;
|
return file.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,13 +189,13 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
const logData = {
|
const logData = {
|
||||||
targetFileName: fileName,
|
targetFileName: fileName,
|
||||||
contentLength: content.length,
|
contentLength: content.length,
|
||||||
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
|
platform: this.getCapabilities().isIOS ? 'iOS' : 'Android',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
};
|
}
|
||||||
logger.log(
|
logger.log(
|
||||||
"Starting writeFile operation",
|
'Starting writeFile operation',
|
||||||
JSON.stringify(logData, null, 2),
|
JSON.stringify(logData, null, 2)
|
||||||
);
|
)
|
||||||
|
|
||||||
// For Android, we need to handle content URIs differently
|
// For Android, we need to handle content URIs differently
|
||||||
if (this.getCapabilities().isIOS) {
|
if (this.getCapabilities().isIOS) {
|
||||||
@@ -204,44 +204,44 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
path: fileName,
|
path: fileName,
|
||||||
data: content,
|
data: content,
|
||||||
directory: Directory.Data,
|
directory: Directory.Data,
|
||||||
encoding: Encoding.UTF8,
|
encoding: Encoding.UTF8
|
||||||
});
|
})
|
||||||
|
|
||||||
const writeSuccessLogData = {
|
const writeSuccessLogData = {
|
||||||
path: writeResult.uri,
|
path: writeResult.uri,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
};
|
}
|
||||||
logger.log(
|
logger.log(
|
||||||
"File write successful",
|
'File write successful',
|
||||||
JSON.stringify(writeSuccessLogData, null, 2),
|
JSON.stringify(writeSuccessLogData, null, 2)
|
||||||
);
|
)
|
||||||
|
|
||||||
// Offer to share the file
|
// Offer to share the file
|
||||||
try {
|
try {
|
||||||
await Share.share({
|
await Share.share({
|
||||||
title: "TimeSafari Backup",
|
title: 'TimeSafari Backup',
|
||||||
text: "Here is your TimeSafari backup file.",
|
text: 'Here is your TimeSafari backup file.',
|
||||||
url: writeResult.uri,
|
url: writeResult.uri,
|
||||||
dialogTitle: "Share your backup",
|
dialogTitle: 'Share your backup'
|
||||||
});
|
})
|
||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
"Share dialog shown",
|
'Share dialog shown',
|
||||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2)
|
||||||
);
|
)
|
||||||
} catch (shareError) {
|
} catch (shareError) {
|
||||||
// Log share error but don't fail the operation
|
// Log share error but don't fail the operation
|
||||||
logger.error(
|
logger.error(
|
||||||
"Share dialog failed",
|
'Share dialog failed',
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
error: shareError,
|
error: shareError,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2
|
||||||
),
|
)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// For Android, first write to app's Documents directory
|
// For Android, first write to app's Documents directory
|
||||||
@@ -249,61 +249,61 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
path: fileName,
|
path: fileName,
|
||||||
data: content,
|
data: content,
|
||||||
directory: Directory.Data,
|
directory: Directory.Data,
|
||||||
encoding: Encoding.UTF8,
|
encoding: Encoding.UTF8
|
||||||
});
|
})
|
||||||
|
|
||||||
const writeSuccessLogData = {
|
const writeSuccessLogData = {
|
||||||
path: writeResult.uri,
|
path: writeResult.uri,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
};
|
}
|
||||||
logger.log(
|
logger.log(
|
||||||
"File write successful to app storage",
|
'File write successful to app storage',
|
||||||
JSON.stringify(writeSuccessLogData, null, 2),
|
JSON.stringify(writeSuccessLogData, null, 2)
|
||||||
);
|
)
|
||||||
|
|
||||||
// Then share the file to let user choose where to save it
|
// Then share the file to let user choose where to save it
|
||||||
try {
|
try {
|
||||||
await Share.share({
|
await Share.share({
|
||||||
title: "TimeSafari Backup",
|
title: 'TimeSafari Backup',
|
||||||
text: "Here is your TimeSafari backup file.",
|
text: 'Here is your TimeSafari backup file.',
|
||||||
url: writeResult.uri,
|
url: writeResult.uri,
|
||||||
dialogTitle: "Save your backup",
|
dialogTitle: 'Save your backup'
|
||||||
});
|
})
|
||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
"Share dialog shown for Android",
|
'Share dialog shown for Android',
|
||||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2)
|
||||||
);
|
)
|
||||||
} catch (shareError) {
|
} catch (shareError) {
|
||||||
// Log share error but don't fail the operation
|
// Log share error but don't fail the operation
|
||||||
logger.error(
|
logger.error(
|
||||||
"Share dialog failed for Android",
|
'Share dialog failed for Android',
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
error: shareError,
|
error: shareError,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2
|
||||||
),
|
)
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const err = error as Error;
|
const err = error as Error
|
||||||
const finalErrorLogData = {
|
const finalErrorLogData = {
|
||||||
error: {
|
error: {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
name: err.name,
|
name: err.name,
|
||||||
stack: err.stack,
|
stack: err.stack
|
||||||
},
|
},
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
};
|
}
|
||||||
logger.error(
|
logger.error(
|
||||||
"Error in writeFile operation:",
|
'Error in writeFile operation:',
|
||||||
JSON.stringify(finalErrorLogData, null, 2),
|
JSON.stringify(finalErrorLogData, null, 2)
|
||||||
);
|
)
|
||||||
throw new Error(`Failed to save file: ${err.message}`);
|
throw new Error(`Failed to save file: ${err.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,14 +317,14 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
* @param content - The content to write to the file
|
* @param content - The content to write to the file
|
||||||
*/
|
*/
|
||||||
async writeAndShareFile(fileName: string, content: string): Promise<void> {
|
async writeAndShareFile(fileName: string, content: string): Promise<void> {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString()
|
||||||
const logData = {
|
const logData = {
|
||||||
action: "writeAndShareFile",
|
action: 'writeAndShareFile',
|
||||||
fileName,
|
fileName,
|
||||||
contentLength: content.length,
|
contentLength: content.length,
|
||||||
timestamp,
|
timestamp
|
||||||
};
|
}
|
||||||
logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
|
logger.log('[CapacitorPlatformService]', JSON.stringify(logData, null, 2))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { uri } = await Filesystem.writeFile({
|
const { uri } = await Filesystem.writeFile({
|
||||||
@@ -332,32 +332,32 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
data: content,
|
data: content,
|
||||||
directory: Directory.Data,
|
directory: Directory.Data,
|
||||||
encoding: Encoding.UTF8,
|
encoding: Encoding.UTF8,
|
||||||
recursive: true,
|
recursive: true
|
||||||
});
|
})
|
||||||
|
|
||||||
logger.log("[CapacitorPlatformService] File write successful:", {
|
logger.log('[CapacitorPlatformService] File write successful:', {
|
||||||
uri,
|
uri,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
});
|
})
|
||||||
|
|
||||||
await Share.share({
|
await Share.share({
|
||||||
title: "TimeSafari Backup",
|
title: 'TimeSafari Backup',
|
||||||
text: "Here is your backup file.",
|
text: 'Here is your backup file.',
|
||||||
url: uri,
|
url: uri,
|
||||||
dialogTitle: "Share your backup file",
|
dialogTitle: 'Share your backup file'
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error
|
||||||
const errLog = {
|
const errLog = {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
stack: err.stack,
|
stack: err.stack,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString()
|
||||||
};
|
}
|
||||||
logger.error(
|
logger.error(
|
||||||
"[CapacitorPlatformService] Error writing or sharing file:",
|
'[CapacitorPlatformService] Error writing or sharing file:',
|
||||||
JSON.stringify(errLog, null, 2),
|
JSON.stringify(errLog, null, 2)
|
||||||
);
|
)
|
||||||
throw new Error(`Failed to write or share file: ${err.message}`);
|
throw new Error(`Failed to write or share file: ${err.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,8 +369,8 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
async deleteFile(path: string): Promise<void> {
|
async deleteFile(path: string): Promise<void> {
|
||||||
await Filesystem.deleteFile({
|
await Filesystem.deleteFile({
|
||||||
path,
|
path,
|
||||||
directory: Directory.Data,
|
directory: Directory.Data
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -382,11 +382,11 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
async listFiles(directory: string): Promise<string[]> {
|
async listFiles(directory: string): Promise<string[]> {
|
||||||
const result = await Filesystem.readdir({
|
const result = await Filesystem.readdir({
|
||||||
path: directory,
|
path: directory,
|
||||||
directory: Directory.Data,
|
directory: Directory.Data
|
||||||
});
|
})
|
||||||
return result.files.map((file) =>
|
return result.files.map((file) =>
|
||||||
typeof file === "string" ? file : file.name,
|
typeof file === 'string' ? file : file.name
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -401,17 +401,17 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
quality: 90,
|
quality: 90,
|
||||||
allowEditing: true,
|
allowEditing: true,
|
||||||
resultType: CameraResultType.Base64,
|
resultType: CameraResultType.Base64,
|
||||||
source: CameraSource.Camera,
|
source: CameraSource.Camera
|
||||||
});
|
})
|
||||||
|
|
||||||
const blob = await this.processImageData(image.base64String);
|
const blob = await this.processImageData(image.base64String)
|
||||||
return {
|
return {
|
||||||
blob,
|
blob,
|
||||||
fileName: `photo_${Date.now()}.${image.format || "jpg"}`,
|
fileName: `photo_${Date.now()}.${image.format || 'jpg'}`
|
||||||
};
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error taking picture with Capacitor:", error);
|
logger.error('Error taking picture with Capacitor:', error)
|
||||||
throw new Error("Failed to take picture");
|
throw new Error('Failed to take picture')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,17 +427,17 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
quality: 90,
|
quality: 90,
|
||||||
allowEditing: true,
|
allowEditing: true,
|
||||||
resultType: CameraResultType.Base64,
|
resultType: CameraResultType.Base64,
|
||||||
source: CameraSource.Photos,
|
source: CameraSource.Photos
|
||||||
});
|
})
|
||||||
|
|
||||||
const blob = await this.processImageData(image.base64String);
|
const blob = await this.processImageData(image.base64String)
|
||||||
return {
|
return {
|
||||||
blob,
|
blob,
|
||||||
fileName: `photo_${Date.now()}.${image.format || "jpg"}`,
|
fileName: `photo_${Date.now()}.${image.format || 'jpg'}`
|
||||||
};
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error picking image with Capacitor:", error);
|
logger.error('Error picking image with Capacitor:', error)
|
||||||
throw new Error("Failed to pick image");
|
throw new Error('Failed to pick image')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,22 +449,22 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
*/
|
*/
|
||||||
private async processImageData(base64String?: string): Promise<Blob> {
|
private async processImageData(base64String?: string): Promise<Blob> {
|
||||||
if (!base64String) {
|
if (!base64String) {
|
||||||
throw new Error("No image data received");
|
throw new Error('No image data received')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert base64 to blob
|
// Convert base64 to blob
|
||||||
const byteCharacters = atob(base64String);
|
const byteCharacters = atob(base64String)
|
||||||
const byteArrays = [];
|
const byteArrays = []
|
||||||
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||||
const slice = byteCharacters.slice(offset, offset + 512);
|
const slice = byteCharacters.slice(offset, offset + 512)
|
||||||
const byteNumbers = new Array(slice.length);
|
const byteNumbers = new Array(slice.length)
|
||||||
for (let i = 0; i < slice.length; i++) {
|
for (let i = 0; i < slice.length; i++) {
|
||||||
byteNumbers[i] = slice.charCodeAt(i);
|
byteNumbers[i] = slice.charCodeAt(i)
|
||||||
}
|
}
|
||||||
const byteArray = new Uint8Array(byteNumbers);
|
const byteArray = new Uint8Array(byteNumbers)
|
||||||
byteArrays.push(byteArray);
|
byteArrays.push(byteArray)
|
||||||
}
|
}
|
||||||
return new Blob(byteArrays, { type: "image/jpeg" });
|
return new Blob(byteArrays, { type: 'image/jpeg' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -475,7 +475,7 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
async handleDeepLink(_url: string): Promise<void> {
|
async handleDeepLink(_url: string): Promise<void> {
|
||||||
// Capacitor handles deep links automatically
|
// Capacitor handles deep links automatically
|
||||||
// This is just a placeholder for the interface
|
// This is just a placeholder for the interface
|
||||||
return Promise.resolve();
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -486,11 +486,11 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
async writeToClipboard(text: string): Promise<void> {
|
async writeToClipboard(text: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await Clipboard.write({
|
await Clipboard.write({
|
||||||
string: text,
|
string: text
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error writing to clipboard:", error);
|
logger.error('Error writing to clipboard:', error)
|
||||||
throw new Error("Failed to write to clipboard");
|
throw new Error('Failed to write to clipboard')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,11 +500,11 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
*/
|
*/
|
||||||
async readFromClipboard(): Promise<string> {
|
async readFromClipboard(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const { value } = await Clipboard.read();
|
const { value } = await Clipboard.read()
|
||||||
return value;
|
return value
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error reading from clipboard:", error);
|
logger.error('Error reading from clipboard:', error)
|
||||||
throw new Error("Failed to read from clipboard");
|
throw new Error('Failed to read from clipboard')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
ImageResult,
|
ImageResult,
|
||||||
PlatformService,
|
PlatformService,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities
|
||||||
} from "../PlatformService";
|
} from '../PlatformService'
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from '../../utils/logger'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service implementation for Electron (desktop) platform.
|
* Platform service implementation for Electron (desktop) platform.
|
||||||
@@ -29,8 +29,8 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
isMobile: false,
|
isMobile: false,
|
||||||
isIOS: false,
|
isIOS: false,
|
||||||
hasFileDownload: false, // Not implemented yet
|
hasFileDownload: false, // Not implemented yet
|
||||||
needsFileHandlingInstructions: false,
|
needsFileHandlingInstructions: false
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +41,7 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
* @todo Implement file reading using Electron's file system API
|
* @todo Implement file reading using Electron's file system API
|
||||||
*/
|
*/
|
||||||
async readFile(_path: string): Promise<string> {
|
async readFile(_path: string): Promise<string> {
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,7 +52,7 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
* @todo Implement file writing using Electron's file system API
|
* @todo Implement file writing using Electron's file system API
|
||||||
*/
|
*/
|
||||||
async writeFile(_path: string, _content: string): Promise<void> {
|
async writeFile(_path: string, _content: string): Promise<void> {
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,7 +62,7 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
* @todo Implement file deletion using Electron's file system API
|
* @todo Implement file deletion using Electron's file system API
|
||||||
*/
|
*/
|
||||||
async deleteFile(_path: string): Promise<void> {
|
async deleteFile(_path: string): Promise<void> {
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,7 +73,7 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
* @todo Implement directory listing using Electron's file system API
|
* @todo Implement directory listing using Electron's file system API
|
||||||
*/
|
*/
|
||||||
async listFiles(_directory: string): Promise<string[]> {
|
async listFiles(_directory: string): Promise<string[]> {
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,8 +83,8 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
* @todo Implement camera access using Electron's media APIs
|
* @todo Implement camera access using Electron's media APIs
|
||||||
*/
|
*/
|
||||||
async takePicture(): Promise<ImageResult> {
|
async takePicture(): Promise<ImageResult> {
|
||||||
logger.error("takePicture not implemented in Electron platform");
|
logger.error('takePicture not implemented in Electron platform')
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,8 +94,8 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
* @todo Implement file picker using Electron's dialog API
|
* @todo Implement file picker using Electron's dialog API
|
||||||
*/
|
*/
|
||||||
async pickImage(): Promise<ImageResult> {
|
async pickImage(): Promise<ImageResult> {
|
||||||
logger.error("pickImage not implemented in Electron platform");
|
logger.error('pickImage not implemented in Electron platform')
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,7 +105,7 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
* @todo Implement deep link handling using Electron's protocol handler
|
* @todo Implement deep link handling using Electron's protocol handler
|
||||||
*/
|
*/
|
||||||
async handleDeepLink(_url: string): Promise<void> {
|
async handleDeepLink(_url: string): Promise<void> {
|
||||||
logger.error("handleDeepLink not implemented in Electron platform");
|
logger.error('handleDeepLink not implemented in Electron platform')
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
ImageResult,
|
ImageResult,
|
||||||
PlatformService,
|
PlatformService,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities
|
||||||
} from "../PlatformService";
|
} from '../PlatformService'
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from '../../utils/logger'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service implementation for PyWebView platform.
|
* Platform service implementation for PyWebView platform.
|
||||||
@@ -30,8 +30,8 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
isMobile: false,
|
isMobile: false,
|
||||||
isIOS: false,
|
isIOS: false,
|
||||||
hasFileDownload: false, // Not implemented yet
|
hasFileDownload: false, // Not implemented yet
|
||||||
needsFileHandlingInstructions: false,
|
needsFileHandlingInstructions: false
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,7 +42,7 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
* @todo Implement file reading through pywebview's Python-JavaScript bridge
|
* @todo Implement file reading through pywebview's Python-JavaScript bridge
|
||||||
*/
|
*/
|
||||||
async readFile(_path: string): Promise<string> {
|
async readFile(_path: string): Promise<string> {
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,7 +53,7 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
* @todo Implement file writing through pywebview's Python-JavaScript bridge
|
* @todo Implement file writing through pywebview's Python-JavaScript bridge
|
||||||
*/
|
*/
|
||||||
async writeFile(_path: string, _content: string): Promise<void> {
|
async writeFile(_path: string, _content: string): Promise<void> {
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +63,7 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
* @todo Implement file deletion through pywebview's Python-JavaScript bridge
|
* @todo Implement file deletion through pywebview's Python-JavaScript bridge
|
||||||
*/
|
*/
|
||||||
async deleteFile(_path: string): Promise<void> {
|
async deleteFile(_path: string): Promise<void> {
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,7 +74,7 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
* @todo Implement directory listing through pywebview's Python-JavaScript bridge
|
* @todo Implement directory listing through pywebview's Python-JavaScript bridge
|
||||||
*/
|
*/
|
||||||
async listFiles(_directory: string): Promise<string[]> {
|
async listFiles(_directory: string): Promise<string[]> {
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,8 +84,8 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
* @todo Implement camera access using Python's camera libraries
|
* @todo Implement camera access using Python's camera libraries
|
||||||
*/
|
*/
|
||||||
async takePicture(): Promise<ImageResult> {
|
async takePicture(): Promise<ImageResult> {
|
||||||
logger.error("takePicture not implemented in PyWebView platform");
|
logger.error('takePicture not implemented in PyWebView platform')
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,8 +95,8 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
* @todo Implement file picker using pywebview's file dialog API
|
* @todo Implement file picker using pywebview's file dialog API
|
||||||
*/
|
*/
|
||||||
async pickImage(): Promise<ImageResult> {
|
async pickImage(): Promise<ImageResult> {
|
||||||
logger.error("pickImage not implemented in PyWebView platform");
|
logger.error('pickImage not implemented in PyWebView platform')
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,7 +106,7 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
* @todo Implement deep link handling using Python's URL handling capabilities
|
* @todo Implement deep link handling using Python's URL handling capabilities
|
||||||
*/
|
*/
|
||||||
async handleDeepLink(_url: string): Promise<void> {
|
async handleDeepLink(_url: string): Promise<void> {
|
||||||
logger.error("handleDeepLink not implemented in PyWebView platform");
|
logger.error('handleDeepLink not implemented in PyWebView platform')
|
||||||
throw new Error("Not implemented");
|
throw new Error('Not implemented')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
ImageResult,
|
ImageResult,
|
||||||
PlatformService,
|
PlatformService,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities
|
||||||
} from "../PlatformService";
|
} from '../PlatformService'
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from '../../utils/logger'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service implementation for web browser platform.
|
* Platform service implementation for web browser platform.
|
||||||
@@ -30,8 +30,8 @@ export class WebPlatformService implements PlatformService {
|
|||||||
isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent),
|
isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent),
|
||||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||||
hasFileDownload: true,
|
hasFileDownload: true,
|
||||||
needsFileHandlingInstructions: false,
|
needsFileHandlingInstructions: false
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,7 +40,7 @@ export class WebPlatformService implements PlatformService {
|
|||||||
* @throws Error indicating file system access is not available
|
* @throws Error indicating file system access is not available
|
||||||
*/
|
*/
|
||||||
async readFile(_path: string): Promise<string> {
|
async readFile(_path: string): Promise<string> {
|
||||||
throw new Error("File system access not available in web platform");
|
throw new Error('File system access not available in web platform')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,7 +50,7 @@ export class WebPlatformService implements PlatformService {
|
|||||||
* @throws Error indicating file system access is not available
|
* @throws Error indicating file system access is not available
|
||||||
*/
|
*/
|
||||||
async writeFile(_path: string, _content: string): Promise<void> {
|
async writeFile(_path: string, _content: string): Promise<void> {
|
||||||
throw new Error("File system access not available in web platform");
|
throw new Error('File system access not available in web platform')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,7 +59,7 @@ export class WebPlatformService implements PlatformService {
|
|||||||
* @throws Error indicating file system access is not available
|
* @throws Error indicating file system access is not available
|
||||||
*/
|
*/
|
||||||
async deleteFile(_path: string): Promise<void> {
|
async deleteFile(_path: string): Promise<void> {
|
||||||
throw new Error("File system access not available in web platform");
|
throw new Error('File system access not available in web platform')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,7 +68,7 @@ export class WebPlatformService implements PlatformService {
|
|||||||
* @throws Error indicating file system access is not available
|
* @throws Error indicating file system access is not available
|
||||||
*/
|
*/
|
||||||
async listFiles(_directory: string): Promise<string[]> {
|
async listFiles(_directory: string): Promise<string[]> {
|
||||||
throw new Error("File system access not available in web platform");
|
throw new Error('File system access not available in web platform')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,31 +85,31 @@ export class WebPlatformService implements PlatformService {
|
|||||||
*/
|
*/
|
||||||
async takePicture(): Promise<ImageResult> {
|
async takePicture(): Promise<ImageResult> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement('input')
|
||||||
input.type = "file";
|
input.type = 'file'
|
||||||
input.accept = "image/*";
|
input.accept = 'image/*'
|
||||||
input.capture = "environment";
|
input.capture = 'environment'
|
||||||
|
|
||||||
input.onchange = async (e) => {
|
input.onchange = async (e) => {
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
const file = (e.target as HTMLInputElement).files?.[0]
|
||||||
if (file) {
|
if (file) {
|
||||||
try {
|
try {
|
||||||
const blob = await this.processImageFile(file);
|
const blob = await this.processImageFile(file)
|
||||||
resolve({
|
resolve({
|
||||||
blob,
|
blob,
|
||||||
fileName: file.name || "photo.jpg",
|
fileName: file.name || 'photo.jpg'
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error processing camera image:", error);
|
logger.error('Error processing camera image:', error)
|
||||||
reject(new Error("Failed to process camera image"));
|
reject(new Error('Failed to process camera image'))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reject(new Error("No image captured"));
|
reject(new Error('No image captured'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
input.click();
|
input.click()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,30 +125,30 @@ export class WebPlatformService implements PlatformService {
|
|||||||
*/
|
*/
|
||||||
async pickImage(): Promise<ImageResult> {
|
async pickImage(): Promise<ImageResult> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const input = document.createElement("input");
|
const input = document.createElement('input')
|
||||||
input.type = "file";
|
input.type = 'file'
|
||||||
input.accept = "image/*";
|
input.accept = 'image/*'
|
||||||
|
|
||||||
input.onchange = async (e) => {
|
input.onchange = async (e) => {
|
||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
const file = (e.target as HTMLInputElement).files?.[0]
|
||||||
if (file) {
|
if (file) {
|
||||||
try {
|
try {
|
||||||
const blob = await this.processImageFile(file);
|
const blob = await this.processImageFile(file)
|
||||||
resolve({
|
resolve({
|
||||||
blob,
|
blob,
|
||||||
fileName: file.name || "photo.jpg",
|
fileName: file.name || 'photo.jpg'
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error processing picked image:", error);
|
logger.error('Error processing picked image:', error)
|
||||||
reject(new Error("Failed to process picked image"));
|
reject(new Error('Failed to process picked image'))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reject(new Error("No image selected"));
|
reject(new Error('No image selected'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
input.click();
|
input.click()
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -165,24 +165,24 @@ export class WebPlatformService implements PlatformService {
|
|||||||
*/
|
*/
|
||||||
private async processImageFile(file: File): Promise<Blob> {
|
private async processImageFile(file: File): Promise<Blob> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader()
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
const dataUrl = event.target?.result as string;
|
const dataUrl = event.target?.result as string
|
||||||
// Convert to blob to ensure consistent format
|
// Convert to blob to ensure consistent format
|
||||||
fetch(dataUrl)
|
fetch(dataUrl)
|
||||||
.then((res) => res.blob())
|
.then((res) => res.blob())
|
||||||
.then((blob) => resolve(blob))
|
.then((blob) => resolve(blob))
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error("Error converting data URL to blob:", error);
|
logger.error('Error converting data URL to blob:', error)
|
||||||
reject(error);
|
reject(error)
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
reader.onerror = (error) => {
|
reader.onerror = (error) => {
|
||||||
logger.error("Error reading file:", error);
|
logger.error('Error reading file:', error)
|
||||||
reject(error);
|
reject(error)
|
||||||
};
|
}
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -190,7 +190,7 @@ export class WebPlatformService implements PlatformService {
|
|||||||
* @returns false, as this is not Capacitor
|
* @returns false, as this is not Capacitor
|
||||||
*/
|
*/
|
||||||
isCapacitor(): boolean {
|
isCapacitor(): boolean {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -198,7 +198,7 @@ export class WebPlatformService implements PlatformService {
|
|||||||
* @returns false, as this is not Electron
|
* @returns false, as this is not Electron
|
||||||
*/
|
*/
|
||||||
isElectron(): boolean {
|
isElectron(): boolean {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,7 +206,7 @@ export class WebPlatformService implements PlatformService {
|
|||||||
* @returns false, as this is not PyWebView
|
* @returns false, as this is not PyWebView
|
||||||
*/
|
*/
|
||||||
isPyWebView(): boolean {
|
isPyWebView(): boolean {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -214,7 +214,7 @@ export class WebPlatformService implements PlatformService {
|
|||||||
* @returns true, as this is the web implementation
|
* @returns true, as this is the web implementation
|
||||||
*/
|
*/
|
||||||
isWeb(): boolean {
|
isWeb(): boolean {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -226,7 +226,7 @@ export class WebPlatformService implements PlatformService {
|
|||||||
*/
|
*/
|
||||||
async handleDeepLink(_url: string): Promise<void> {
|
async handleDeepLink(_url: string): Promise<void> {
|
||||||
// Web platform can handle deep links through URL parameters
|
// Web platform can handle deep links through URL parameters
|
||||||
return Promise.resolve();
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -236,10 +236,10 @@ export class WebPlatformService implements PlatformService {
|
|||||||
*/
|
*/
|
||||||
async writeToClipboard(text: string): Promise<void> {
|
async writeToClipboard(text: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error writing to clipboard:", error);
|
logger.error('Error writing to clipboard:', error)
|
||||||
throw new Error("Failed to write to clipboard");
|
throw new Error('Failed to write to clipboard')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,10 +249,10 @@ export class WebPlatformService implements PlatformService {
|
|||||||
*/
|
*/
|
||||||
async readFromClipboard(): Promise<string> {
|
async readFromClipboard(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
return await navigator.clipboard.readText();
|
return await navigator.clipboard.readText()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error reading from clipboard:", error);
|
logger.error('Error reading from clipboard:', error)
|
||||||
throw new Error("Failed to read from clipboard");
|
throw new Error('Failed to read from clipboard')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,62 @@
|
|||||||
import axios from "axios";
|
import axios from 'axios'
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from 'did-jwt'
|
||||||
import { AppString } from "../constants/app";
|
import { AppString } from '../constants/app'
|
||||||
import { retrieveSettingsForActiveAccount } from "../db";
|
import { retrieveSettingsForActiveAccount } from '../db'
|
||||||
import { SERVICE_ID } from "../libs/endorserServer";
|
import { SERVICE_ID } from '../libs/endorserServer'
|
||||||
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
import { deriveAddress, newIdentifier } from '../libs/crypto'
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from '../utils/logger'
|
||||||
/**
|
/**
|
||||||
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
|
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
|
||||||
*/
|
*/
|
||||||
export async function testServerRegisterUser() {
|
export async function testServerRegisterUser() {
|
||||||
const testUser0Mnem =
|
const testUser0Mnem =
|
||||||
"seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control";
|
'seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control'
|
||||||
|
|
||||||
const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem);
|
const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem)
|
||||||
|
|
||||||
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
|
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath)
|
||||||
|
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount()
|
||||||
|
|
||||||
// Make a claim
|
// Make a claim
|
||||||
const vcClaim = {
|
const vcClaim = {
|
||||||
"@context": "https://schema.org",
|
'@context': 'https://schema.org',
|
||||||
"@type": "RegisterAction",
|
'@type': 'RegisterAction',
|
||||||
agent: { did: identity0.did },
|
agent: { did: identity0.did },
|
||||||
object: SERVICE_ID,
|
object: SERVICE_ID,
|
||||||
participant: { did: settings.activeDid },
|
participant: { did: settings.activeDid }
|
||||||
};
|
}
|
||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
sub: "RegisterAction",
|
sub: 'RegisterAction',
|
||||||
vc: {
|
vc: {
|
||||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
||||||
type: ["VerifiableCredential"],
|
type: ['VerifiableCredential'],
|
||||||
credentialSubject: vcClaim,
|
credentialSubject: vcClaim
|
||||||
},
|
}
|
||||||
};
|
}
|
||||||
// create a signature using private key of identity
|
// create a signature using private key of identity
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const privateKeyHex: string = identity0.keys[0].privateKeyHex!;
|
const privateKeyHex: string = identity0.keys[0].privateKeyHex!;
|
||||||
const signer = await didJwt.SimpleSigner(privateKeyHex);
|
const signer = await didJwt.SimpleSigner(privateKeyHex)
|
||||||
const alg = undefined;
|
const alg = undefined
|
||||||
// create a JWT for the request
|
// create a JWT for the request
|
||||||
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
||||||
alg: alg,
|
alg: alg,
|
||||||
issuer: identity0.did,
|
issuer: identity0.did,
|
||||||
signer: signer,
|
signer: signer
|
||||||
});
|
})
|
||||||
|
|
||||||
// Make the xhr request payload
|
// Make the xhr request payload
|
||||||
|
|
||||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
const payload = JSON.stringify({ jwtEncoded: vcJwt })
|
||||||
const endorserApiServer =
|
const endorserApiServer =
|
||||||
settings.apiServer || AppString.TEST_ENDORSER_API_SERVER;
|
settings.apiServer || AppString.TEST_ENDORSER_API_SERVER
|
||||||
const url = endorserApiServer + "/api/claim";
|
const url = endorserApiServer + '/api/claim'
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json'
|
||||||
};
|
}
|
||||||
|
|
||||||
const resp = await axios.post(url, payload, { headers });
|
const resp = await axios.post(url, payload, { headers })
|
||||||
logger.log("User registration result:", resp);
|
logger.log('User registration result:', resp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,79 +25,79 @@
|
|||||||
* // TypeScript knows params.id exists and params.view is optional
|
* // TypeScript knows params.id exists and params.view is optional
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
import { z } from "zod";
|
import { z } from 'zod'
|
||||||
|
|
||||||
// Add a union type of all valid route paths
|
// Add a union type of all valid route paths
|
||||||
export const VALID_DEEP_LINK_ROUTES = [
|
export const VALID_DEEP_LINK_ROUTES = [
|
||||||
"user-profile",
|
'user-profile',
|
||||||
"project-details",
|
'project-details',
|
||||||
"onboard-meeting-setup",
|
'onboard-meeting-setup',
|
||||||
"invite-one-accept",
|
'invite-one-accept',
|
||||||
"contact-import",
|
'contact-import',
|
||||||
"confirm-gift",
|
'confirm-gift',
|
||||||
"claim",
|
'claim',
|
||||||
"claim-cert",
|
'claim-cert',
|
||||||
"claim-add-raw",
|
'claim-add-raw',
|
||||||
"contact-edit",
|
'contact-edit',
|
||||||
"contacts",
|
'contacts',
|
||||||
"did",
|
'did'
|
||||||
] as const;
|
] as const
|
||||||
|
|
||||||
// Create a type from the array
|
// Create a type from the array
|
||||||
export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number];
|
export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number]
|
||||||
|
|
||||||
// Update your schema definitions to use this type
|
// Update your schema definitions to use this type
|
||||||
export const baseUrlSchema = z.object({
|
export const baseUrlSchema = z.object({
|
||||||
scheme: z.literal("timesafari"),
|
scheme: z.literal('timesafari'),
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
queryParams: z.record(z.string()).optional(),
|
queryParams: z.record(z.string()).optional()
|
||||||
});
|
})
|
||||||
|
|
||||||
// Use the type to ensure route validation
|
// Use the type to ensure route validation
|
||||||
export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES);
|
export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES)
|
||||||
|
|
||||||
// Parameter validation schemas for each route type
|
// Parameter validation schemas for each route type
|
||||||
export const deepLinkSchemas = {
|
export const deepLinkSchemas = {
|
||||||
"user-profile": z.object({
|
'user-profile': z.object({
|
||||||
id: z.string(),
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
"project-details": z.object({
|
'project-details': z.object({
|
||||||
id: z.string(),
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
"onboard-meeting-setup": z.object({
|
'onboard-meeting-setup': z.object({
|
||||||
id: z.string(),
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
"invite-one-accept": z.object({
|
'invite-one-accept': z.object({
|
||||||
id: z.string(),
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
"contact-import": z.object({
|
'contact-import': z.object({
|
||||||
jwt: z.string(),
|
jwt: z.string()
|
||||||
}),
|
}),
|
||||||
"confirm-gift": z.object({
|
'confirm-gift': z.object({
|
||||||
id: z.string(),
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
claim: z.object({
|
claim: z.object({
|
||||||
id: z.string(),
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
"claim-cert": z.object({
|
'claim-cert': z.object({
|
||||||
id: z.string(),
|
id: z.string()
|
||||||
}),
|
}),
|
||||||
"claim-add-raw": z.object({
|
'claim-add-raw': z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
claim: z.string().optional(),
|
claim: z.string().optional(),
|
||||||
claimJwtId: z.string().optional(),
|
claimJwtId: z.string().optional()
|
||||||
}),
|
}),
|
||||||
"contact-edit": z.object({
|
'contact-edit': z.object({
|
||||||
did: z.string(),
|
did: z.string()
|
||||||
}),
|
}),
|
||||||
contacts: z.object({
|
contacts: z.object({
|
||||||
contacts: z.string(), // JSON string of contacts array
|
contacts: z.string() // JSON string of contacts array
|
||||||
}),
|
}),
|
||||||
did: z.object({
|
did: z.object({
|
||||||
id: z.string(),
|
id: z.string()
|
||||||
}),
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
export type DeepLinkParams = {
|
export type DeepLinkParams = {
|
||||||
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
|
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>
|
||||||
};
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user