Merge branch 'qrcode-capacitor' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into qrcode-capacitor
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",
|
||||
"asn1-ber": "^1.2.2",
|
||||
"axios": "^1.6.8",
|
||||
"buffer": "^6.0.3",
|
||||
"cbor-x": "^1.5.9",
|
||||
"class-transformer": "^0.5.1",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"dexie": "^3.2.7",
|
||||
"dexie-export-import": "^4.1.4",
|
||||
"did-jwt": "^7.4.7",
|
||||
@@ -63,7 +65,7 @@
|
||||
"lru-cache": "^10.2.0",
|
||||
"luxon": "^3.4.4",
|
||||
"merkletreejs": "^0.3.11",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"nostr-tools": "^2.12.0",
|
||||
"notiwind": "^2.0.2",
|
||||
"papaparse": "^5.4.1",
|
||||
"pina": "^0.20.2204228",
|
||||
@@ -105,21 +107,23 @@
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"concurrently": "^8.2.2",
|
||||
"electron": "^33.2.1",
|
||||
"electron-builder": "^25.1.8",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"eslint-import-resolver-node": "^0.3.9",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"espree": "^10.3.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"markdownlint": "^0.37.4",
|
||||
"markdownlint-cli": "^0.44.0",
|
||||
"npm-check-updates": "^17.1.13",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier": "^3.5.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "~5.2.2",
|
||||
@@ -4625,6 +4629,24 @@
|
||||
"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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -10745,15 +10767,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/eslint-config-typescript": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-11.0.3.tgz",
|
||||
"integrity": "sha512-dkt6W0PX6H/4Xuxg/BlFj5xHvksjpSlVjtkQCpaYJBIEuKj2hOVU7r+TIe+ysCwRYFz/lGqvklntRkCAibsbPw==",
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz",
|
||||
"integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
"vue-eslint-parser": "^9.1.1"
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||
"@typescript-eslint/parser": "^6.7.0",
|
||||
"vue-eslint-parser": "^9.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
||||
@@ -12558,6 +12360,119 @@
|
||||
"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": {
|
||||
"version": "4.24.4",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
||||
@@ -14091,7 +14006,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
@@ -14253,6 +14167,22 @@
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
@@ -14379,6 +14309,45 @@
|
||||
"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": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
@@ -14841,6 +14810,16 @@
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
@@ -14981,6 +14960,23 @@
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||
@@ -15832,6 +15828,28 @@
|
||||
"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": {
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
|
||||
@@ -15927,6 +15945,24 @@
|
||||
"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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@@ -15951,18 +15987,31 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "10.3.0",
|
||||
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
|
||||
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"acorn": "^8.9.0",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
"eslint-visitor-keys": "^4.2.0"
|
||||
},
|
||||
"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": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
@@ -22576,6 +22625,25 @@
|
||||
"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": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||
@@ -23019,13 +23087,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@@ -23924,6 +23985,53 @@
|
||||
"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": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
|
||||
@@ -24619,7 +24727,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/progress": {
|
||||
@@ -24718,6 +24825,26 @@
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"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": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
@@ -25073,6 +25200,16 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -29109,29 +29246,6 @@
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||
"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": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
@@ -30096,6 +30210,24 @@
|
||||
"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": {
|
||||
"version": "3.0.4",
|
||||
"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",
|
||||
"version": "0.4.4",
|
||||
"description": "Time Safari Application",
|
||||
"type": "module",
|
||||
"author": {
|
||||
"name": "Time Safari Team"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:web": "vite --config vite.config.web.mts",
|
||||
"serve": "vite preview",
|
||||
"build": "vite build",
|
||||
"build:mobile": "VITE_PLATFORM=capacitor vite build",
|
||||
"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/",
|
||||
"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",
|
||||
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
|
||||
"test:prerequisites": "node scripts/check-prerequisites.js",
|
||||
@@ -84,8 +86,10 @@
|
||||
"@zxing/text-encoding": "^0.9.0",
|
||||
"asn1-ber": "^1.2.2",
|
||||
"axios": "^1.6.8",
|
||||
"buffer": "^6.0.3",
|
||||
"cbor-x": "^1.5.9",
|
||||
"class-transformer": "^0.5.1",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"dexie": "^3.2.7",
|
||||
"dexie-export-import": "^4.1.4",
|
||||
"did-jwt": "^7.4.7",
|
||||
@@ -102,7 +106,7 @@
|
||||
"lru-cache": "^10.2.0",
|
||||
"luxon": "^3.4.4",
|
||||
"merkletreejs": "^0.3.11",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"nostr-tools": "^2.12.0",
|
||||
"notiwind": "^2.0.2",
|
||||
"papaparse": "^5.4.1",
|
||||
"pina": "^0.20.2204228",
|
||||
@@ -144,21 +148,23 @@
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"concurrently": "^8.2.2",
|
||||
"electron": "^33.2.1",
|
||||
"electron-builder": "^25.1.8",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"eslint-import-resolver-node": "^0.3.9",
|
||||
"eslint-plugin-prettier": "^5.2.6",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"espree": "^10.3.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"markdownlint": "^0.37.4",
|
||||
"markdownlint-cli": "^0.44.0",
|
||||
"npm-check-updates": "^17.1.13",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier": "^3.5.3",
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "~5.2.2",
|
||||
|
||||
266
src/App.vue
266
src/App.vue
@@ -29,7 +29,9 @@
|
||||
>
|
||||
<div class="w-full px-4 py-3">
|
||||
<span class="font-semibold">{{ notification.title }}</span>
|
||||
<p class="text-sm">{{ notification.text }}</p>
|
||||
<p class="text-sm">
|
||||
{{ notification.text }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,21 +42,20 @@
|
||||
<div
|
||||
class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
|
||||
>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="fa-fw fa-xl"
|
||||
></font-awesome>
|
||||
<font-awesome icon="circle-info" class="fa-fw fa-xl" />
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
|
||||
<span class="font-semibold">{{ notification.title }}</span>
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
<p class="text-sm">
|
||||
{{ truncateLongWords(notification.text) }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
|
||||
@click="close(notification.id)"
|
||||
>
|
||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
||||
<font-awesome icon="xmark" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,21 +67,20 @@
|
||||
<div
|
||||
class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
|
||||
>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="fa-fw fa-xl"
|
||||
></font-awesome>
|
||||
<font-awesome icon="circle-info" class="fa-fw fa-xl" />
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
|
||||
<span class="font-semibold">{{ notification.title }}</span>
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
<p class="text-sm">
|
||||
{{ truncateLongWords(notification.text) }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
|
||||
@click="close(notification.id)"
|
||||
>
|
||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
||||
<font-awesome icon="xmark" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,21 +92,20 @@
|
||||
<div
|
||||
class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
|
||||
>
|
||||
<font-awesome
|
||||
icon="triangle-exclamation"
|
||||
class="fa-fw fa-xl"
|
||||
></font-awesome>
|
||||
<font-awesome icon="triangle-exclamation" class="fa-fw fa-xl" />
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
|
||||
<span class="font-semibold">{{ notification.title }}</span>
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
<p class="text-sm">
|
||||
{{ truncateLongWords(notification.text) }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
|
||||
@click="close(notification.id)"
|
||||
>
|
||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
||||
<font-awesome icon="xmark" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,21 +117,20 @@
|
||||
<div
|
||||
class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
|
||||
>
|
||||
<font-awesome
|
||||
icon="triangle-exclamation"
|
||||
class="fa-fw fa-xl"
|
||||
></font-awesome>
|
||||
<font-awesome icon="triangle-exclamation" class="fa-fw fa-xl" />
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
|
||||
<span class="font-semibold">{{ notification.title }}</span>
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
<p class="text-sm">
|
||||
{{ truncateLongWords(notification.text) }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
|
||||
@click="close(notification.id)"
|
||||
>
|
||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
||||
<font-awesome icon="xmark" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,31 +181,33 @@
|
||||
<span class="font-semibold text-lg">
|
||||
{{ notification.title }}
|
||||
</span>
|
||||
<p class="text-sm mb-2">{{ notification.text }}</p>
|
||||
<p class="text-sm mb-2">
|
||||
{{ notification.text }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
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"
|
||||
@click="
|
||||
@click="{
|
||||
notification.onYes();
|
||||
close(notification.id);
|
||||
"
|
||||
}"
|
||||
>
|
||||
Yes{{
|
||||
notification.yesText ? ", " + notification.yesText : ""
|
||||
notification.yesText ? ', ' + notification.yesText : ''
|
||||
}}
|
||||
</button>
|
||||
|
||||
<button
|
||||
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"
|
||||
@click="
|
||||
@click="{
|
||||
notification.onNo(stopAsking);
|
||||
close(notification.id);
|
||||
stopAsking = false; // reset value
|
||||
"
|
||||
}"
|
||||
>
|
||||
No{{ notification.noText ? ", " + notification.noText : "" }}
|
||||
No{{ notification.noText ? ', ' + notification.noText : '' }}
|
||||
</button>
|
||||
|
||||
<label
|
||||
@@ -228,25 +228,25 @@
|
||||
class="sr-only"
|
||||
/>
|
||||
<!-- 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 -->
|
||||
<div
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<button
|
||||
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(stopAsking)
|
||||
: null;
|
||||
close(notification.id);
|
||||
stopAsking = false; // reset value for next time they open this modal
|
||||
"
|
||||
}"
|
||||
>
|
||||
{{ notification.onYes ? "Cancel" : "Close" }}
|
||||
{{ notification.onYes ? 'Cancel' : 'Close' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,10 +306,10 @@
|
||||
|
||||
<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"
|
||||
@click="
|
||||
@click="{
|
||||
close(notification.id);
|
||||
turnOffNotifications(notification);
|
||||
"
|
||||
}"
|
||||
>
|
||||
Turn Off Notification
|
||||
</button>
|
||||
@@ -329,21 +329,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
|
||||
import { NotificationIface } from "./constants/app";
|
||||
import { logger } from "./utils/logger";
|
||||
import { Vue, Component } from 'vue-facing-decorator'
|
||||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from './db/index'
|
||||
import { NotificationIface } from './constants/app'
|
||||
import { logger } from './utils/logger'
|
||||
|
||||
interface Settings {
|
||||
notifyingNewActivityTime?: string;
|
||||
notifyingReminderTime?: string;
|
||||
notifyingNewActivityTime?: string
|
||||
notifyingReminderTime?: string
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class App extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
|
||||
stopAsking = false;
|
||||
stopAsking = false
|
||||
|
||||
// created() {
|
||||
// logger.log(
|
||||
@@ -382,158 +382,158 @@ export default class App extends Vue {
|
||||
|
||||
truncateLongWords(sentence: string) {
|
||||
return sentence
|
||||
.split(" ")
|
||||
.map((word) => (word.length > 30 ? word.slice(0, 30) + "..." : word))
|
||||
.join(" ");
|
||||
.split(' ')
|
||||
.map((word) => (word.length > 30 ? word.slice(0, 30) + '...' : word))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
async turnOffNotifications(
|
||||
notification: NotificationIface,
|
||||
notification: NotificationIface
|
||||
): Promise<boolean> {
|
||||
logger.log("Starting turnOffNotifications...");
|
||||
let subscription: PushSubscriptionJSON | null = null;
|
||||
let allGoingOff = false;
|
||||
logger.log('Starting turnOffNotifications...')
|
||||
let subscription: PushSubscriptionJSON | null = null
|
||||
let allGoingOff = false
|
||||
|
||||
try {
|
||||
logger.log("Retrieving settings for the active account...");
|
||||
const settings: Settings = await retrieveSettingsForActiveAccount();
|
||||
logger.log("Retrieved settings:", settings);
|
||||
logger.log('Retrieving settings for the active account...')
|
||||
const settings: Settings = await retrieveSettingsForActiveAccount()
|
||||
logger.log('Retrieved settings:', settings)
|
||||
|
||||
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
||||
const notifyingReminder = !!settings?.notifyingReminderTime;
|
||||
const notifyingNewActivity = !!settings?.notifyingNewActivityTime
|
||||
const notifyingReminder = !!settings?.notifyingReminderTime
|
||||
|
||||
if (!notifyingNewActivity || !notifyingReminder) {
|
||||
allGoingOff = true;
|
||||
logger.log("Both notifications are being turned off.");
|
||||
allGoingOff = true
|
||||
logger.log('Both notifications are being turned off.')
|
||||
}
|
||||
|
||||
logger.log("Checking service worker readiness...");
|
||||
logger.log('Checking service worker readiness...')
|
||||
await navigator.serviceWorker?.ready
|
||||
.then((registration) => {
|
||||
logger.log("Service worker is ready. Fetching subscription...");
|
||||
return registration.pushManager.getSubscription();
|
||||
logger.log('Service worker is ready. Fetching subscription...')
|
||||
return registration.pushManager.getSubscription()
|
||||
})
|
||||
.then(async (subscript: PushSubscription | null) => {
|
||||
if (subscript) {
|
||||
subscription = subscript.toJSON();
|
||||
logger.log("PushSubscription retrieved:", subscription);
|
||||
subscription = subscript.toJSON()
|
||||
logger.log('PushSubscription retrieved:', subscription)
|
||||
|
||||
if (allGoingOff) {
|
||||
logger.log("Unsubscribing from push notifications...");
|
||||
await subscript.unsubscribe();
|
||||
logger.log("Successfully unsubscribed.");
|
||||
logger.log('Unsubscribing from push notifications...')
|
||||
await subscript.unsubscribe()
|
||||
logger.log('Successfully unsubscribed.')
|
||||
}
|
||||
} else {
|
||||
logConsoleAndDb("Subscription object is not available.");
|
||||
logger.log("No subscription found.");
|
||||
logConsoleAndDb('Subscription object is not available.')
|
||||
logger.log('No subscription found.')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
logConsoleAndDb(
|
||||
"Push provider server communication failed: " +
|
||||
'Push provider server communication failed: ' +
|
||||
JSON.stringify(error),
|
||||
true,
|
||||
);
|
||||
logger.error("Error during subscription fetch:", error);
|
||||
});
|
||||
true
|
||||
)
|
||||
logger.error('Error during subscription fetch:', error)
|
||||
})
|
||||
|
||||
if (!subscription) {
|
||||
logger.log("No subscription available. Notifying user...");
|
||||
logger.log('No subscription available. Notifying user...')
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Finished",
|
||||
text: "Notifications are off.",
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
title: 'Finished',
|
||||
text: 'Notifications are off.'
|
||||
},
|
||||
5000,
|
||||
);
|
||||
logger.log("Exiting as there is no subscription to process.");
|
||||
return true;
|
||||
5000
|
||||
)
|
||||
logger.log('Exiting as there is no subscription to process.')
|
||||
return true
|
||||
}
|
||||
|
||||
const serverSubscription = {
|
||||
...subscription,
|
||||
};
|
||||
...subscription
|
||||
}
|
||||
if (!allGoingOff) {
|
||||
serverSubscription["notifyType"] = notification.title;
|
||||
serverSubscription['notifyType'] = notification.title
|
||||
logger.log(
|
||||
`Server subscription updated with notifyType: ${notification.title}`,
|
||||
);
|
||||
`Server subscription updated with notifyType: ${notification.title}`
|
||||
)
|
||||
}
|
||||
|
||||
logger.log("Sending unsubscribe request to the server...");
|
||||
const pushServerSuccess = await fetch("/web-push/unsubscribe", {
|
||||
method: "POST",
|
||||
logger.log('Sending unsubscribe request to the server...')
|
||||
const pushServerSuccess = await fetch('/web-push/unsubscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(serverSubscription),
|
||||
body: JSON.stringify(serverSubscription)
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
const errorBody = await response.text()
|
||||
logConsoleAndDb(
|
||||
`Push server failed: ${response.status} ${errorBody}`,
|
||||
true,
|
||||
);
|
||||
logger.error("Push server error response:", errorBody);
|
||||
true
|
||||
)
|
||||
logger.error('Push server error response:', errorBody)
|
||||
}
|
||||
logger.log(`Server response status: ${response.status}`);
|
||||
return response.ok;
|
||||
logger.log(`Server response status: ${response.status}`)
|
||||
return response.ok
|
||||
})
|
||||
.catch((error) => {
|
||||
logConsoleAndDb(
|
||||
"Push server communication failed: " + JSON.stringify(error),
|
||||
true,
|
||||
);
|
||||
logger.error("Error during server communication:", error);
|
||||
return false;
|
||||
});
|
||||
'Push server communication failed: ' + JSON.stringify(error),
|
||||
true
|
||||
)
|
||||
logger.error('Error during server communication:', error)
|
||||
return false
|
||||
})
|
||||
|
||||
const message = pushServerSuccess
|
||||
? "Notification is off."
|
||||
: "Notification is still on. Try to turn it off again.";
|
||||
logger.log("Server response processed. Message:", message);
|
||||
? 'Notification is off.'
|
||||
: 'Notification is still on. Try to turn it off again.'
|
||||
logger.log('Server response processed. Message:', message)
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Finished",
|
||||
text: message,
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
title: 'Finished',
|
||||
text: message
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
|
||||
if (notification.callback) {
|
||||
logger.log("Executing notification callback...");
|
||||
notification.callback(pushServerSuccess);
|
||||
logger.log('Executing notification callback...')
|
||||
notification.callback(pushServerSuccess)
|
||||
}
|
||||
|
||||
logger.log(
|
||||
"Completed turnOffNotifications with success:",
|
||||
pushServerSuccess,
|
||||
);
|
||||
return pushServerSuccess;
|
||||
'Completed turnOffNotifications with success:',
|
||||
pushServerSuccess
|
||||
)
|
||||
return pushServerSuccess
|
||||
} catch (error) {
|
||||
logConsoleAndDb(
|
||||
"Error turning off notifications: " + JSON.stringify(error),
|
||||
true,
|
||||
);
|
||||
logger.error("Critical error in turnOffNotifications:", error);
|
||||
'Error turning off notifications: ' + JSON.stringify(error),
|
||||
true
|
||||
)
|
||||
logger.error('Critical error in turnOffNotifications:', error)
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "error",
|
||||
title: "Error",
|
||||
text: "Failed to turn off notifications. Please try again.",
|
||||
group: 'alert',
|
||||
type: 'error',
|
||||
title: 'Error',
|
||||
text: 'Failed to turn off notifications. Please try again.'
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold">
|
||||
{{ record.issuer.known ? record.issuer.displayName : "" }}
|
||||
{{ record.issuer.known ? record.issuer.displayName : '' }}
|
||||
</h3>
|
||||
<p class="ms-auto text-xs text-slate-500 italic">
|
||||
{{ friendlyDate }}
|
||||
@@ -123,7 +123,7 @@
|
||||
|
||||
<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"
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -182,59 +182,59 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import { GiveRecordWithContactInfo } from "../types";
|
||||
import EntityIcon from "./EntityIcon.vue";
|
||||
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
|
||||
import { containsHiddenDid } from "../libs/endorserServer";
|
||||
import ProjectIcon from "./ProjectIcon.vue";
|
||||
import { Component, Prop, Vue } from 'vue-facing-decorator'
|
||||
import { GiveRecordWithContactInfo } from '../types'
|
||||
import EntityIcon from './EntityIcon.vue'
|
||||
import { isGiveClaimType, notifyWhyCannotConfirm } from '../libs/util'
|
||||
import { containsHiddenDid } from '../libs/endorserServer'
|
||||
import ProjectIcon from './ProjectIcon.vue'
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
EntityIcon,
|
||||
ProjectIcon,
|
||||
},
|
||||
ProjectIcon
|
||||
}
|
||||
})
|
||||
export default class ActivityListItem extends Vue {
|
||||
@Prop() record!: GiveRecordWithContactInfo;
|
||||
@Prop() lastViewedClaimId?: string;
|
||||
@Prop() isRegistered!: boolean;
|
||||
@Prop() activeDid!: string;
|
||||
@Prop() confirmerIdList?: string[];
|
||||
@Prop() record!: GiveRecordWithContactInfo
|
||||
@Prop() lastViewedClaimId?: string
|
||||
@Prop() isRegistered!: boolean
|
||||
@Prop() activeDid!: string
|
||||
@Prop() confirmerIdList?: string[]
|
||||
|
||||
get fetchAmount(): string {
|
||||
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
|
||||
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
||||
: "";
|
||||
: ''
|
||||
|
||||
return amount;
|
||||
return amount
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
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) {
|
||||
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
|
||||
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`
|
||||
}
|
||||
|
||||
private currencyShortWordForCode(unitCode: string, single: boolean) {
|
||||
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
||||
return unitCode === 'HUR' ? (single ? 'hour' : 'hours') : unitCode
|
||||
}
|
||||
|
||||
get canConfirm(): boolean {
|
||||
if (!this.isRegistered) return false;
|
||||
if (!isGiveClaimType(this.record.fullClaim?.["@type"])) return false;
|
||||
if (this.confirmerIdList?.includes(this.activeDid)) return false;
|
||||
if (this.record.issuerDid === this.activeDid) return false;
|
||||
if (containsHiddenDid(this.record.fullClaim)) return false;
|
||||
return true;
|
||||
if (!this.isRegistered) return false
|
||||
if (!isGiveClaimType(this.record.fullClaim?.['@type'])) return false
|
||||
if (this.confirmerIdList?.includes(this.activeDid)) return false
|
||||
if (this.record.issuerDid === this.activeDid) return false
|
||||
if (containsHiddenDid(this.record.fullClaim)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
handleConfirmClick() {
|
||||
@@ -242,24 +242,24 @@ export default class ActivityListItem extends Vue {
|
||||
notifyWhyCannotConfirm(
|
||||
this.$notify,
|
||||
this.isRegistered,
|
||||
this.record.fullClaim?.["@type"],
|
||||
this.record.fullClaim?.['@type'],
|
||||
this.record,
|
||||
this.activeDid,
|
||||
this.confirmerIdList,
|
||||
);
|
||||
return;
|
||||
this.confirmerIdList
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit("confirmClaim", this.record);
|
||||
this.$emit('confirmClaim', this.record)
|
||||
}
|
||||
|
||||
get friendlyDate(): string {
|
||||
const date = new Date(this.record.issuedAt);
|
||||
const date = new Date(this.record.issuedAt)
|
||||
return date.toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -26,7 +26,9 @@
|
||||
>
|
||||
<div class="w-full px-6 py-6 text-slate-900 text-center">
|
||||
<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
|
||||
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>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
import { NotificationIface } from '../constants/app'
|
||||
|
||||
@Component
|
||||
export default class PromptDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
|
||||
title = "";
|
||||
text = "";
|
||||
option1Text = "";
|
||||
option2Text = "";
|
||||
option3Text = "";
|
||||
onOption1?: () => void;
|
||||
onOption2?: () => void;
|
||||
onOption3?: () => void;
|
||||
onCancel?: () => Promise<void>;
|
||||
title = ''
|
||||
text = ''
|
||||
option1Text = ''
|
||||
option2Text = ''
|
||||
option3Text = ''
|
||||
onOption1?: () => void
|
||||
onOption2?: () => void
|
||||
onOption3?: () => void
|
||||
onCancel?: () => Promise<void>
|
||||
|
||||
open(options: {
|
||||
title: string;
|
||||
text: string;
|
||||
option1Text?: string;
|
||||
option2Text?: string;
|
||||
option3Text?: string;
|
||||
onOption1?: () => void;
|
||||
onOption2?: () => void;
|
||||
onOption3?: () => void;
|
||||
onCancel?: () => Promise<void>;
|
||||
title: string
|
||||
text: string
|
||||
option1Text?: string
|
||||
option2Text?: string
|
||||
option3Text?: string
|
||||
onOption1?: () => void
|
||||
onOption2?: () => void
|
||||
onOption3?: () => void
|
||||
onCancel?: () => Promise<void>
|
||||
}) {
|
||||
this.title = options.title;
|
||||
this.text = options.text;
|
||||
this.option1Text = options.option1Text || "";
|
||||
this.option2Text = options.option2Text || "";
|
||||
this.option3Text = options.option3Text || "";
|
||||
this.onOption1 = options.onOption1;
|
||||
this.onOption2 = options.onOption2;
|
||||
this.onOption3 = options.onOption3;
|
||||
this.onCancel = options.onCancel;
|
||||
this.title = options.title
|
||||
this.text = options.text
|
||||
this.option1Text = options.option1Text || ''
|
||||
this.option2Text = options.option2Text || ''
|
||||
this.option3Text = options.option3Text || ''
|
||||
this.onOption1 = options.onOption1
|
||||
this.onOption2 = options.onOption2
|
||||
this.onOption3 = options.onOption3
|
||||
this.onCancel = options.onCancel
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "customModal",
|
||||
type: "confirm",
|
||||
group: 'customModal',
|
||||
type: 'confirm',
|
||||
title: this.title,
|
||||
text: this.text,
|
||||
option1Text: this.option1Text,
|
||||
@@ -115,38 +117,38 @@ export default class PromptDialog extends Vue {
|
||||
onOption1: this.onOption1,
|
||||
onOption2: this.onOption2,
|
||||
onOption3: this.onOption3,
|
||||
onCancel: this.onCancel,
|
||||
onCancel: this.onCancel
|
||||
} as NotificationIface,
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
}
|
||||
|
||||
handleOption1(close: (id: string) => void) {
|
||||
if (this.onOption1) {
|
||||
this.onOption1();
|
||||
this.onOption1()
|
||||
}
|
||||
close("string that does not matter");
|
||||
close('string that does not matter')
|
||||
}
|
||||
|
||||
handleOption2(close: (id: string) => void) {
|
||||
if (this.onOption2) {
|
||||
this.onOption2();
|
||||
this.onOption2()
|
||||
}
|
||||
close("string that does not matter");
|
||||
close('string that does not matter')
|
||||
}
|
||||
|
||||
handleOption3(close: (id: string) => void) {
|
||||
if (this.onOption3) {
|
||||
this.onOption3();
|
||||
this.onOption3()
|
||||
}
|
||||
close("string that does not matter");
|
||||
close('string that does not matter')
|
||||
}
|
||||
|
||||
handleCancel(close: (id: string) => void) {
|
||||
if (this.onCancel) {
|
||||
this.onCancel();
|
||||
this.onCancel()
|
||||
}
|
||||
close("string that does not matter");
|
||||
close('string that does not matter')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<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 }}
|
||||
Note that their name is only stored on this device.
|
||||
<input
|
||||
@@ -35,43 +37,43 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
import { Vue, Component } from 'vue-facing-decorator'
|
||||
|
||||
@Component
|
||||
export default class ContactNameDialog extends Vue {
|
||||
cancelCallback: () => void = () => {};
|
||||
saveCallback: (name?: string) => void = () => {};
|
||||
message = "";
|
||||
newText = "";
|
||||
title = "Contact Name";
|
||||
visible = false;
|
||||
cancelCallback: () => void = () => {}
|
||||
saveCallback: (name?: string) => void = () => {}
|
||||
message = ''
|
||||
newText = ''
|
||||
title = 'Contact Name'
|
||||
visible = false
|
||||
|
||||
async open(
|
||||
title?: string,
|
||||
message?: string,
|
||||
saveCallback?: (name?: string) => void,
|
||||
cancelCallback?: () => void,
|
||||
defaultName?: string,
|
||||
defaultName?: string
|
||||
) {
|
||||
this.cancelCallback = cancelCallback || this.cancelCallback;
|
||||
this.saveCallback = saveCallback || this.saveCallback;
|
||||
this.message = message ?? this.message;
|
||||
this.newText = defaultName ?? "";
|
||||
this.title = title ?? this.title;
|
||||
this.visible = true;
|
||||
this.cancelCallback = cancelCallback || this.cancelCallback
|
||||
this.saveCallback = saveCallback || this.saveCallback
|
||||
this.message = message ?? this.message
|
||||
this.newText = defaultName ?? ''
|
||||
this.title = title ?? this.title
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
async onClickSaveChanges() {
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
if (this.saveCallback) {
|
||||
this.saveCallback(this.newText);
|
||||
this.saveCallback(this.newText)
|
||||
}
|
||||
}
|
||||
|
||||
onClickCancel() {
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
if (this.cancelCallback) {
|
||||
this.cancelCallback();
|
||||
this.cancelCallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,15 +61,15 @@ backup and database export, with platform-specific download instructions. * *
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { Component, Prop, Vue } from 'vue-facing-decorator'
|
||||
import { NotificationIface } from '../constants/app'
|
||||
import { db } from '../db/index'
|
||||
import { logger } from '../utils/logger'
|
||||
import { PlatformServiceFactory } from '../services/PlatformServiceFactory'
|
||||
import {
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} from "../services/PlatformService";
|
||||
PlatformCapabilities
|
||||
} from '../services/PlatformService'
|
||||
|
||||
/**
|
||||
* @vue-component
|
||||
@@ -82,33 +82,33 @@ export default class DataExportSection extends Vue {
|
||||
* Notification function injected by Vue
|
||||
* 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
|
||||
* Controls visibility of seed backup option
|
||||
* @required
|
||||
*/
|
||||
@Prop({ required: true }) readonly activeDid!: string;
|
||||
@Prop({ required: true }) readonly activeDid!: string
|
||||
|
||||
/**
|
||||
* URL for the database export download
|
||||
* Created and revoked dynamically during export process
|
||||
* Only used in web platform
|
||||
*/
|
||||
downloadUrl = "";
|
||||
downloadUrl = ''
|
||||
|
||||
/**
|
||||
* Platform service instance for platform-specific operations
|
||||
*/
|
||||
private platformService: PlatformService =
|
||||
PlatformServiceFactory.getInstance();
|
||||
PlatformServiceFactory.getInstance()
|
||||
|
||||
/**
|
||||
* Platform capabilities for the current platform
|
||||
*/
|
||||
private get platformCapabilities(): PlatformCapabilities {
|
||||
return this.platformService.getCapabilities();
|
||||
return this.platformService.getCapabilities()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +117,7 @@ export default class DataExportSection extends Vue {
|
||||
*/
|
||||
beforeUnmount() {
|
||||
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() {
|
||||
try {
|
||||
const blob = await db.export({ prettyJson: true });
|
||||
const fileName = `${db.name}-backup.json`;
|
||||
const blob = await db.export({ prettyJson: true })
|
||||
const fileName = `${db.name}-backup.json`
|
||||
|
||||
if (this.platformCapabilities.hasFileDownload) {
|
||||
// Web platform: Use download link
|
||||
this.downloadUrl = URL.createObjectURL(blob);
|
||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
||||
downloadAnchor.href = this.downloadUrl;
|
||||
downloadAnchor.download = fileName;
|
||||
downloadAnchor.click();
|
||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
||||
this.downloadUrl = URL.createObjectURL(blob)
|
||||
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement
|
||||
downloadAnchor.href = this.downloadUrl
|
||||
downloadAnchor.download = fileName
|
||||
downloadAnchor.click()
|
||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000)
|
||||
} else if (this.platformCapabilities.hasFileSystem) {
|
||||
// Native platform: Write to app directory
|
||||
const content = await blob.text();
|
||||
await this.platformService.writeAndShareFile(fileName, content);
|
||||
const content = await blob.text()
|
||||
await this.platformService.writeAndShareFile(fileName, content)
|
||||
}
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Export Successful",
|
||||
group: 'alert',
|
||||
type: 'success',
|
||||
title: 'Export Successful',
|
||||
text: this.platformCapabilities.hasFileDownload
|
||||
? "See your downloads directory for the backup. It is in the Dexie format."
|
||||
: "Please choose a location to save your backup file.",
|
||||
? 'See your downloads directory for the backup. It is in the Dexie format.'
|
||||
: 'Please choose a location to save your backup file.'
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error("Export Error:", error);
|
||||
logger.error('Export Error:', error)
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Export Error",
|
||||
text: "There was an error exporting the data.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Export Error',
|
||||
text: 'There was an error exporting the data.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
3000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,8 +179,8 @@ export default class DataExportSection extends Vue {
|
||||
*/
|
||||
public computedStartDownloadLinkClassNames() {
|
||||
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() {
|
||||
return {
|
||||
hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload,
|
||||
};
|
||||
hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
<template>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="w-fit" v-html="generateIcon()"></div>
|
||||
<div class="w-fit" v-html="generateIcon()" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { createAvatar, StyleOptions } from "@dicebear/core";
|
||||
import { avataaars } from "@dicebear/collection";
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { createAvatar, StyleOptions } from '@dicebear/core'
|
||||
import { avataaars } from '@dicebear/collection'
|
||||
import { Vue, Component, Prop } from 'vue-facing-decorator'
|
||||
import { Contact } from '../db/tables/contacts'
|
||||
|
||||
@Component
|
||||
export default class EntityIcon extends Vue {
|
||||
@Prop contact: Contact;
|
||||
@Prop entityId = ""; // overridden by contact.did or profileImageUrl
|
||||
@Prop iconSize = 0;
|
||||
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
|
||||
@Prop contact: Contact
|
||||
@Prop entityId = '' // overridden by contact.did or profileImageUrl
|
||||
@Prop iconSize = 0
|
||||
@Prop profileImageUrl = '' // overridden by contact.profileImageUrl
|
||||
|
||||
generateIcon() {
|
||||
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
|
||||
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl
|
||||
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 {
|
||||
const identifier = this.contact?.did || this.entityId;
|
||||
const identifier = this.contact?.did || this.entityId
|
||||
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=
|
||||
// ... 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
|
||||
// ... which looks similar to '' at the dicebear site but which is different.
|
||||
const options: StyleOptions<object> = {
|
||||
seed: (identifier as string) || "",
|
||||
size: this.iconSize,
|
||||
};
|
||||
const avatar = createAvatar(avataaars, options);
|
||||
const svgString = avatar.toString();
|
||||
return svgString;
|
||||
seed: (identifier as string) || '',
|
||||
size: this.iconSize
|
||||
}
|
||||
const avatar = createAvatar(avataaars, options)
|
||||
const svgString = avatar.toString()
|
||||
return svgString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
class="sr-only"
|
||||
/>
|
||||
<!-- 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 -->
|
||||
<div
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
class="sr-only"
|
||||
/>
|
||||
<!-- 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 -->
|
||||
<div
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="relative ml-2">
|
||||
<button class="ml-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500">
|
||||
@@ -91,101 +91,96 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
import {
|
||||
LMap,
|
||||
LMarker,
|
||||
LRectangle,
|
||||
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";
|
||||
import { Vue, Component } from 'vue-facing-decorator'
|
||||
import { LMap, LMarker, LRectangle, 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({
|
||||
components: {
|
||||
LRectangle,
|
||||
LMap,
|
||||
LMarker,
|
||||
LTileLayer,
|
||||
},
|
||||
LTileLayer
|
||||
}
|
||||
})
|
||||
export default class FeedFilters extends Vue {
|
||||
$router!: Router;
|
||||
onCloseIfChanged = () => {};
|
||||
hasSearchBox = false;
|
||||
hasVisibleDid = false;
|
||||
isNearby = false;
|
||||
settingChanged = false;
|
||||
visible = false;
|
||||
$router!: Router
|
||||
onCloseIfChanged = () => {}
|
||||
hasSearchBox = false
|
||||
hasVisibleDid = false
|
||||
isNearby = false
|
||||
settingChanged = false
|
||||
visible = false
|
||||
|
||||
async open(onCloseIfChanged: () => void) {
|
||||
this.onCloseIfChanged = onCloseIfChanged;
|
||||
this.onCloseIfChanged = onCloseIfChanged
|
||||
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.hasVisibleDid = !!settings.filterFeedByVisible;
|
||||
this.isNearby = !!settings.filterFeedByNearby;
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
this.hasVisibleDid = !!settings.filterFeedByVisible
|
||||
this.isNearby = !!settings.filterFeedByNearby
|
||||
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
||||
this.hasSearchBox = true;
|
||||
this.hasSearchBox = true
|
||||
}
|
||||
|
||||
this.settingChanged = false;
|
||||
this.visible = true;
|
||||
this.settingChanged = false
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
async toggleHasVisibleDid() {
|
||||
this.settingChanged = true;
|
||||
this.hasVisibleDid = !this.hasVisibleDid;
|
||||
this.settingChanged = true
|
||||
this.hasVisibleDid = !this.hasVisibleDid
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByVisible: this.hasVisibleDid,
|
||||
});
|
||||
filterFeedByVisible: this.hasVisibleDid
|
||||
})
|
||||
}
|
||||
|
||||
async toggleNearby() {
|
||||
this.settingChanged = true;
|
||||
this.isNearby = !this.isNearby;
|
||||
this.settingChanged = true
|
||||
this.isNearby = !this.isNearby
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: this.isNearby,
|
||||
});
|
||||
filterFeedByNearby: this.isNearby
|
||||
})
|
||||
}
|
||||
|
||||
async clearAll() {
|
||||
if (this.hasVisibleDid || this.isNearby) {
|
||||
this.settingChanged = true;
|
||||
this.settingChanged = true
|
||||
}
|
||||
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: false,
|
||||
filterFeedByVisible: false,
|
||||
});
|
||||
filterFeedByVisible: false
|
||||
})
|
||||
|
||||
this.hasVisibleDid = false;
|
||||
this.isNearby = false;
|
||||
this.hasVisibleDid = false
|
||||
this.isNearby = false
|
||||
}
|
||||
|
||||
async setAll() {
|
||||
if (!this.hasVisibleDid || !this.isNearby) {
|
||||
this.settingChanged = true;
|
||||
this.settingChanged = true
|
||||
}
|
||||
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: true,
|
||||
filterFeedByVisible: true,
|
||||
});
|
||||
filterFeedByVisible: true
|
||||
})
|
||||
|
||||
this.hasVisibleDid = true;
|
||||
this.isNearby = true;
|
||||
this.hasVisibleDid = true
|
||||
this.isNearby = true
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.settingChanged) {
|
||||
this.onCloseIfChanged();
|
||||
this.onCloseIfChanged()
|
||||
}
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
}
|
||||
|
||||
done() {
|
||||
this.close();
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
providerProjectId: fromProjectId,
|
||||
recipientDid: receiver?.did,
|
||||
recipientName: receiver?.name,
|
||||
unitCode,
|
||||
},
|
||||
unitCode
|
||||
}
|
||||
}"
|
||||
class="text-blue-500"
|
||||
>
|
||||
@@ -87,44 +87,44 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
serverMessageForUser,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
serverMessageForUser
|
||||
} from '../libs/endorserServer'
|
||||
import * as libsUtil from '../libs/util'
|
||||
import { db, retrieveSettingsForActiveAccount } from '../db/index'
|
||||
import { Contact } from '../db/tables/contacts'
|
||||
import { retrieveAccountDids } from '../libs/util'
|
||||
|
||||
@Component
|
||||
export default class GiftedDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
|
||||
@Prop() fromProjectId = "";
|
||||
@Prop() toProjectId = "";
|
||||
@Prop() fromProjectId = ''
|
||||
@Prop() toProjectId = ''
|
||||
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: Array<string> = [];
|
||||
apiServer = "";
|
||||
activeDid = ''
|
||||
allContacts: Array<Contact> = []
|
||||
allMyDids: Array<string> = []
|
||||
apiServer = ''
|
||||
|
||||
amountInput = "0";
|
||||
callbackOnSuccess?: (amount: number) => void = () => {};
|
||||
customTitle?: string;
|
||||
description = "";
|
||||
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
|
||||
isTrade = false;
|
||||
offerId = "";
|
||||
prompt = "";
|
||||
receiver?: libsUtil.GiverReceiverInputInfo;
|
||||
unitCode = "HUR";
|
||||
visible = false;
|
||||
amountInput = '0'
|
||||
callbackOnSuccess?: (amount: number) => void = () => {}
|
||||
customTitle?: string
|
||||
description = ''
|
||||
giver?: libsUtil.GiverReceiverInputInfo // undefined means no identified giver agent
|
||||
isTrade = false
|
||||
offerId = ''
|
||||
prompt = ''
|
||||
receiver?: libsUtil.GiverReceiverInputInfo
|
||||
unitCode = 'HUR'
|
||||
visible = false
|
||||
|
||||
libsUtil = libsUtil;
|
||||
libsUtil = libsUtil
|
||||
|
||||
async open(
|
||||
giver?: libsUtil.GiverReceiverInputInfo,
|
||||
@@ -132,146 +132,143 @@ export default class GiftedDialog extends Vue {
|
||||
offerId?: string,
|
||||
customTitle?: string,
|
||||
prompt?: string,
|
||||
callbackOnSuccess: (amount: number) => void = () => {},
|
||||
callbackOnSuccess: (amount: number) => void = () => {}
|
||||
) {
|
||||
this.customTitle = customTitle;
|
||||
this.giver = giver;
|
||||
this.prompt = prompt || "";
|
||||
this.receiver = receiver;
|
||||
this.customTitle = customTitle
|
||||
this.giver = giver
|
||||
this.prompt = prompt || ''
|
||||
this.receiver = receiver
|
||||
// if we show "given to user" selection, default checkbox to true
|
||||
this.amountInput = "0";
|
||||
this.callbackOnSuccess = callbackOnSuccess;
|
||||
this.offerId = offerId || "";
|
||||
this.amountInput = '0'
|
||||
this.callbackOnSuccess = callbackOnSuccess
|
||||
this.offerId = offerId || ''
|
||||
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
this.apiServer = settings.apiServer || ''
|
||||
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) {
|
||||
this.giver.name = didInfo(
|
||||
this.giver.did,
|
||||
this.activeDid,
|
||||
this.allMyDids,
|
||||
this.allContacts,
|
||||
);
|
||||
this.allContacts
|
||||
)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
logger.error('Error retrieving settings from database:', err)
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: err.message || "There was an error retrieving your settings.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: err.message || 'There was an error retrieving your settings.'
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
}
|
||||
|
||||
this.visible = true;
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
close() {
|
||||
// close the dialog but don't change values (since it might be submitting info)
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
}
|
||||
|
||||
changeUnitCode() {
|
||||
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||
const index = units.indexOf(this.unitCode);
|
||||
this.unitCode = units[(index + 1) % units.length];
|
||||
const units = Object.keys(this.libsUtil.UNIT_SHORT)
|
||||
const index = units.indexOf(this.unitCode)
|
||||
this.unitCode = units[(index + 1) % units.length]
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`
|
||||
}
|
||||
|
||||
decrement() {
|
||||
this.amountInput = `${Math.max(
|
||||
0,
|
||||
(parseFloat(this.amountInput) || 1) - 1,
|
||||
)}`;
|
||||
this.amountInput = `${Math.max(0, (parseFloat(this.amountInput) || 1) - 1)}`
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.close();
|
||||
this.eraseValues();
|
||||
this.close()
|
||||
this.eraseValues()
|
||||
}
|
||||
|
||||
eraseValues() {
|
||||
this.description = "";
|
||||
this.giver = undefined;
|
||||
this.amountInput = "0";
|
||||
this.prompt = "";
|
||||
this.unitCode = "HUR";
|
||||
this.description = ''
|
||||
this.giver = undefined
|
||||
this.amountInput = '0'
|
||||
this.prompt = ''
|
||||
this.unitCode = 'HUR'
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must select an identifier before you can record a give.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: 'You must select an identifier before you can record a give.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
return;
|
||||
3000
|
||||
)
|
||||
return
|
||||
}
|
||||
if (parseFloat(this.amountInput) < 0) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
text: "You may not send a negative number.",
|
||||
title: "",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
text: 'You may not send a negative number.',
|
||||
title: ''
|
||||
},
|
||||
2000,
|
||||
);
|
||||
return;
|
||||
2000
|
||||
)
|
||||
return
|
||||
}
|
||||
if (!this.description && !parseFloat(this.amountInput)) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: `You must enter a description or some number of ${
|
||||
this.libsUtil.UNIT_LONG[this.unitCode]
|
||||
}.`,
|
||||
}.`
|
||||
},
|
||||
2000,
|
||||
);
|
||||
return;
|
||||
2000
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
this.close();
|
||||
this.close()
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
text: "Recording the give...",
|
||||
title: "",
|
||||
group: 'alert',
|
||||
type: 'toast',
|
||||
text: 'Recording the give...',
|
||||
title: ''
|
||||
},
|
||||
1000,
|
||||
);
|
||||
1000
|
||||
)
|
||||
// this is asynchronous, but we don't need to wait for it to complete
|
||||
await this.recordGive(
|
||||
(this.giver?.did as string) || null,
|
||||
(this.receiver?.did as string) || null,
|
||||
this.description,
|
||||
parseFloat(this.amountInput),
|
||||
this.unitCode,
|
||||
this.unitCode
|
||||
).then(() => {
|
||||
this.eraseValues();
|
||||
});
|
||||
this.eraseValues()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,7 +284,7 @@ export default class GiftedDialog extends Vue {
|
||||
recipientDid: string | null,
|
||||
description: string,
|
||||
amount: number,
|
||||
unitCode: string = "HUR",
|
||||
unitCode: string = 'HUR'
|
||||
) {
|
||||
try {
|
||||
const result = await createAndSubmitGive(
|
||||
@@ -303,54 +300,54 @@ export default class GiftedDialog extends Vue {
|
||||
this.offerId,
|
||||
this.isTrade,
|
||||
undefined,
|
||||
this.fromProjectId,
|
||||
);
|
||||
this.fromProjectId
|
||||
)
|
||||
|
||||
if (
|
||||
result.type === "error" ||
|
||||
result.type === 'error' ||
|
||||
this.isGiveCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
logger.error("Error with give creation result:", result);
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result)
|
||||
logger.error('Error with give creation result:', result)
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: errorMessage || "There was an error creating the give.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: errorMessage || 'There was an error creating the give.'
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
text: `That ${this.isTrade ? "trade" : "gift"} was recorded.`,
|
||||
group: 'alert',
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
text: `That ${this.isTrade ? 'trade' : 'gift'} was recorded.`
|
||||
},
|
||||
7000,
|
||||
);
|
||||
7000
|
||||
)
|
||||
if (this.callbackOnSuccess) {
|
||||
this.callbackOnSuccess(amount);
|
||||
this.callbackOnSuccess(amount)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
logger.error("Error with give recordation caught:", error);
|
||||
logger.error('Error with give recordation caught:', error)
|
||||
const errorMessage =
|
||||
error.userMessage ||
|
||||
serverMessageForUser(error) ||
|
||||
"There was an error recording the give.";
|
||||
'There was an error recording the give.'
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: errorMessage,
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: errorMessage
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +359,7 @@ export default class GiftedDialog extends Vue {
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-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?.error ||
|
||||
result.response?.data?.error?.message
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
explainData() {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Data Sharing",
|
||||
text: libsUtil.PRIVACY_MESSAGE,
|
||||
group: 'alert',
|
||||
type: 'success',
|
||||
title: 'Data Sharing',
|
||||
text: libsUtil.PRIVACY_MESSAGE
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
|
||||
@click="cancel"
|
||||
>
|
||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
||||
<font-awesome icon="xmark" class="w-[1em]" />
|
||||
</div>
|
||||
</h1>
|
||||
<span class="mt-2 flex justify-between">
|
||||
@@ -71,92 +71,92 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { Vue, Component } from 'vue-facing-decorator'
|
||||
import { Router } from 'vue-router'
|
||||
|
||||
import { AppString, NotificationIface } from "../constants/app";
|
||||
import { db } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { GiverReceiverInputInfo } from "../libs/util";
|
||||
import { AppString, NotificationIface } from '../constants/app'
|
||||
import { db } from '../db/index'
|
||||
import { Contact } from '../db/tables/contacts'
|
||||
import { GiverReceiverInputInfo } from '../libs/util'
|
||||
|
||||
@Component
|
||||
export default class GivenPrompts extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
$router!: Router
|
||||
|
||||
CATEGORY_CONTACTS = 1;
|
||||
CATEGORY_IDEAS = 0;
|
||||
CATEGORY_CONTACTS = 1
|
||||
CATEGORY_IDEAS = 0
|
||||
IDEAS = [
|
||||
"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 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?)",
|
||||
"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?)",
|
||||
"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 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?)",
|
||||
"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?)",
|
||||
"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?)",
|
||||
"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 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 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?)',
|
||||
'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?)',
|
||||
'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 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?)',
|
||||
'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?)',
|
||||
'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?)',
|
||||
'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?)'
|
||||
]
|
||||
|
||||
callbackOnFullGiftInfo?: (
|
||||
contactInfo?: GiverReceiverInputInfo,
|
||||
description?: string,
|
||||
) => void;
|
||||
currentCategory = this.CATEGORY_IDEAS; // 0 = IDEAS, 1 = CONTACTS
|
||||
currentContact: Contact | undefined = undefined;
|
||||
currentIdeaIndex = 0;
|
||||
numContacts = 0;
|
||||
shownContactDbIndices: Array<boolean> = [];
|
||||
visible = false;
|
||||
description?: string
|
||||
) => void
|
||||
currentCategory = this.CATEGORY_IDEAS // 0 = IDEAS, 1 = CONTACTS
|
||||
currentContact: Contact | undefined = undefined
|
||||
currentIdeaIndex = 0
|
||||
numContacts = 0
|
||||
shownContactDbIndices: Array<boolean> = []
|
||||
visible = false
|
||||
|
||||
AppString = AppString;
|
||||
AppString = AppString
|
||||
|
||||
async open(
|
||||
callbackOnFullGiftInfo?: (
|
||||
contactInfo?: GiverReceiverInputInfo,
|
||||
description?: string,
|
||||
) => void,
|
||||
description?: string
|
||||
) => void
|
||||
) {
|
||||
this.visible = true;
|
||||
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
|
||||
this.visible = true
|
||||
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo
|
||||
|
||||
await db.open();
|
||||
this.numContacts = await db.contacts.count();
|
||||
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
|
||||
await db.open()
|
||||
this.numContacts = await db.contacts.count()
|
||||
this.shownContactDbIndices = new Array<boolean>(this.numContacts) // all undefined to start
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.currentCategory = this.CATEGORY_IDEAS;
|
||||
this.currentContact = undefined;
|
||||
this.currentIdeaIndex = 0;
|
||||
this.numContacts = 0;
|
||||
this.shownContactDbIndices = [];
|
||||
this.currentCategory = this.CATEGORY_IDEAS
|
||||
this.currentContact = undefined
|
||||
this.currentIdeaIndex = 0
|
||||
this.numContacts = 0
|
||||
this.shownContactDbIndices = []
|
||||
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
}
|
||||
|
||||
proceed() {
|
||||
// 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) {
|
||||
this.$router.push({
|
||||
name: "contact-gift",
|
||||
name: 'contact-gift',
|
||||
query: {
|
||||
prompt: this.IDEAS[this.currentIdeaIndex],
|
||||
},
|
||||
});
|
||||
prompt: this.IDEAS[this.currentIdeaIndex]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// must be this.CATEGORY_CONTACTS
|
||||
this.callbackOnFullGiftInfo?.(
|
||||
this.currentContact as GiverReceiverInputInfo,
|
||||
);
|
||||
this.currentContact as GiverReceiverInputInfo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,14 +167,14 @@ export default class GivenPrompts extends Vue {
|
||||
async nextIdea() {
|
||||
// check if the next one is an idea or a contact
|
||||
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
||||
this.currentIdeaIndex++;
|
||||
this.currentIdeaIndex++
|
||||
if (this.currentIdeaIndex === this.IDEAS.length) {
|
||||
// must have just finished ideas so move to contacts
|
||||
this.findNextUnshownContact();
|
||||
this.findNextUnshownContact()
|
||||
}
|
||||
} else {
|
||||
// must be this.CATEGORY_CONTACTS
|
||||
this.findNextUnshownContact();
|
||||
this.findNextUnshownContact()
|
||||
// when that's finished, it'll reset to ideas
|
||||
}
|
||||
}
|
||||
@@ -186,54 +186,52 @@ export default class GivenPrompts extends Vue {
|
||||
async prevIdea() {
|
||||
// check if the next one is an idea or a contact
|
||||
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
||||
this.currentIdeaIndex--;
|
||||
this.currentIdeaIndex--
|
||||
if (this.currentIdeaIndex < 0) {
|
||||
// must have just finished ideas so move to contacts
|
||||
this.findNextUnshownContact();
|
||||
this.findNextUnshownContact()
|
||||
}
|
||||
} else {
|
||||
// must be this.CATEGORY_CONTACTS
|
||||
this.findNextUnshownContact();
|
||||
this.findNextUnshownContact()
|
||||
// when that's finished, it'll reset to ideas
|
||||
}
|
||||
}
|
||||
|
||||
nextIdeaPastContacts() {
|
||||
this.currentContact = undefined;
|
||||
this.shownContactDbIndices = new Array<boolean>(this.numContacts);
|
||||
this.currentContact = undefined
|
||||
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
|
||||
this.currentIdeaIndex =
|
||||
this.currentIdeaIndex >= this.IDEAS.length ? 0 : this.IDEAS.length - 1;
|
||||
this.currentIdeaIndex >= this.IDEAS.length ? 0 : this.IDEAS.length - 1
|
||||
}
|
||||
|
||||
async findNextUnshownContact() {
|
||||
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
||||
// 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 count = 0;
|
||||
let someContactDbIndex = Math.floor(Math.random() * this.numContacts)
|
||||
let count = 0
|
||||
// as long as the index has an entry, loop
|
||||
while (
|
||||
this.shownContactDbIndices[someContactDbIndex] != null &&
|
||||
count++ < this.numContacts
|
||||
) {
|
||||
someContactDbIndex = (someContactDbIndex + 1) % this.numContacts;
|
||||
someContactDbIndex = (someContactDbIndex + 1) % this.numContacts
|
||||
}
|
||||
if (count >= this.numContacts) {
|
||||
// all contacts have been shown
|
||||
this.nextIdeaPastContacts();
|
||||
this.nextIdeaPastContacts()
|
||||
} else {
|
||||
// get the contact at that offset
|
||||
await db.open();
|
||||
this.currentContact = await db.contacts
|
||||
.offset(someContactDbIndex)
|
||||
.first();
|
||||
this.shownContactDbIndices[someContactDbIndex] = true;
|
||||
await db.open()
|
||||
this.currentContact = await db.contacts.offset(someContactDbIndex).first()
|
||||
this.shownContactDbIndices[someContactDbIndex] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,33 +100,33 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import * as R from "ramda";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
import * as R from 'ramda'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { Contact } from '../db/tables/contacts'
|
||||
import * as serverUtil from '../libs/endorserServer'
|
||||
import { NotificationIface } from '../constants/app'
|
||||
|
||||
@Component
|
||||
export default class HiddenDidDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
|
||||
isOpen = false;
|
||||
roleName = "";
|
||||
visibleToDids: string[] = [];
|
||||
allContacts: Array<Contact> = [];
|
||||
activeDid = "";
|
||||
allMyDids: Array<string> = [];
|
||||
canShare = false;
|
||||
windowLocation = window.location.href;
|
||||
isOpen = false
|
||||
roleName = ''
|
||||
visibleToDids: string[] = []
|
||||
allContacts: Array<Contact> = []
|
||||
activeDid = ''
|
||||
allMyDids: Array<string> = []
|
||||
canShare = false
|
||||
windowLocation = window.location.href
|
||||
|
||||
R = R;
|
||||
serverUtil = serverUtil;
|
||||
R = R
|
||||
serverUtil = serverUtil
|
||||
|
||||
created() {
|
||||
// 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()
|
||||
this.canShare = !!navigator.share;
|
||||
this.canShare = !!navigator.share
|
||||
}
|
||||
|
||||
open(
|
||||
@@ -134,18 +134,18 @@ export default class HiddenDidDialog extends Vue {
|
||||
visibleToDids: string[],
|
||||
allContacts: Array<Contact>,
|
||||
activeDid: string,
|
||||
allMyDids: Array<string>,
|
||||
allMyDids: Array<string>
|
||||
) {
|
||||
this.roleName = roleName;
|
||||
this.visibleToDids = visibleToDids;
|
||||
this.allContacts = allContacts;
|
||||
this.activeDid = activeDid;
|
||||
this.allMyDids = allMyDids;
|
||||
this.isOpen = true;
|
||||
this.roleName = roleName
|
||||
this.visibleToDids = visibleToDids
|
||||
this.allContacts = allContacts
|
||||
this.activeDid = activeDid
|
||||
this.allMyDids = allMyDids
|
||||
this.isOpen = true
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.isOpen = false
|
||||
}
|
||||
|
||||
didInfo(did: string) {
|
||||
@@ -153,8 +153,8 @@ export default class HiddenDidDialog extends Vue {
|
||||
did,
|
||||
this.activeDid,
|
||||
this.allMyDids,
|
||||
this.allContacts,
|
||||
);
|
||||
this.allContacts
|
||||
)
|
||||
}
|
||||
|
||||
copyToClipboard(name: string, text: string) {
|
||||
@@ -163,23 +163,23 @@ export default class HiddenDidDialog extends Vue {
|
||||
.then(() => {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
title: "Copied",
|
||||
text: (name || "That") + " was copied to the clipboard.",
|
||||
group: 'alert',
|
||||
type: 'toast',
|
||||
title: 'Copied',
|
||||
text: (name || 'That') + ' was copied to the clipboard.'
|
||||
},
|
||||
2000,
|
||||
);
|
||||
});
|
||||
2000
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
onClickShareClaim() {
|
||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
||||
this.copyToClipboard('A link to this page', this.windowLocation)
|
||||
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?",
|
||||
url: this.windowLocation,
|
||||
});
|
||||
url: this.windowLocation
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
||||
@click="close()"
|
||||
>
|
||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
||||
<font-awesome icon="xmark" class="w-[1em]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,103 +56,102 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import axios from "axios";
|
||||
import { ref } from "vue";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import axios from 'axios'
|
||||
import { ref } from 'vue'
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
|
||||
import PhotoDialog from "../components/PhotoDialog.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import PhotoDialog from '../components/PhotoDialog.vue'
|
||||
import { NotificationIface } from '../constants/app'
|
||||
|
||||
const inputImageFileNameRef = ref<Blob>();
|
||||
const inputImageFileNameRef = ref<Blob>()
|
||||
|
||||
@Component({
|
||||
components: { PhotoDialog },
|
||||
components: { PhotoDialog }
|
||||
})
|
||||
export default class ImageMethodDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
|
||||
claimType: string;
|
||||
crop: boolean = false;
|
||||
imageCallback: (imageUrl?: string) => void = () => {};
|
||||
imageUrl?: string;
|
||||
visible = false;
|
||||
claimType: string
|
||||
crop: boolean = false
|
||||
imageCallback: (imageUrl?: string) => void = () => {}
|
||||
imageUrl?: string
|
||||
visible = false
|
||||
|
||||
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
|
||||
this.claimType = claimType;
|
||||
this.crop = !!crop;
|
||||
this.imageCallback = setImageFn;
|
||||
this.claimType = claimType
|
||||
this.crop = !!crop
|
||||
this.imageCallback = setImageFn
|
||||
|
||||
this.visible = true;
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
openPhotoDialog(blob?: Blob, fileName?: string) {
|
||||
this.visible = false;
|
||||
|
||||
(this.$refs.photoDialog as PhotoDialog).open(
|
||||
this.visible = false
|
||||
;(this.$refs.photoDialog as PhotoDialog).open(
|
||||
this.imageCallback,
|
||||
this.claimType,
|
||||
this.crop,
|
||||
blob,
|
||||
fileName,
|
||||
);
|
||||
fileName
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
// ... plus it has a `type` property from my testing
|
||||
const file = inputImageFileNameRef.value;
|
||||
const file = inputImageFileNameRef.value
|
||||
if (file != null) {
|
||||
const reader = new FileReader();
|
||||
const reader = new FileReader()
|
||||
reader.onload = async (e) => {
|
||||
const data = e.target?.result as ArrayBuffer;
|
||||
const data = e.target?.result as ArrayBuffer
|
||||
if (data) {
|
||||
const blob = new Blob([new Uint8Array(data)], {
|
||||
type: file.type,
|
||||
});
|
||||
this.openPhotoDialog(blob, file.name as string);
|
||||
type: file.type
|
||||
})
|
||||
this.openPhotoDialog(blob, file.name as string)
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file as Blob);
|
||||
}
|
||||
reader.readAsArrayBuffer(file as Blob)
|
||||
}
|
||||
}
|
||||
|
||||
async acceptUrl() {
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
if (this.crop) {
|
||||
try {
|
||||
const urlBlobResponse: Blob = await axios.get(this.imageUrl as string, {
|
||||
responseType: "blob", // This ensures the data is returned as a Blob
|
||||
});
|
||||
const fullUrl = new URL(this.imageUrl as string);
|
||||
const fileName = fullUrl.pathname.split("/").pop() as string;
|
||||
(this.$refs.photoDialog as PhotoDialog).open(
|
||||
responseType: 'blob' // This ensures the data is returned as a Blob
|
||||
})
|
||||
const fullUrl = new URL(this.imageUrl as string)
|
||||
const fileName = fullUrl.pathname.split('/').pop() as string
|
||||
;(this.$refs.photoDialog as PhotoDialog).open(
|
||||
this.imageCallback,
|
||||
this.claimType,
|
||||
this.crop,
|
||||
urlBlobResponse.data as Blob,
|
||||
fileName,
|
||||
);
|
||||
fileName
|
||||
)
|
||||
} catch (error) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error retrieving that image.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: 'There was an error retrieving that image.'
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this.imageCallback(this.imageUrl);
|
||||
this.imageCallback(this.imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -38,44 +38,44 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { Component, Vue, Prop } from 'vue-facing-decorator'
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
|
||||
@Component({ emits: ["update:isOpen"] })
|
||||
@Component({ emits: ['update:isOpen'] })
|
||||
export default class ImageViewer extends Vue {
|
||||
@Prop() imageUrl!: string;
|
||||
@Prop() imageData!: Blob | null;
|
||||
@Prop() isOpen!: boolean;
|
||||
@Prop() imageUrl!: string
|
||||
@Prop() imageData!: Blob | null
|
||||
@Prop() isOpen!: boolean
|
||||
|
||||
userAgent = new UAParser();
|
||||
userAgent = new UAParser()
|
||||
|
||||
get isMobile() {
|
||||
const os = this.userAgent.getOS().name;
|
||||
return os === "iOS" || os === "Android";
|
||||
const os = this.userAgent.getOS().name
|
||||
return os === 'iOS' || os === 'Android'
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit("update:isOpen", false);
|
||||
this.$emit('update:isOpen', false)
|
||||
}
|
||||
|
||||
async handleShare() {
|
||||
const os = this.userAgent.getOS().name;
|
||||
const os = this.userAgent.getOS().name
|
||||
|
||||
try {
|
||||
if (os === "iOS" || os === "Android") {
|
||||
if (os === 'iOS' || os === 'Android') {
|
||||
if (navigator.share) {
|
||||
// Always share the URL since it's more reliable across platforms
|
||||
await navigator.share({
|
||||
url: this.imageUrl,
|
||||
});
|
||||
url: this.imageUrl
|
||||
})
|
||||
} else {
|
||||
// Fallback for browsers without share API
|
||||
window.open(this.imageUrl, "_blank");
|
||||
window.open(this.imageUrl, '_blank')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn("Share failed, opening in new tab:", error);
|
||||
window.open(this.imageUrl, "_blank");
|
||||
logger.warn('Share failed, opening in new tab:', error)
|
||||
window.open(this.imageUrl, '_blank')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ loading state management. * * @author Matthew Raymer * @version 1.0.0 */
|
||||
<template>
|
||||
<div ref="scrollContainer">
|
||||
<slot />
|
||||
<div ref="sentinel" style="height: 1px"></div>
|
||||
<div ref="sentinel" style="height: 1px" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Emit, Prop, Vue } from "vue-facing-decorator";
|
||||
import { Component, Emit, Prop, Vue } from 'vue-facing-decorator'
|
||||
|
||||
/**
|
||||
* InfiniteScroll Component
|
||||
@@ -38,19 +38,19 @@ import { Component, Emit, Prop, Vue } from "vue-facing-decorator";
|
||||
export default class InfiniteScroll extends Vue {
|
||||
/** Distance in pixels from the bottom at which to trigger the reached-bottom event */
|
||||
@Prop({ default: 200 })
|
||||
readonly distance!: number;
|
||||
readonly distance!: number
|
||||
|
||||
/** Intersection Observer instance for detecting scroll position */
|
||||
private observer!: IntersectionObserver;
|
||||
private observer!: IntersectionObserver
|
||||
|
||||
/** Flag to track initial render state */
|
||||
private isInitialRender = true;
|
||||
private isInitialRender = true
|
||||
|
||||
/** Flag to prevent multiple simultaneous loading states */
|
||||
private isLoading = false;
|
||||
private isLoading = false
|
||||
|
||||
/** Timeout ID for debouncing scroll events */
|
||||
private debounceTimeout: number | null = null;
|
||||
private debounceTimeout: number | null = null
|
||||
|
||||
/**
|
||||
* Vue lifecycle hook that runs after component updates.
|
||||
@@ -64,13 +64,10 @@ export default class InfiniteScroll extends Vue {
|
||||
const options = {
|
||||
root: null,
|
||||
rootMargin: `0px 0px ${this.distance}px 0px`,
|
||||
threshold: 1.0,
|
||||
};
|
||||
this.observer = new IntersectionObserver(
|
||||
this.handleIntersection,
|
||||
options,
|
||||
);
|
||||
this.observer.observe(this.$refs.sentinel as HTMLElement);
|
||||
threshold: 1.0
|
||||
}
|
||||
this.observer = new IntersectionObserver(this.handleIntersection, options)
|
||||
this.observer.observe(this.$refs.sentinel as HTMLElement)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +80,10 @@ export default class InfiniteScroll extends Vue {
|
||||
*/
|
||||
beforeUnmount() {
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer.disconnect()
|
||||
}
|
||||
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
|
||||
* @emits reached-bottom - Emitted when the user scrolls near the bottom
|
||||
*/
|
||||
@Emit("reached-bottom")
|
||||
@Emit('reached-bottom')
|
||||
handleIntersection(entries: IntersectionObserverEntry[]) {
|
||||
const entry = entries[0];
|
||||
const entry = entries[0]
|
||||
if (entry.isIntersecting && !this.isLoading) {
|
||||
// Debounce the intersection event
|
||||
if (this.debounceTimeout) {
|
||||
window.clearTimeout(this.debounceTimeout);
|
||||
window.clearTimeout(this.debounceTimeout)
|
||||
}
|
||||
|
||||
this.debounceTimeout = window.setTimeout(() => {
|
||||
this.isLoading = true;
|
||||
this.$emit("reached-bottom", true);
|
||||
this.isLoading = true
|
||||
this.$emit('reached-bottom', true)
|
||||
// Reset loading state after a short delay
|
||||
setTimeout(() => {
|
||||
this.isLoading = false;
|
||||
}, 1000);
|
||||
}, 300);
|
||||
this.isLoading = false
|
||||
}, 1000)
|
||||
}, 300)
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -46,50 +46,50 @@
|
||||
</template>
|
||||
|
||||
<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
|
||||
export default class InviteDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
|
||||
callback: (text: string, expiresAt: string) => void = () => {};
|
||||
inviteIdentifier = "";
|
||||
text = "";
|
||||
visible = false;
|
||||
callback: (text: string, expiresAt: string) => void = () => {}
|
||||
inviteIdentifier = ''
|
||||
text = ''
|
||||
visible = false
|
||||
expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7)
|
||||
.toISOString()
|
||||
.substring(0, 10);
|
||||
.substring(0, 10)
|
||||
|
||||
async open(
|
||||
inviteIdentifier: string,
|
||||
aCallback: (text: string, expiresAt: string) => void,
|
||||
aCallback: (text: string, expiresAt: string) => void
|
||||
) {
|
||||
this.callback = aCallback;
|
||||
this.inviteIdentifier = inviteIdentifier;
|
||||
this.visible = true;
|
||||
this.callback = aCallback
|
||||
this.inviteIdentifier = inviteIdentifier
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
async onClickSaveChanges() {
|
||||
if (!this.expiresAt) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Needs Expiration",
|
||||
text: "You must select an expiration date.",
|
||||
group: 'alert',
|
||||
type: 'warning',
|
||||
title: 'Needs Expiration',
|
||||
text: 'You must select an expiration date.'
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
} else {
|
||||
this.callback(this.text, this.expiresAt);
|
||||
this.visible = false;
|
||||
this.callback(this.text, this.expiresAt)
|
||||
this.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
onClickCancel() {
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -80,7 +80,9 @@
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<h3 class="text-lg font-medium">{{ member.name }}</h3>
|
||||
<h3 class="text-lg font-medium">
|
||||
{{ member.name }}
|
||||
</h3>
|
||||
<div
|
||||
v-if="!getContactFor(member.did) && member.did !== activeDid"
|
||||
class="flex justify-end"
|
||||
@@ -99,7 +101,7 @@
|
||||
title="Contact info"
|
||||
@click="
|
||||
informAboutAddingContact(
|
||||
getContactFor(member.did) !== undefined,
|
||||
getContactFor(member.did) !== undefined
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -157,138 +159,138 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { Component, Vue, Prop } from 'vue-facing-decorator'
|
||||
|
||||
import {
|
||||
logConsoleAndDb,
|
||||
retrieveSettingsForActiveAccount,
|
||||
db,
|
||||
} from "../db/index";
|
||||
db
|
||||
} from '../db/index'
|
||||
import {
|
||||
errorStringForLog,
|
||||
getHeaders,
|
||||
register,
|
||||
serverMessageForUser,
|
||||
} from "../libs/endorserServer";
|
||||
import { decryptMessage } from "../libs/crypto";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
serverMessageForUser
|
||||
} from '../libs/endorserServer'
|
||||
import { decryptMessage } from '../libs/crypto'
|
||||
import { Contact } from '../db/tables/contacts'
|
||||
import * as libsUtil from '../libs/util'
|
||||
import { NotificationIface } from '../constants/app'
|
||||
|
||||
interface Member {
|
||||
admitted: boolean;
|
||||
content: string;
|
||||
memberId: number;
|
||||
admitted: boolean
|
||||
content: string
|
||||
memberId: number
|
||||
}
|
||||
|
||||
interface DecryptedMember {
|
||||
member: Member;
|
||||
name: string;
|
||||
did: string;
|
||||
isRegistered: boolean;
|
||||
member: Member
|
||||
name: string
|
||||
did: string
|
||||
isRegistered: boolean
|
||||
}
|
||||
|
||||
@Component
|
||||
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({ default: false }) showOrganizerTools!: boolean;
|
||||
@Prop({ required: true }) password!: string
|
||||
@Prop({ default: false }) showOrganizerTools!: boolean
|
||||
|
||||
decryptedMembers: DecryptedMember[] = [];
|
||||
firstName = "";
|
||||
isLoading = true;
|
||||
isOrganizer = false;
|
||||
members: Member[] = [];
|
||||
missingPassword = false;
|
||||
missingMyself = false;
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
contacts: Array<Contact> = [];
|
||||
decryptedMembers: DecryptedMember[] = []
|
||||
firstName = ''
|
||||
isLoading = true
|
||||
isOrganizer = false
|
||||
members: Member[] = []
|
||||
missingPassword = false
|
||||
missingMyself = false
|
||||
activeDid = ''
|
||||
apiServer = ''
|
||||
contacts: Array<Contact> = []
|
||||
|
||||
async created() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.firstName = settings.firstName || "";
|
||||
await this.fetchMembers();
|
||||
await this.loadContacts();
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
this.activeDid = settings.activeDid || ''
|
||||
this.apiServer = settings.apiServer || ''
|
||||
this.firstName = settings.firstName || ''
|
||||
await this.fetchMembers()
|
||||
await this.loadContacts()
|
||||
}
|
||||
|
||||
async fetchMembers() {
|
||||
try {
|
||||
this.isLoading = true;
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
this.isLoading = true
|
||||
const headers = await getHeaders(this.activeDid)
|
||||
const response = await this.axios.get(
|
||||
`${this.apiServer}/api/partner/groupOnboardMembers`,
|
||||
{ headers },
|
||||
);
|
||||
{ headers }
|
||||
)
|
||||
|
||||
if (response.data && response.data.data) {
|
||||
this.members = response.data.data;
|
||||
await this.decryptMemberContents();
|
||||
this.members = response.data.data
|
||||
await this.decryptMemberContents()
|
||||
}
|
||||
} catch (error) {
|
||||
logConsoleAndDb(
|
||||
"Error fetching members: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
'Error fetching members: ' + errorStringForLog(error),
|
||||
true
|
||||
)
|
||||
this.$emit(
|
||||
"error",
|
||||
serverMessageForUser(error) || "Failed to fetch members.",
|
||||
);
|
||||
'error',
|
||||
serverMessageForUser(error) || 'Failed to fetch members.'
|
||||
)
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async decryptMemberContents() {
|
||||
this.decryptedMembers = [];
|
||||
this.decryptedMembers = []
|
||||
|
||||
if (!this.password) {
|
||||
this.missingPassword = true;
|
||||
return;
|
||||
this.missingPassword = true
|
||||
return
|
||||
}
|
||||
|
||||
let isFirstEntry = true,
|
||||
foundMyself = false;
|
||||
foundMyself = false
|
||||
for (const member of this.members) {
|
||||
try {
|
||||
const decryptedContent = await decryptMessage(
|
||||
member.content,
|
||||
this.password,
|
||||
);
|
||||
const content = JSON.parse(decryptedContent);
|
||||
this.password
|
||||
)
|
||||
const content = JSON.parse(decryptedContent)
|
||||
|
||||
this.decryptedMembers.push({
|
||||
member: member,
|
||||
name: content.name,
|
||||
did: content.did,
|
||||
isRegistered: !!content.isRegistered,
|
||||
});
|
||||
isRegistered: !!content.isRegistered
|
||||
})
|
||||
if (isFirstEntry && content.did === this.activeDid) {
|
||||
this.isOrganizer = true;
|
||||
this.isOrganizer = true
|
||||
}
|
||||
if (content.did === this.activeDid) {
|
||||
foundMyself = true;
|
||||
foundMyself = true
|
||||
}
|
||||
} catch (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 {
|
||||
if (this.isOrganizer) {
|
||||
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 {
|
||||
// the lists must be equal
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
// 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[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 {
|
||||
// the first (organizer) member was decrypted OK
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,118 +309,118 @@ export default class MembersList extends Vue {
|
||||
membersToShow(): DecryptedMember[] {
|
||||
if (this.isOrganizer) {
|
||||
if (this.showOrganizerTools) {
|
||||
return this.decryptedMembers;
|
||||
return this.decryptedMembers
|
||||
} else {
|
||||
return this.decryptedMembers.filter(
|
||||
(member: DecryptedMember) => member.member.admitted,
|
||||
);
|
||||
(member: DecryptedMember) => member.member.admitted
|
||||
)
|
||||
}
|
||||
}
|
||||
// non-organizers only get visible members from server
|
||||
return this.decryptedMembers;
|
||||
return this.decryptedMembers
|
||||
}
|
||||
|
||||
informAboutAdmission() {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "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.",
|
||||
group: 'alert',
|
||||
type: '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."
|
||||
},
|
||||
10000,
|
||||
);
|
||||
10000
|
||||
)
|
||||
}
|
||||
|
||||
informAboutAddingContact(contactImportedAlready: boolean) {
|
||||
if (contactImportedAlready) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Contact Exists",
|
||||
text: "They are in your contacts. If you want to remove them, you must do that from the contacts screen.",
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
title: 'Contact Exists',
|
||||
text: 'They are in your contacts. If you want to remove them, you must do that from the contacts screen.'
|
||||
},
|
||||
10000,
|
||||
);
|
||||
10000
|
||||
)
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
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.",
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
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.'
|
||||
},
|
||||
10000,
|
||||
);
|
||||
10000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async loadContacts() {
|
||||
this.contacts = await db.contacts.toArray();
|
||||
this.contacts = await db.contacts.toArray()
|
||||
}
|
||||
|
||||
getContactFor(did: string): Contact | undefined {
|
||||
return this.contacts.find((contact) => contact.did === did);
|
||||
return this.contacts.find((contact) => contact.did === did)
|
||||
}
|
||||
|
||||
checkWhetherContactBeforeAdmitting(decrMember: DecryptedMember) {
|
||||
const contact = this.getContactFor(decrMember.did);
|
||||
const contact = this.getContactFor(decrMember.did)
|
||||
if (!decrMember.member.admitted && !contact) {
|
||||
// If not a contact, show confirmation dialog
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: "Add as Contact First?",
|
||||
text: "This person is not in your contacts. Would you like to add them as a contact first?",
|
||||
yesText: "Add as Contact",
|
||||
noText: "Skip Adding Contact",
|
||||
group: 'modal',
|
||||
type: 'confirm',
|
||||
title: 'Add as Contact First?',
|
||||
text: 'This person is not in your contacts. Would you like to add them as a contact first?',
|
||||
yesText: 'Add as Contact',
|
||||
noText: 'Skip Adding Contact',
|
||||
onYes: async () => {
|
||||
await this.addAsContact(decrMember);
|
||||
await this.addAsContact(decrMember)
|
||||
// After adding as contact, proceed with admission
|
||||
await this.toggleAdmission(decrMember);
|
||||
await this.toggleAdmission(decrMember)
|
||||
},
|
||||
onNo: async () => {
|
||||
// If they choose not to add as contact, show second confirmation
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
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.",
|
||||
yesText: "Continue",
|
||||
group: 'modal',
|
||||
type: 'confirm',
|
||||
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.',
|
||||
yesText: 'Continue',
|
||||
onYes: async () => {
|
||||
await this.toggleAdmission(decrMember);
|
||||
await this.toggleAdmission(decrMember)
|
||||
},
|
||||
onCancel: async () => {
|
||||
// Do nothing, effectively canceling the operation
|
||||
},
|
||||
}
|
||||
},
|
||||
-1,
|
||||
);
|
||||
},
|
||||
-1
|
||||
)
|
||||
}
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
} else {
|
||||
// If already a contact, proceed directly with admission
|
||||
this.toggleAdmission(decrMember);
|
||||
this.toggleAdmission(decrMember)
|
||||
}
|
||||
}
|
||||
|
||||
async toggleAdmission(decrMember: DecryptedMember) {
|
||||
try {
|
||||
const headers = await getHeaders(this.activeDid);
|
||||
const headers = await getHeaders(this.activeDid)
|
||||
await this.axios.put(
|
||||
`${this.apiServer}/api/partner/groupOnboardMember/${decrMember.member.memberId}`,
|
||||
{ admitted: !decrMember.member.admitted },
|
||||
{ headers },
|
||||
);
|
||||
{ headers }
|
||||
)
|
||||
// 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 (
|
||||
decrMember.member.admitted &&
|
||||
@@ -427,61 +429,61 @@ export default class MembersList extends Vue {
|
||||
) {
|
||||
const contactOldOrNew: Contact = oldContact || {
|
||||
did: decrMember.did,
|
||||
name: decrMember.name,
|
||||
};
|
||||
name: decrMember.name
|
||||
}
|
||||
try {
|
||||
const result = await register(
|
||||
this.activeDid,
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
contactOldOrNew,
|
||||
);
|
||||
contactOldOrNew
|
||||
)
|
||||
if (result.success) {
|
||||
decrMember.isRegistered = true;
|
||||
decrMember.isRegistered = true
|
||||
if (oldContact) {
|
||||
await db.contacts.update(decrMember.did, { registered: true });
|
||||
oldContact.registered = true;
|
||||
await db.contacts.update(decrMember.did, { registered: true })
|
||||
oldContact.registered = true
|
||||
}
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Registered",
|
||||
text: "Besides being admitted, they were also registered.",
|
||||
group: 'alert',
|
||||
type: 'success',
|
||||
title: 'Registered',
|
||||
text: 'Besides being admitted, they were also registered.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
3000
|
||||
)
|
||||
} else {
|
||||
throw result;
|
||||
throw result
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
// registration failure is likely explained by a message from the server
|
||||
const additionalInfo =
|
||||
serverMessageForUser(error) || error?.error || "";
|
||||
serverMessageForUser(error) || error?.error || ''
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Registration failed",
|
||||
group: 'alert',
|
||||
type: 'warning',
|
||||
title: 'Registration failed',
|
||||
text:
|
||||
"They were admitted to the meeting. However, registration failed. You can register them from the contacts screen. " +
|
||||
additionalInfo,
|
||||
'They were admitted to the meeting. However, registration failed. You can register them from the contacts screen. ' +
|
||||
additionalInfo
|
||||
},
|
||||
12000,
|
||||
);
|
||||
12000
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logConsoleAndDb(
|
||||
"Error toggling admission: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
'Error toggling admission: ' + errorStringForLog(error),
|
||||
true
|
||||
)
|
||||
this.$emit(
|
||||
"error",
|
||||
'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 {
|
||||
const newContact = {
|
||||
did: member.did,
|
||||
name: member.name,
|
||||
};
|
||||
name: member.name
|
||||
}
|
||||
|
||||
await db.contacts.add(newContact);
|
||||
this.contacts.push(newContact);
|
||||
await db.contacts.add(newContact)
|
||||
this.contacts.push(newContact)
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Contact Added",
|
||||
text: "They were added to your contacts.",
|
||||
group: 'alert',
|
||||
type: 'success',
|
||||
title: 'Contact Added',
|
||||
text: 'They were added to your contacts.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
3000
|
||||
)
|
||||
} catch (err) {
|
||||
logConsoleAndDb("Error adding contact: " + errorStringForLog(err), true);
|
||||
let message = "An error prevented adding this contact.";
|
||||
if (err instanceof Error && err.message?.indexOf("already exists") > -1) {
|
||||
message = "This person is already in your contact list.";
|
||||
logConsoleAndDb('Error adding contact: ' + errorStringForLog(err), true)
|
||||
let message = 'An error prevented adding this contact.'
|
||||
if (err instanceof Error && err.message?.indexOf('already exists') > -1) {
|
||||
message = 'This person is already in your contact list.'
|
||||
}
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Contact Not Added",
|
||||
text: message,
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Contact Not Added',
|
||||
text: message
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
projectName,
|
||||
recipientDid,
|
||||
recipientName,
|
||||
unitCode: amountUnitCode,
|
||||
},
|
||||
unitCode: amountUnitCode
|
||||
}
|
||||
}"
|
||||
class="text-blue-500"
|
||||
>
|
||||
@@ -80,117 +80,114 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
createAndSubmitOffer,
|
||||
serverMessageForUser,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
serverMessageForUser
|
||||
} from '../libs/endorserServer'
|
||||
import * as libsUtil from '../libs/util'
|
||||
import { retrieveSettingsForActiveAccount } from '../db/index'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
@Component
|
||||
export default class OfferDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
|
||||
@Prop projectId?: string;
|
||||
@Prop projectName?: string;
|
||||
@Prop projectId?: string
|
||||
@Prop projectName?: string
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
activeDid = ''
|
||||
apiServer = ''
|
||||
|
||||
amountInput = "0";
|
||||
amountUnitCode = "HUR";
|
||||
description = "";
|
||||
expirationDateInput = "";
|
||||
recipientDid? = "";
|
||||
recipientName? = "";
|
||||
visible = false;
|
||||
amountInput = '0'
|
||||
amountUnitCode = 'HUR'
|
||||
description = ''
|
||||
expirationDateInput = ''
|
||||
recipientDid? = ''
|
||||
recipientName? = ''
|
||||
visible = false
|
||||
|
||||
libsUtil = libsUtil;
|
||||
libsUtil = libsUtil
|
||||
|
||||
async open(recipientDid?: string, recipientName?: string) {
|
||||
try {
|
||||
this.recipientDid = recipientDid;
|
||||
this.recipientName = recipientName;
|
||||
this.recipientDid = recipientDid
|
||||
this.recipientName = recipientName
|
||||
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
this.apiServer = settings.apiServer || ''
|
||||
this.activeDid = settings.activeDid || ''
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
logger.error('Error retrieving settings from database:', err)
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: err.message || "There was an error retrieving your settings.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: err.message || 'There was an error retrieving your settings.'
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
}
|
||||
|
||||
this.visible = true;
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
close() {
|
||||
// close the dialog but don't change values (since it might be submitting info)
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
}
|
||||
|
||||
changeUnitCode() {
|
||||
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||
const index = units.indexOf(this.amountUnitCode);
|
||||
this.amountUnitCode = units[(index + 1) % units.length];
|
||||
const units = Object.keys(this.libsUtil.UNIT_SHORT)
|
||||
const index = units.indexOf(this.amountUnitCode)
|
||||
this.amountUnitCode = units[(index + 1) % units.length]
|
||||
}
|
||||
|
||||
increment() {
|
||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`
|
||||
}
|
||||
|
||||
decrement() {
|
||||
this.amountInput = `${Math.max(
|
||||
0,
|
||||
(parseFloat(this.amountInput) || 1) - 1,
|
||||
)}`;
|
||||
this.amountInput = `${Math.max(0, (parseFloat(this.amountInput) || 1) - 1)}`
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.close();
|
||||
this.eraseValues();
|
||||
this.close()
|
||||
this.eraseValues()
|
||||
}
|
||||
|
||||
eraseValues() {
|
||||
this.description = "";
|
||||
this.amountInput = "0";
|
||||
this.amountUnitCode = "HUR";
|
||||
this.description = ''
|
||||
this.amountInput = '0'
|
||||
this.amountUnitCode = 'HUR'
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
this.close();
|
||||
this.close()
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
text: "Recording the offer...",
|
||||
title: "",
|
||||
group: 'alert',
|
||||
type: 'toast',
|
||||
text: 'Recording the offer...',
|
||||
title: ''
|
||||
},
|
||||
1000,
|
||||
);
|
||||
1000
|
||||
)
|
||||
// this is asynchronous, but we don't need to wait for it to complete
|
||||
this.recordOffer(
|
||||
this.description,
|
||||
parseFloat(this.amountInput),
|
||||
this.amountUnitCode,
|
||||
this.expirationDateInput,
|
||||
this.expirationDateInput
|
||||
).then(() => {
|
||||
this.description = "";
|
||||
this.amountInput = "0";
|
||||
});
|
||||
this.description = ''
|
||||
this.amountInput = '0'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,33 +199,33 @@ export default class OfferDialog extends Vue {
|
||||
public async recordOffer(
|
||||
description: string,
|
||||
amount: number,
|
||||
unitCode: string = "HUR",
|
||||
expirationDateInput?: string,
|
||||
unitCode: string = 'HUR',
|
||||
expirationDateInput?: string
|
||||
) {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must select an identity before you can record an offer.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: 'You must select an identity before you can record an offer.'
|
||||
},
|
||||
7000,
|
||||
);
|
||||
return;
|
||||
7000
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!description && !amount) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`,
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: `You must enter a description or some number of ${this.libsUtil.UNIT_LONG[unitCode]}.`
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
-1
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -239,54 +236,54 @@ export default class OfferDialog extends Vue {
|
||||
description,
|
||||
amount,
|
||||
unitCode,
|
||||
"",
|
||||
'',
|
||||
expirationDateInput,
|
||||
this.recipientDid,
|
||||
this.projectId,
|
||||
);
|
||||
this.projectId
|
||||
)
|
||||
|
||||
if (
|
||||
result.type === "error" ||
|
||||
result.type === 'error' ||
|
||||
this.isOfferCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
||||
logger.error("Error with offer creation result:", result);
|
||||
const errorMessage = this.getOfferCreationErrorMessage(result)
|
||||
logger.error('Error with offer creation result:', result)
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: errorMessage || "There was an error creating the offer.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: errorMessage || 'There was an error creating the offer.'
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
text: "That offer was recorded.",
|
||||
group: 'alert',
|
||||
type: 'success',
|
||||
title: 'Success',
|
||||
text: 'That offer was recorded.'
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
logger.error("Error with offer recordation caught:", error);
|
||||
logger.error('Error with offer recordation caught:', error)
|
||||
const message =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
"There was an error recording the offer.";
|
||||
'There was an error recording the offer.'
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: message,
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: message
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +295,7 @@ export default class OfferDialog extends Vue {
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-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) ||
|
||||
result.error?.userMessage ||
|
||||
result.error?.error
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -198,64 +198,64 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
import { Router } from 'vue-router'
|
||||
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { NotificationIface } from '../constants/app'
|
||||
import {
|
||||
db,
|
||||
retrieveSettingsForActiveAccount,
|
||||
updateAccountSettings,
|
||||
} from "../db/index";
|
||||
import { OnboardPage } from "../libs/util";
|
||||
updateAccountSettings
|
||||
} from '../db/index'
|
||||
import { OnboardPage } from '../libs/util'
|
||||
|
||||
@Component({
|
||||
computed: {
|
||||
OnboardPage() {
|
||||
return OnboardPage;
|
||||
},
|
||||
return OnboardPage
|
||||
}
|
||||
},
|
||||
components: { OnboardPage },
|
||||
components: { OnboardPage }
|
||||
})
|
||||
export default class OnboardingDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
$router!: Router
|
||||
|
||||
activeDid = "";
|
||||
firstContactName = null;
|
||||
givenName = "";
|
||||
isRegistered = false;
|
||||
numContacts = 0;
|
||||
page = OnboardPage.Home;
|
||||
visible = false;
|
||||
activeDid = ''
|
||||
firstContactName = null
|
||||
givenName = ''
|
||||
isRegistered = false
|
||||
numContacts = 0
|
||||
page = OnboardPage.Home
|
||||
visible = false
|
||||
|
||||
async open(page: OnboardPage) {
|
||||
this.page = page;
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
const contacts = await db.contacts.toArray();
|
||||
this.numContacts = contacts.length;
|
||||
this.page = page
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
this.activeDid = settings.activeDid || ''
|
||||
this.isRegistered = !!settings.isRegistered
|
||||
const contacts = await db.contacts.toArray()
|
||||
this.numContacts = contacts.length
|
||||
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) {
|
||||
// we'll assume that they've been through all the other pages
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
finishedOnboarding: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async onClickClose(done?: boolean, goHome?: boolean) {
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
if (done) {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
finishedOnboarding: true
|
||||
})
|
||||
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"
|
||||
@click="close()"
|
||||
>
|
||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
||||
<font-awesome icon="xmark" class="w-[1em]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
<VuePictureCropper
|
||||
:box-style="{
|
||||
backgroundColor: '#f8f8f8',
|
||||
margin: 'auto',
|
||||
margin: 'auto'
|
||||
}"
|
||||
:img="createBlobURL(blob)"
|
||||
:options="{
|
||||
viewMode: 1,
|
||||
dragMode: 'crop',
|
||||
aspectRatio: 9 / 9,
|
||||
aspectRatio: 9 / 9
|
||||
}"
|
||||
class="max-h-[90vh] max-w-[90vw] object-contain"
|
||||
/>
|
||||
@@ -100,47 +100,47 @@
|
||||
* @file PhotoDialog.vue
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import axios from 'axios'
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
import VuePictureCropper, { cropper } from 'vue-picture-cropper'
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from '../constants/app'
|
||||
import { retrieveSettingsForActiveAccount } from '../db/index'
|
||||
import { accessToken } from '../libs/crypto'
|
||||
import { logger } from '../utils/logger'
|
||||
import { PlatformServiceFactory } from '../services/PlatformServiceFactory'
|
||||
|
||||
@Component({ components: { VuePictureCropper } })
|
||||
export default class PhotoDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
|
||||
activeDid = "";
|
||||
blob?: Blob;
|
||||
claimType = "";
|
||||
crop = false;
|
||||
fileName?: string;
|
||||
setImageCallback: (arg: string) => void = () => {};
|
||||
showRetry = true;
|
||||
uploading = false;
|
||||
visible = false;
|
||||
activeDid = ''
|
||||
blob?: Blob
|
||||
claimType = ''
|
||||
crop = false
|
||||
fileName?: string
|
||||
setImageCallback: (arg: string) => void = () => {}
|
||||
showRetry = true
|
||||
uploading = false
|
||||
visible = false
|
||||
|
||||
private platformService = PlatformServiceFactory.getInstance();
|
||||
URL = window.URL || window.webkitURL;
|
||||
private platformService = PlatformServiceFactory.getInstance()
|
||||
URL = window.URL || window.webkitURL
|
||||
|
||||
async mounted() {
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
this.activeDid = settings.activeDid || ''
|
||||
} catch (err: unknown) {
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
logger.error('Error retrieving settings from database:', err)
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: err.message || "There was an error retrieving your settings.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: err.message || 'There was an error retrieving your settings.'
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,140 +149,140 @@ export default class PhotoDialog extends Vue {
|
||||
claimType: string,
|
||||
crop?: boolean,
|
||||
blob?: Blob,
|
||||
inputFileName?: string,
|
||||
inputFileName?: string
|
||||
) {
|
||||
this.visible = true;
|
||||
this.claimType = claimType;
|
||||
this.crop = !!crop;
|
||||
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
||||
this.visible = true
|
||||
this.claimType = claimType
|
||||
this.crop = !!crop
|
||||
const bottomNav = document.querySelector('#QuickNav') as HTMLElement
|
||||
if (bottomNav) {
|
||||
bottomNav.style.display = "none";
|
||||
bottomNav.style.display = 'none'
|
||||
}
|
||||
this.setImageCallback = setImageFn;
|
||||
this.setImageCallback = setImageFn
|
||||
if (blob) {
|
||||
this.blob = blob;
|
||||
this.fileName = inputFileName;
|
||||
this.showRetry = false;
|
||||
this.blob = blob
|
||||
this.fileName = inputFileName
|
||||
this.showRetry = false
|
||||
} else {
|
||||
this.blob = undefined;
|
||||
this.fileName = undefined;
|
||||
this.showRetry = true;
|
||||
this.blob = undefined
|
||||
this.fileName = undefined
|
||||
this.showRetry = true
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.visible = false;
|
||||
const bottomNav = document.querySelector("#QuickNav") as HTMLElement;
|
||||
this.visible = false
|
||||
const bottomNav = document.querySelector('#QuickNav') as HTMLElement
|
||||
if (bottomNav) {
|
||||
bottomNav.style.display = "";
|
||||
bottomNav.style.display = ''
|
||||
}
|
||||
this.blob = undefined;
|
||||
this.blob = undefined
|
||||
}
|
||||
|
||||
async takePhoto() {
|
||||
try {
|
||||
const result = await this.platformService.takePicture();
|
||||
this.blob = result.blob;
|
||||
this.fileName = result.fileName;
|
||||
const result = await this.platformService.takePicture()
|
||||
this.blob = result.blob
|
||||
this.fileName = result.fileName
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error taking picture:", error);
|
||||
logger.error('Error taking picture:', error)
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to take picture. Please try again.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: 'Failed to take picture. Please try again.'
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async pickPhoto() {
|
||||
try {
|
||||
const result = await this.platformService.pickImage();
|
||||
this.blob = result.blob;
|
||||
this.fileName = result.fileName;
|
||||
const result = await this.platformService.pickImage()
|
||||
this.blob = result.blob
|
||||
this.fileName = result.fileName
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error picking image:", error);
|
||||
logger.error('Error picking image:', error)
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to pick image. Please try again.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: 'Failed to pick image. Please try again.'
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private createBlobURL(blob: Blob): string {
|
||||
return URL.createObjectURL(blob);
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
async retryImage() {
|
||||
this.blob = undefined;
|
||||
this.blob = undefined
|
||||
}
|
||||
|
||||
async uploadImage() {
|
||||
this.uploading = true;
|
||||
this.uploading = true
|
||||
|
||||
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 = {
|
||||
Authorization: "Bearer " + token,
|
||||
};
|
||||
const formData = new FormData();
|
||||
Authorization: 'Bearer ' + token
|
||||
}
|
||||
const formData = new FormData()
|
||||
if (!this.blob) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error finding the picture. Please try again.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: 'There was an error finding the picture. Please try again.'
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.uploading = false;
|
||||
return;
|
||||
5000
|
||||
)
|
||||
this.uploading = false
|
||||
return
|
||||
}
|
||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||
formData.append("claimType", this.claimType);
|
||||
formData.append('image', this.blob, this.fileName || 'photo.jpg')
|
||||
formData.append('claimType', this.claimType)
|
||||
try {
|
||||
if (
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
window.location.hostname === 'localhost' &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes('localhost')
|
||||
) {
|
||||
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(
|
||||
DEFAULT_IMAGE_API_SERVER + "/image",
|
||||
DEFAULT_IMAGE_API_SERVER + '/image',
|
||||
formData,
|
||||
{ headers },
|
||||
);
|
||||
this.uploading = false;
|
||||
{ headers }
|
||||
)
|
||||
this.uploading = false
|
||||
|
||||
this.close();
|
||||
this.setImageCallback(response.data.url as string);
|
||||
this.close()
|
||||
this.setImageCallback(response.data.url as string)
|
||||
} catch (error: unknown) {
|
||||
// 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)) {
|
||||
const status = error.response?.status;
|
||||
const statusText = error.response?.statusText;
|
||||
const data = error.response?.data;
|
||||
const status = error.response?.status
|
||||
const statusText = error.response?.statusText
|
||||
const data = error.response?.data
|
||||
|
||||
// Log detailed error information
|
||||
logger.error("Upload error details:", {
|
||||
logger.error('Upload error details:', {
|
||||
status,
|
||||
statusText,
|
||||
data: JSON.stringify(data, null, 2),
|
||||
@@ -290,49 +290,49 @@ export default class PhotoDialog extends Vue {
|
||||
config: {
|
||||
url: error.config?.url,
|
||||
method: error.config?.method,
|
||||
headers: error.config?.headers,
|
||||
},
|
||||
});
|
||||
headers: error.config?.headers
|
||||
}
|
||||
})
|
||||
|
||||
if (status === 401) {
|
||||
errorMessage = "Authentication failed. Please try logging in again.";
|
||||
errorMessage = 'Authentication failed. Please try logging in again.'
|
||||
} 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) {
|
||||
errorMessage =
|
||||
"Unsupported image format. Please try a different image.";
|
||||
'Unsupported image format. Please try a different image.'
|
||||
} else if (status && status >= 500) {
|
||||
errorMessage = "Server error. Please try again later.";
|
||||
errorMessage = 'Server error. Please try again later.'
|
||||
} else if (data?.message) {
|
||||
errorMessage = data.message;
|
||||
errorMessage = data.message
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
// Log non-Axios error with full details
|
||||
logger.error("Non-Axios error details:", {
|
||||
logger.error('Non-Axios error details:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2),
|
||||
});
|
||||
error: JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
|
||||
})
|
||||
} else {
|
||||
// Log any other type of error
|
||||
logger.error("Unknown error type:", {
|
||||
logger.error('Unknown error type:', {
|
||||
error: JSON.stringify(error, null, 2),
|
||||
type: typeof error,
|
||||
});
|
||||
type: typeof error
|
||||
})
|
||||
}
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: errorMessage,
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: errorMessage
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.uploading = false;
|
||||
this.blob = undefined;
|
||||
5000
|
||||
)
|
||||
this.uploading = false
|
||||
this.blob = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,35 +15,35 @@
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { toSvg } from "jdenticon";
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { toSvg } from 'jdenticon'
|
||||
import { Vue, Component, Prop } from 'vue-facing-decorator'
|
||||
|
||||
const BLANK_CONFIG = {
|
||||
lightness: {
|
||||
color: [1.0, 1.0],
|
||||
grayscale: [1.0, 1.0],
|
||||
grayscale: [1.0, 1.0]
|
||||
},
|
||||
saturation: {
|
||||
color: 0.0,
|
||||
grayscale: 0.0,
|
||||
grayscale: 0.0
|
||||
},
|
||||
backColor: "#0000",
|
||||
};
|
||||
backColor: '#0000'
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class ProjectIcon extends Vue {
|
||||
@Prop entityId = "";
|
||||
@Prop iconSize = 0;
|
||||
@Prop imageUrl = "";
|
||||
@Prop linkToFull = false;
|
||||
@Prop entityId = ''
|
||||
@Prop iconSize = 0
|
||||
@Prop imageUrl = ''
|
||||
@Prop linkToFull = false
|
||||
|
||||
generateIdenticon() {
|
||||
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 {
|
||||
const config = this.entityId ? undefined : BLANK_CONFIG;
|
||||
const svgString = toSvg(this.entityId, this.iconSize, config);
|
||||
return svgString;
|
||||
const config = this.entityId ? undefined : BLANK_CONFIG
|
||||
const svgString = toSvg(this.entityId, this.iconSize, config)
|
||||
return svgString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
>
|
||||
<!-- eslint-enable -->
|
||||
<span class="w-full flex justify-between text-xs text-slate-500">
|
||||
<span></span>
|
||||
<span />
|
||||
<span>(100 characters max)</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -79,8 +79,8 @@
|
||||
<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"
|
||||
@click="
|
||||
close();
|
||||
turnOnNotifications();
|
||||
close()
|
||||
turnOnNotifications()
|
||||
"
|
||||
>
|
||||
Turn on Daily Message
|
||||
@@ -100,46 +100,46 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
logConsoleAndDb,
|
||||
retrieveSettingsForActiveAccount,
|
||||
secretDB,
|
||||
} from "../db/index";
|
||||
import { MASTER_SECRET_KEY } from "../db/tables/secret";
|
||||
import { urlBase64ToUint8Array } from "../libs/crypto/vc/util";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
secretDB
|
||||
} from '../db/index'
|
||||
import { MASTER_SECRET_KEY } from '../db/tables/secret'
|
||||
import { urlBase64ToUint8Array } from '../libs/crypto/vc/util'
|
||||
import * as libsUtil from '../libs/util'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
// Example interface for error
|
||||
interface ErrorResponse {
|
||||
message: string;
|
||||
message: string
|
||||
}
|
||||
|
||||
// PushSubscriptionJSON is defined in the Push API https://www.w3.org/TR/push-api/#dom-pushsubscriptionjson
|
||||
interface PushSubscriptionWithTime extends PushSubscriptionJSON {
|
||||
message?: string;
|
||||
notifyTime: { utcHour: number; minute: number };
|
||||
notifyType: string;
|
||||
message?: string
|
||||
notifyTime: { utcHour: number; minute: number }
|
||||
notifyType: string
|
||||
}
|
||||
|
||||
interface ServiceWorkerMessage {
|
||||
type: string;
|
||||
data: string;
|
||||
type: string
|
||||
data: string
|
||||
}
|
||||
|
||||
interface ServiceWorkerResponse {
|
||||
// Define the properties and their types
|
||||
success: boolean;
|
||||
message?: string;
|
||||
success: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
interface VapidResponse {
|
||||
data: {
|
||||
vapidKey: string;
|
||||
};
|
||||
vapidKey: string
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
@@ -147,135 +147,135 @@ export default class PushNotificationPermission extends Vue {
|
||||
// eslint-disable-next-line
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => Promise<() => void>;
|
||||
|
||||
DAILY_CHECK_TITLE = libsUtil.DAILY_CHECK_TITLE;
|
||||
DIRECT_PUSH_TITLE = libsUtil.DIRECT_PUSH_TITLE;
|
||||
DAILY_CHECK_TITLE = libsUtil.DAILY_CHECK_TITLE
|
||||
DIRECT_PUSH_TITLE = libsUtil.DIRECT_PUSH_TITLE
|
||||
|
||||
callback: (success: boolean, time: string, message?: string) => void =
|
||||
() => {};
|
||||
hourAm = true;
|
||||
hourInput = "8";
|
||||
isVisible = false;
|
||||
messageInput = "";
|
||||
minuteInput = "00";
|
||||
pushType = "";
|
||||
serviceWorkerReady = false;
|
||||
vapidKey = "";
|
||||
() => {}
|
||||
hourAm = true
|
||||
hourInput = '8'
|
||||
isVisible = false
|
||||
messageInput = ''
|
||||
minuteInput = '00'
|
||||
pushType = ''
|
||||
serviceWorkerReady = false
|
||||
vapidKey = ''
|
||||
|
||||
async open(
|
||||
pushType: string,
|
||||
callback?: (success: boolean, time: string, message?: string) => void,
|
||||
callback?: (success: boolean, time: string, message?: string) => void
|
||||
) {
|
||||
this.callback = callback || this.callback;
|
||||
this.isVisible = true;
|
||||
this.pushType = pushType;
|
||||
this.callback = callback || this.callback
|
||||
this.isVisible = true
|
||||
this.pushType = pushType
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let pushUrl = DEFAULT_PUSH_SERVER;
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
let pushUrl = DEFAULT_PUSH_SERVER
|
||||
if (settings?.webPushServer) {
|
||||
pushUrl = settings.webPushServer;
|
||||
pushUrl = settings.webPushServer
|
||||
}
|
||||
|
||||
if (pushUrl.startsWith("http://localhost")) {
|
||||
logConsoleAndDb("Not checking for VAPID in this local environment.");
|
||||
if (pushUrl.startsWith('http://localhost')) {
|
||||
logConsoleAndDb('Not checking for VAPID in this local environment.')
|
||||
} else {
|
||||
let responseData = "";
|
||||
let responseData = ''
|
||||
await this.axios
|
||||
.get(pushUrl + "/web-push/vapid")
|
||||
.get(pushUrl + '/web-push/vapid')
|
||||
.then((response: VapidResponse) => {
|
||||
this.vapidKey = response.data?.vapidKey || "";
|
||||
logConsoleAndDb("Got vapid key: " + this.vapidKey);
|
||||
responseData = JSON.stringify(response.data);
|
||||
this.vapidKey = response.data?.vapidKey || ''
|
||||
logConsoleAndDb('Got vapid key: ' + this.vapidKey)
|
||||
responseData = JSON.stringify(response.data)
|
||||
navigator.serviceWorker?.addEventListener(
|
||||
"controllerchange",
|
||||
'controllerchange',
|
||||
() => {
|
||||
logConsoleAndDb(
|
||||
"New service worker is now controlling the page",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
'New service worker is now controlling the page'
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
if (!this.vapidKey) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Setting Notifications",
|
||||
text: "Could not set notifications.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error Setting Notifications',
|
||||
text: 'Could not set notifications.'
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
logConsoleAndDb(
|
||||
"Error Setting Notifications: web push server response didn't have vapidKey: " +
|
||||
responseData,
|
||||
true,
|
||||
);
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.location.host.startsWith("localhost")) {
|
||||
if (window.location.host.startsWith('localhost')) {
|
||||
logConsoleAndDb(
|
||||
"Ignoring the error getting VAPID for local development.",
|
||||
);
|
||||
'Ignoring the error getting VAPID for local development.'
|
||||
)
|
||||
} else {
|
||||
logConsoleAndDb(
|
||||
"Got an error initializing notifications: " + JSON.stringify(error),
|
||||
true,
|
||||
);
|
||||
'Got an error initializing notifications: ' + JSON.stringify(error),
|
||||
true
|
||||
)
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Setting Notifications",
|
||||
text: "Got an error setting notifications.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error Setting Notifications',
|
||||
text: 'Got an error setting notifications.'
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
}
|
||||
}
|
||||
// there may be a long pause here on first initialization
|
||||
navigator.serviceWorker?.ready.then(() => {
|
||||
this.serviceWorkerReady = true;
|
||||
});
|
||||
this.serviceWorkerReady = true
|
||||
})
|
||||
|
||||
if (this.pushType === this.DIRECT_PUSH_TITLE) {
|
||||
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
|
||||
setTimeout(function () {
|
||||
document.getElementById("push-message")?.focus();
|
||||
}, 100);
|
||||
document.getElementById('push-message')?.focus()
|
||||
}, 100)
|
||||
} else {
|
||||
// not critical but doesn't make sense in a daily check
|
||||
this.messageInput = "";
|
||||
this.messageInput = ''
|
||||
}
|
||||
}
|
||||
|
||||
private close() {
|
||||
this.isVisible = false;
|
||||
this.isVisible = false
|
||||
}
|
||||
|
||||
private sendMessageToServiceWorker(
|
||||
message: ServiceWorkerMessage,
|
||||
message: ServiceWorkerMessage
|
||||
): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (navigator.serviceWorker?.controller) {
|
||||
const messageChannel = new MessageChannel();
|
||||
const messageChannel = new MessageChannel()
|
||||
|
||||
messageChannel.port1.onmessage = (event: MessageEvent) => {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error as ErrorResponse);
|
||||
reject(event.data.error as ErrorResponse)
|
||||
} else {
|
||||
resolve(event.data as ServiceWorkerResponse);
|
||||
resolve(event.data as ServiceWorkerResponse)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
navigator.serviceWorker?.controller.postMessage(message, [
|
||||
messageChannel.port2,
|
||||
]);
|
||||
messageChannel.port2
|
||||
])
|
||||
} else {
|
||||
reject("Service worker controller not available");
|
||||
reject('Service worker controller not available')
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
private async askPermission(): Promise<NotificationPermission> {
|
||||
@@ -283,136 +283,136 @@ export default class PushNotificationPermission extends Vue {
|
||||
// "Requesting permission for notifications: " + JSON.stringify(navigator),
|
||||
// );
|
||||
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();
|
||||
const secret = (await secretDB.secret.get(MASTER_SECRET_KEY))?.secret;
|
||||
await secretDB.open()
|
||||
const secret = (await secretDB.secret.get(MASTER_SECRET_KEY))?.secret
|
||||
if (!secret) {
|
||||
return Promise.reject("No secret found.");
|
||||
return Promise.reject('No secret found.')
|
||||
}
|
||||
|
||||
return this.sendSecretToServiceWorker(secret)
|
||||
.then(() => this.checkNotificationSupport())
|
||||
.then(() => this.requestNotificationPermission())
|
||||
.catch((error) => Promise.reject(error));
|
||||
.catch((error) => Promise.reject(error))
|
||||
}
|
||||
|
||||
private sendSecretToServiceWorker(secret: string): Promise<void> {
|
||||
const message: ServiceWorkerMessage = {
|
||||
type: "SEND_LOCAL_DATA",
|
||||
data: secret,
|
||||
};
|
||||
type: 'SEND_LOCAL_DATA',
|
||||
data: secret
|
||||
}
|
||||
|
||||
return this.sendMessageToServiceWorker(message).then((response) => {
|
||||
logConsoleAndDb(
|
||||
"Response from service worker: " + JSON.stringify(response),
|
||||
);
|
||||
});
|
||||
'Response from service worker: ' + JSON.stringify(response)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private checkNotificationSupport(): Promise<void> {
|
||||
if (!("Notification" in window)) {
|
||||
if (!('Notification' in window)) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Browser Notifications Are Not Supported",
|
||||
text: "This browser does not support notifications.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Browser Notifications Are Not Supported',
|
||||
text: 'This browser does not support notifications.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
return Promise.reject("This browser does not support notifications.");
|
||||
3000
|
||||
)
|
||||
return Promise.reject('This browser does not support notifications.')
|
||||
}
|
||||
if (window.Notification.permission === "granted") {
|
||||
return Promise.resolve();
|
||||
if (window.Notification.permission === 'granted') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.resolve();
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
private requestNotificationPermission(): Promise<NotificationPermission> {
|
||||
return window.Notification.requestPermission().then(
|
||||
(permission: string) => {
|
||||
if (permission !== "granted") {
|
||||
if (permission !== 'granted') {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Requesting Notification Permission",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error Requesting Notification Permission',
|
||||
text:
|
||||
"Allow this app permission to make notifications for personal reminders." +
|
||||
" You can adjust them at any time in your settings.",
|
||||
'Allow this app permission to make notifications for personal reminders.' +
|
||||
' You can adjust them at any time in your settings.'
|
||||
},
|
||||
-1,
|
||||
);
|
||||
throw new Error("Permission was not granted to this app.");
|
||||
-1
|
||||
)
|
||||
throw new Error('Permission was not granted to this app.')
|
||||
}
|
||||
return permission;
|
||||
},
|
||||
);
|
||||
return permission
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private checkHourInput() {
|
||||
const hourNum = parseInt(this.hourInput);
|
||||
const hourNum = parseInt(this.hourInput)
|
||||
if (isNaN(hourNum)) {
|
||||
this.hourInput = "12";
|
||||
this.hourInput = '12'
|
||||
} else if (hourNum < 1) {
|
||||
this.hourInput = "12";
|
||||
this.hourAm = !this.hourAm;
|
||||
this.hourInput = '12'
|
||||
this.hourAm = !this.hourAm
|
||||
} else if (hourNum > 12) {
|
||||
this.hourInput = "1";
|
||||
this.hourAm = !this.hourAm;
|
||||
this.hourInput = '1'
|
||||
this.hourAm = !this.hourAm
|
||||
} else {
|
||||
this.hourInput = hourNum.toString();
|
||||
this.hourInput = hourNum.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private checkMinuteInput() {
|
||||
const minuteNum = parseInt(this.minuteInput);
|
||||
const minuteNum = parseInt(this.minuteInput)
|
||||
if (isNaN(minuteNum)) {
|
||||
this.minuteInput = "00";
|
||||
this.minuteInput = '00'
|
||||
} else if (minuteNum < 0) {
|
||||
this.minuteInput = "59";
|
||||
this.minuteInput = '59'
|
||||
} else if (minuteNum < 10) {
|
||||
this.minuteInput = "0" + minuteNum;
|
||||
this.minuteInput = '0' + minuteNum
|
||||
} else if (minuteNum > 59) {
|
||||
this.minuteInput = "00";
|
||||
this.minuteInput = '00'
|
||||
} else {
|
||||
this.minuteInput = minuteNum.toString();
|
||||
this.minuteInput = minuteNum.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private async turnOnNotifications() {
|
||||
let notifyCloser = () => {};
|
||||
let notifyCloser = () => {}
|
||||
return this.askPermission()
|
||||
.then((permission) => {
|
||||
logConsoleAndDb("Permission granted: " + JSON.stringify(permission));
|
||||
logConsoleAndDb('Permission granted: ' + JSON.stringify(permission))
|
||||
|
||||
// Call the function and handle promises
|
||||
return this.subscribeToPush();
|
||||
return this.subscribeToPush()
|
||||
})
|
||||
.then(() => {
|
||||
logConsoleAndDb("Subscribed successfully.");
|
||||
return navigator.serviceWorker?.ready;
|
||||
logConsoleAndDb('Subscribed successfully.')
|
||||
return navigator.serviceWorker?.ready
|
||||
})
|
||||
.then((registration) => {
|
||||
return registration.pushManager.getSubscription();
|
||||
return registration.pushManager.getSubscription()
|
||||
})
|
||||
.then(async (subscription) => {
|
||||
if (subscription) {
|
||||
notifyCloser = await this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
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.",
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
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."
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
// 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
|
||||
? // If it's AM, then we'll change it to 0 for 12 AM but otherwise use rawHourNum
|
||||
rawHourNum === 12
|
||||
@@ -421,153 +421,153 @@ export default class PushNotificationPermission extends Vue {
|
||||
: // Otherwise it's PM, so keep a 12 but otherwise add 12
|
||||
rawHourNum === 12
|
||||
? 12
|
||||
: rawHourNum + 12;
|
||||
const hourNum = adjHourNum % 24; // probably unnecessary now
|
||||
: rawHourNum + 12
|
||||
const hourNum = adjHourNum % 24 // probably unnecessary now
|
||||
const utcHour =
|
||||
hourNum + Math.round(new Date().getTimezoneOffset() / 60);
|
||||
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24;
|
||||
const minuteNum = libsUtil.numberOrZero(this.minuteInput);
|
||||
hourNum + Math.round(new Date().getTimezoneOffset() / 60)
|
||||
const finalUtcHour = (utcHour + (utcHour < 0 ? 24 : 0)) % 24
|
||||
const minuteNum = libsUtil.numberOrZero(this.minuteInput)
|
||||
const utcMinute =
|
||||
minuteNum + Math.round(new Date().getTimezoneOffset() % 60);
|
||||
const finalUtcMinute = (utcMinute + (utcMinute < 0 ? 60 : 0)) % 60;
|
||||
minuteNum + Math.round(new Date().getTimezoneOffset() % 60)
|
||||
const finalUtcMinute = (utcMinute + (utcMinute < 0 ? 60 : 0)) % 60
|
||||
|
||||
const subscriptionWithTime: PushSubscriptionWithTime = {
|
||||
notifyTime: { utcHour: finalUtcHour, minute: finalUtcMinute },
|
||||
notifyType: this.pushType,
|
||||
message: this.messageInput,
|
||||
...subscription.toJSON(),
|
||||
};
|
||||
await this.sendSubscriptionToServer(subscriptionWithTime);
|
||||
...subscription.toJSON()
|
||||
}
|
||||
await this.sendSubscriptionToServer(subscriptionWithTime)
|
||||
// To help investigate potential issues with this: https://firebase.google.com/docs/cloud-messaging/migrate-v1
|
||||
logConsoleAndDb(
|
||||
"Subscription data sent to server with endpoint: " +
|
||||
subscription.endpoint,
|
||||
);
|
||||
return subscriptionWithTime;
|
||||
'Subscription data sent to server with endpoint: ' +
|
||||
subscription.endpoint
|
||||
)
|
||||
return subscriptionWithTime
|
||||
} else {
|
||||
throw new Error("Subscription object is not available.");
|
||||
throw new Error('Subscription object is not available.')
|
||||
}
|
||||
})
|
||||
.then(async (subscription: PushSubscriptionWithTime) => {
|
||||
logConsoleAndDb(
|
||||
"Subscription data sent to server and all finished successfully.",
|
||||
);
|
||||
await libsUtil.sendTestThroughPushServer(subscription, true);
|
||||
notifyCloser();
|
||||
'Subscription data sent to server and all finished successfully.'
|
||||
)
|
||||
await libsUtil.sendTestThroughPushServer(subscription, true)
|
||||
notifyCloser()
|
||||
setTimeout(() => {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Notification Is On",
|
||||
text: "You should see at least one on your device; if not, check the 'Troubleshoot' link.",
|
||||
group: 'alert',
|
||||
type: 'success',
|
||||
title: 'Notification Is On',
|
||||
text: "You should see at least one on your device; if not, check the 'Troubleshoot' link."
|
||||
},
|
||||
7000,
|
||||
);
|
||||
}, 500);
|
||||
7000
|
||||
)
|
||||
}, 500)
|
||||
const timeText =
|
||||
// eslint-disable-next-line
|
||||
this.hourInput + ":" + this.minuteInput + " " + (this.hourAm ? "AM" : "PM");
|
||||
this.callback(true, timeText, this.messageInput);
|
||||
this.callback(true, timeText, this.messageInput)
|
||||
})
|
||||
.catch((error) => {
|
||||
logConsoleAndDb(
|
||||
"Got an error setting notification permissions: " +
|
||||
" string " +
|
||||
'Got an error setting notification permissions: ' +
|
||||
' string ' +
|
||||
error.toString() +
|
||||
" JSON " +
|
||||
' JSON ' +
|
||||
JSON.stringify(error),
|
||||
true,
|
||||
);
|
||||
true
|
||||
)
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Setting Notification Permissions",
|
||||
text: "Could not set notification permissions.",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error Setting 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
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
private subscribeToPush(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!("serviceWorker" in navigator && "PushManager" in window)) {
|
||||
const errorMsg = "Push messaging is not supported";
|
||||
logger.warn(errorMsg);
|
||||
return reject(new Error(errorMsg));
|
||||
if (!('serviceWorker' in navigator && 'PushManager' in window)) {
|
||||
const errorMsg = 'Push messaging is not supported'
|
||||
logger.warn(errorMsg)
|
||||
return reject(new Error(errorMsg))
|
||||
}
|
||||
|
||||
if (window.Notification.permission !== "granted") {
|
||||
const errorMsg = "Notification permission not granted";
|
||||
logger.warn(errorMsg);
|
||||
return reject(new Error(errorMsg));
|
||||
if (window.Notification.permission !== 'granted') {
|
||||
const errorMsg = 'Notification permission not granted'
|
||||
logger.warn(errorMsg)
|
||||
return reject(new Error(errorMsg))
|
||||
}
|
||||
|
||||
const applicationServerKey = urlBase64ToUint8Array(this.vapidKey);
|
||||
const applicationServerKey = urlBase64ToUint8Array(this.vapidKey)
|
||||
const options: PushSubscriptionOptions = {
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: applicationServerKey,
|
||||
};
|
||||
applicationServerKey: applicationServerKey
|
||||
}
|
||||
|
||||
navigator.serviceWorker?.ready
|
||||
.then((registration) => {
|
||||
return registration.pushManager.subscribe(options);
|
||||
return registration.pushManager.subscribe(options)
|
||||
})
|
||||
.then((subscription) => {
|
||||
logConsoleAndDb(
|
||||
"Push subscription successful: " + JSON.stringify(subscription),
|
||||
);
|
||||
resolve();
|
||||
'Push subscription successful: ' + JSON.stringify(subscription)
|
||||
)
|
||||
resolve()
|
||||
})
|
||||
.catch((error) => {
|
||||
logConsoleAndDb(
|
||||
"Push subscription failed: " +
|
||||
'Push subscription failed: ' +
|
||||
JSON.stringify(error) +
|
||||
" - " +
|
||||
' - ' +
|
||||
JSON.stringify(options),
|
||||
true,
|
||||
);
|
||||
true
|
||||
)
|
||||
|
||||
// Inform the user about the issue
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Setting Push Notifications",
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error Setting Push Notifications',
|
||||
text:
|
||||
"We encountered an issue setting up push notifications. " +
|
||||
"If you wish to revoke notification permissions, please do so in your browser settings.",
|
||||
'We encountered an issue setting up push notifications. ' +
|
||||
'If you wish to revoke notification permissions, please do so in your browser settings.'
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private sendSubscriptionToServer(
|
||||
subscription: PushSubscriptionWithTime,
|
||||
subscription: PushSubscriptionWithTime
|
||||
): Promise<void> {
|
||||
logConsoleAndDb(
|
||||
"About to send subscription... " + JSON.stringify(subscription),
|
||||
);
|
||||
return fetch("/web-push/subscribe", {
|
||||
method: "POST",
|
||||
'About to send subscription... ' + JSON.stringify(subscription)
|
||||
)
|
||||
return fetch('/web-push/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(subscription),
|
||||
body: JSON.stringify(subscription)
|
||||
}).then((response) => {
|
||||
if (!response.ok) {
|
||||
logger.error("Bad response subscribing to web push: ", response);
|
||||
throw new Error("Failed to send push subscription to server");
|
||||
logger.error('Bad response subscribing to web push: ', response)
|
||||
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>
|
||||
|
||||
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>
|
||||
<div v-else class="text-center">
|
||||
<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>
|
||||
|
||||
@@ -55,216 +57,216 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { QrcodeStream } from "vue-qrcode-reader";
|
||||
import { reactive } from "vue";
|
||||
import { Component, Vue } from 'vue-facing-decorator'
|
||||
import { QrcodeStream } from 'vue-qrcode-reader'
|
||||
import { reactive } from 'vue'
|
||||
import {
|
||||
BarcodeScanner,
|
||||
type ScanResult,
|
||||
} from "@capacitor-mlkit/barcode-scanning";
|
||||
import type { PluginListenerHandle } from "@capacitor/core";
|
||||
import { logger } from "../utils/logger";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
type ScanResult
|
||||
} from '@capacitor-mlkit/barcode-scanning'
|
||||
import type { PluginListenerHandle } from '@capacitor/core'
|
||||
import { logger } from '../../utils/logger'
|
||||
import { NotificationIface } from '../../constants/app'
|
||||
|
||||
// Declare global constants
|
||||
declare const __USE_QR_READER__: boolean;
|
||||
declare const __IS_MOBILE__: boolean;
|
||||
declare const __USE_QR_READER__: boolean
|
||||
declare const __IS_MOBILE__: boolean
|
||||
|
||||
interface AppState {
|
||||
isProcessing: boolean;
|
||||
processingStatus: string;
|
||||
processingDetails: string;
|
||||
error: string;
|
||||
isProcessing: boolean
|
||||
processingStatus: string
|
||||
processingDetails: string
|
||||
error: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
QrcodeStream,
|
||||
},
|
||||
QrcodeStream
|
||||
}
|
||||
})
|
||||
export default class QRScannerDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
|
||||
visible = false;
|
||||
private scanListener: PluginListenerHandle | null = null;
|
||||
private onScanCallback: ((result: string) => void) | null = null;
|
||||
visible = false
|
||||
private scanListener: PluginListenerHandle | null = null
|
||||
private onScanCallback: ((result: string) => void) | null = null
|
||||
|
||||
state = reactive<AppState>({
|
||||
isProcessing: false,
|
||||
processingStatus: "",
|
||||
processingDetails: "",
|
||||
error: "",
|
||||
});
|
||||
processingStatus: '',
|
||||
processingDetails: '',
|
||||
error: ''
|
||||
})
|
||||
|
||||
async open(onScan: (result: string) => void) {
|
||||
this.onScanCallback = onScan;
|
||||
this.visible = true;
|
||||
this.state.error = "";
|
||||
this.onScanCallback = onScan
|
||||
this.visible = true
|
||||
this.state.error = ''
|
||||
|
||||
if (!this.useQRReader) {
|
||||
// Check if barcode scanning is supported on mobile
|
||||
try {
|
||||
const { supported } = await BarcodeScanner.isSupported();
|
||||
const { supported } = await BarcodeScanner.isSupported()
|
||||
if (!supported) {
|
||||
this.showError("Barcode scanning is not supported on this device");
|
||||
return;
|
||||
this.showError('Barcode scanning is not supported on this device')
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError("Failed to check barcode scanner support");
|
||||
return;
|
||||
this.showError('Failed to check barcode scanner support')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
this.stopScanning().catch((error) => {
|
||||
logger.error("Error stopping scanner during close:", error);
|
||||
});
|
||||
this.onScanCallback = null;
|
||||
logger.error('Error stopping scanner during close:', error)
|
||||
})
|
||||
this.onScanCallback = null
|
||||
}
|
||||
|
||||
async openMobileCamera() {
|
||||
try {
|
||||
this.state.isProcessing = true;
|
||||
this.state.processingStatus = "Starting camera...";
|
||||
logger.log("Opening mobile camera - starting initialization");
|
||||
this.state.isProcessing = true
|
||||
this.state.processingStatus = 'Starting camera...'
|
||||
logger.log('Opening mobile camera - starting initialization')
|
||||
|
||||
// Check current permission status
|
||||
const status = await BarcodeScanner.checkPermissions();
|
||||
logger.log("Camera permission status:", JSON.stringify(status, null, 2));
|
||||
const status = await BarcodeScanner.checkPermissions()
|
||||
logger.log('Camera permission status:', JSON.stringify(status, null, 2))
|
||||
|
||||
if (status.camera !== "granted") {
|
||||
if (status.camera !== 'granted') {
|
||||
// Request permission if not granted
|
||||
logger.log("Requesting camera permissions...");
|
||||
const permissionStatus = await BarcodeScanner.requestPermissions();
|
||||
if (permissionStatus.camera !== "granted") {
|
||||
throw new Error("Camera permission not granted");
|
||||
logger.log('Requesting camera permissions...')
|
||||
const permissionStatus = await BarcodeScanner.requestPermissions()
|
||||
if (permissionStatus.camera !== 'granted') {
|
||||
throw new Error('Camera permission not granted')
|
||||
}
|
||||
logger.log(
|
||||
"Camera permission granted:",
|
||||
JSON.stringify(permissionStatus, null, 2),
|
||||
);
|
||||
'Camera permission granted:',
|
||||
JSON.stringify(permissionStatus, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
// Remove any existing listener first
|
||||
await this.cleanupScanListener();
|
||||
await this.cleanupScanListener()
|
||||
|
||||
// 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(
|
||||
"barcodesScanned",
|
||||
'barcodesScanned',
|
||||
async (result: ScanResult) => {
|
||||
logger.log(
|
||||
"Barcode scan result received:",
|
||||
JSON.stringify(result, null, 2),
|
||||
);
|
||||
'Barcode scan result received:',
|
||||
JSON.stringify(result, null, 2)
|
||||
)
|
||||
if (result.barcodes && result.barcodes.length > 0) {
|
||||
this.state.processingDetails = `Processing QR code: ${result.barcodes[0].rawValue}`;
|
||||
await this.handleScanResult(result.barcodes[0].rawValue);
|
||||
this.state.processingDetails = `Processing QR code: ${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
|
||||
logger.log("Starting barcode scanner");
|
||||
await BarcodeScanner.startScan();
|
||||
logger.log("Barcode scanner started successfully");
|
||||
logger.log('Starting barcode scanner')
|
||||
await BarcodeScanner.startScan()
|
||||
logger.log('Barcode scanner started successfully')
|
||||
|
||||
this.state.isProcessing = false;
|
||||
this.state.processingStatus = "";
|
||||
this.state.isProcessing = false
|
||||
this.state.processingStatus = ''
|
||||
} catch (error) {
|
||||
logger.error("Failed to open camera:", error);
|
||||
this.state.isProcessing = false;
|
||||
this.state.processingStatus = "";
|
||||
logger.error('Failed to open camera:', error)
|
||||
this.state.isProcessing = false
|
||||
this.state.processingStatus = ''
|
||||
this.showError(
|
||||
error instanceof Error ? error.message : "Failed to open camera",
|
||||
);
|
||||
error instanceof Error ? error.message : 'Failed to open camera'
|
||||
)
|
||||
|
||||
// Cleanup on error
|
||||
await this.cleanupScanListener();
|
||||
await this.cleanupScanListener()
|
||||
}
|
||||
}
|
||||
|
||||
private async handleScanResult(rawValue: string) {
|
||||
try {
|
||||
this.state.isProcessing = true;
|
||||
this.state.processingStatus = "Processing QR code...";
|
||||
this.state.processingDetails = `Scanned value: ${rawValue}`;
|
||||
this.state.isProcessing = true
|
||||
this.state.processingStatus = 'Processing QR code...'
|
||||
this.state.processingDetails = `Scanned value: ${rawValue}`
|
||||
|
||||
// Stop scanning before processing
|
||||
await this.stopScanning();
|
||||
await this.stopScanning()
|
||||
|
||||
if (this.onScanCallback) {
|
||||
await this.onScanCallback(rawValue);
|
||||
await this.onScanCallback(rawValue)
|
||||
// Only close after the callback is complete
|
||||
this.close();
|
||||
this.close()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error handling scan result:", error);
|
||||
this.showError("Failed to process scan result");
|
||||
logger.error('Error handling scan result:', error)
|
||||
this.showError('Failed to process scan result')
|
||||
} finally {
|
||||
this.state.isProcessing = false;
|
||||
this.state.processingStatus = "";
|
||||
this.state.processingDetails = "";
|
||||
this.state.isProcessing = false
|
||||
this.state.processingStatus = ''
|
||||
this.state.processingDetails = ''
|
||||
}
|
||||
}
|
||||
|
||||
private async cleanupScanListener() {
|
||||
try {
|
||||
if (this.scanListener) {
|
||||
await this.scanListener.remove();
|
||||
this.scanListener = null;
|
||||
await this.scanListener.remove()
|
||||
this.scanListener = null
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error removing scan listener:", error);
|
||||
logger.error('Error removing scan listener:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async stopScanning() {
|
||||
try {
|
||||
await this.cleanupScanListener();
|
||||
await this.cleanupScanListener()
|
||||
|
||||
if (!this.useQRReader) {
|
||||
// Stop the native scanner
|
||||
await BarcodeScanner.stopScan();
|
||||
await BarcodeScanner.stopScan()
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error stopping scanner:", error);
|
||||
throw error;
|
||||
logger.error('Error stopping scanner:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Web QR reader handlers
|
||||
async onScanDetect(result: { rawValue: string }) {
|
||||
await this.handleScanResult(result.rawValue);
|
||||
await this.handleScanResult(result.rawValue)
|
||||
}
|
||||
|
||||
onScanError(error: Error) {
|
||||
logger.error("Scan error:", error);
|
||||
this.showError("Failed to scan QR code");
|
||||
logger.error('Scan error:', error)
|
||||
this.showError('Failed to scan QR code')
|
||||
}
|
||||
|
||||
private showError(message: string) {
|
||||
this.state.error = message;
|
||||
this.state.error = message
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: message,
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error',
|
||||
text: message
|
||||
},
|
||||
5000,
|
||||
);
|
||||
5000
|
||||
)
|
||||
}
|
||||
|
||||
get useQRReader(): boolean {
|
||||
return __USE_QR_READER__;
|
||||
return __USE_QR_READER__
|
||||
}
|
||||
|
||||
get isMobile(): boolean {
|
||||
return __IS_MOBILE__;
|
||||
return __IS_MOBILE__
|
||||
}
|
||||
}
|
||||
</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,
|
||||
'rounded-md': true,
|
||||
'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">
|
||||
@@ -24,7 +24,7 @@
|
||||
'basis-1/5': true,
|
||||
'rounded-md': true,
|
||||
'bg-slate-400 text-white': selected === 'Discover',
|
||||
'text-slate-500': selected !== 'Discover',
|
||||
'text-slate-500': selected !== 'Discover'
|
||||
}"
|
||||
>
|
||||
<router-link
|
||||
@@ -43,7 +43,7 @@
|
||||
'basis-1/5': true,
|
||||
'rounded-md': true,
|
||||
'bg-slate-400 text-white': selected === 'Projects',
|
||||
'text-slate-500': selected !== 'Projects',
|
||||
'text-slate-500': selected !== 'Projects'
|
||||
}"
|
||||
>
|
||||
<router-link
|
||||
@@ -62,7 +62,7 @@
|
||||
'basis-1/5': true,
|
||||
'rounded-md': true,
|
||||
'bg-slate-400 text-white': selected === 'Contacts',
|
||||
'text-slate-500': selected !== 'Contacts',
|
||||
'text-slate-500': selected !== 'Contacts'
|
||||
}"
|
||||
>
|
||||
<router-link
|
||||
@@ -81,7 +81,7 @@
|
||||
'basis-1/5': true,
|
||||
'rounded-md': true,
|
||||
'bg-slate-400 text-white': selected === 'Profile',
|
||||
'text-slate-500': selected !== 'Profile',
|
||||
'text-slate-500': selected !== 'Profile'
|
||||
}"
|
||||
>
|
||||
<router-link
|
||||
@@ -106,10 +106,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { Component, Vue, Prop } from 'vue-facing-decorator'
|
||||
|
||||
@Component
|
||||
export default class QuickNav extends Vue {
|
||||
@Prop selected = "";
|
||||
@Prop selected = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -13,46 +13,46 @@
|
||||
</template>
|
||||
|
||||
<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 { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { AppString, NotificationIface } from '../constants/app'
|
||||
import { retrieveSettingsForActiveAccount } from '../db/index'
|
||||
|
||||
@Component
|
||||
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() {
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
if (
|
||||
settings.warnIfTestServer &&
|
||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||
) {
|
||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||
this.message = "You're linked to a non-prod server, user " + didPrefix;
|
||||
const didPrefix = settings.activeDid?.slice(11, 15)
|
||||
this.message = "You're linked to a non-prod server, user " + didPrefix
|
||||
} else if (
|
||||
settings.warnIfProdServer &&
|
||||
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 the production server, user " + didPrefix;
|
||||
"You're linked to the production server, user " + didPrefix
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error Detecting Server",
|
||||
text: JSON.stringify(err),
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
title: 'Error Detecting Server',
|
||||
text: JSON.stringify(err)
|
||||
},
|
||||
-1,
|
||||
);
|
||||
-1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,49 +35,49 @@
|
||||
</template>
|
||||
|
||||
<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 { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { NotificationIface } from '../constants/app'
|
||||
import { db, retrieveSettingsForActiveAccount } from '../db/index'
|
||||
import { MASTER_SETTINGS_KEY } from '../db/tables/settings'
|
||||
|
||||
@Component
|
||||
export default class UserNameDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void
|
||||
|
||||
@Prop({
|
||||
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;
|
||||
@Prop({ default: false }) callbackOnCancel!: boolean;
|
||||
sharingExplanation!: string
|
||||
@Prop({ default: false }) callbackOnCancel!: boolean
|
||||
|
||||
callback: (name?: string) => void = () => {};
|
||||
givenName = "";
|
||||
visible = false;
|
||||
callback: (name?: string) => void = () => {}
|
||||
givenName = ''
|
||||
visible = false
|
||||
|
||||
/**
|
||||
* @param aCallback - callback function for name, which may be ""
|
||||
*/
|
||||
async open(aCallback?: (name?: string) => void) {
|
||||
this.callback = aCallback || this.callback;
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.givenName = settings.firstName || "";
|
||||
this.visible = true;
|
||||
this.callback = aCallback || this.callback
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
this.givenName = settings.firstName || ''
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
async onClickSaveChanges() {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
firstName: this.givenName,
|
||||
});
|
||||
this.visible = false;
|
||||
this.callback(this.givenName);
|
||||
firstName: this.givenName
|
||||
})
|
||||
this.visible = false
|
||||
this.callback(this.givenName)
|
||||
}
|
||||
|
||||
onClickCancel() {
|
||||
this.visible = false;
|
||||
this.visible = false
|
||||
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
|
||||
|
||||
import * as TWEEN from "@tweenjs/tween.js";
|
||||
import * as THREE from "three";
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { createCamera } from "./components/camera.js";
|
||||
import { createLights } from "./components/lights.js";
|
||||
import { createScene } from "./components/scene.js";
|
||||
import { loadLandmarks } from "./components/objects/landmarks.js";
|
||||
import { createTerrain } from "./components/objects/terrain.js";
|
||||
import { Loop } from "./systems/Loop.js";
|
||||
import { Resizer } from "./systems/Resizer.js";
|
||||
import { createControls } from "./systems/controls.js";
|
||||
import { createRenderer } from "./systems/renderer.js";
|
||||
import { createCamera } from './components/camera.js'
|
||||
import { createLights } from './components/lights.js'
|
||||
import { createScene } from './components/scene.js'
|
||||
import { loadLandmarks } from './components/objects/landmarks.js'
|
||||
import { createTerrain } from './components/objects/terrain.js'
|
||||
import { Loop } from './systems/Loop.js'
|
||||
import { Resizer } from './systems/Resizer.js'
|
||||
import { createControls } from './systems/controls.js'
|
||||
import { createRenderer } from './systems/renderer.js'
|
||||
|
||||
const COLOR1 = "#dddddd";
|
||||
const COLOR2 = "#0055aa";
|
||||
const COLOR1 = '#dddddd'
|
||||
const COLOR2 = '#0055aa'
|
||||
|
||||
class World {
|
||||
constructor(container, vue) {
|
||||
this.PLATFORM_BORDER = 5;
|
||||
this.PLATFORM_EDGE_FOR_UNKNOWNS = 10;
|
||||
this.PLATFORM_SIZE = 100; // note that the loadLandmarks calculations may still assume 100
|
||||
this.PLATFORM_BORDER = 5
|
||||
this.PLATFORM_EDGE_FOR_UNKNOWNS = 10
|
||||
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
|
||||
this.camera = createCamera();
|
||||
this.scene = createScene(COLOR2);
|
||||
this.renderer = createRenderer();
|
||||
this.camera = createCamera()
|
||||
this.scene = createScene(COLOR2)
|
||||
this.renderer = createRenderer()
|
||||
|
||||
// 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.lights = [];
|
||||
this.bushes = [];
|
||||
this.light = null
|
||||
this.lights = []
|
||||
this.bushes = []
|
||||
|
||||
// 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
|
||||
const controls = createControls(this.camera, this.renderer.domElement);
|
||||
const controls = createControls(this.camera, this.renderer.domElement)
|
||||
|
||||
// Light Instance, with optional light helper
|
||||
const { light } = createLights(COLOR1);
|
||||
const { light } = createLights(COLOR1)
|
||||
|
||||
// Terrain Instance
|
||||
const terrain = createTerrain({
|
||||
@@ -56,55 +56,55 @@ class World {
|
||||
width:
|
||||
this.PLATFORM_SIZE +
|
||||
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(light);
|
||||
this.loop.updatables.push(terrain);
|
||||
this.loop.updatables.push(controls)
|
||||
this.loop.updatables.push(light)
|
||||
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
|
||||
const resizer = new Resizer(container, this.camera, this.renderer);
|
||||
const resizer = new Resizer(container, this.camera, this.renderer)
|
||||
resizer.onResize = () => {
|
||||
this.render();
|
||||
};
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
|
||||
update(time) {
|
||||
TWEEN.update(time);
|
||||
TWEEN.update(time)
|
||||
this.lights.forEach((light) => {
|
||||
light.updateMatrixWorld();
|
||||
light.target.updateMatrixWorld();
|
||||
});
|
||||
light.updateMatrixWorld()
|
||||
light.target.updateMatrixWorld()
|
||||
})
|
||||
this.lights.forEach((bush) => {
|
||||
bush.updateMatrixWorld();
|
||||
});
|
||||
requestAnimationFrame(this.update);
|
||||
bush.updateMatrixWorld()
|
||||
})
|
||||
requestAnimationFrame(this.update)
|
||||
}
|
||||
|
||||
render() {
|
||||
// draw a single frame
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
}
|
||||
|
||||
// Animation handlers
|
||||
start() {
|
||||
this.loop.start();
|
||||
this.loop.start()
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.loop.stop();
|
||||
this.loop.stop()
|
||||
}
|
||||
|
||||
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() {
|
||||
const camera = new PerspectiveCamera(
|
||||
35, // fov = Field Of View
|
||||
1, // aspect ratio (dummy value)
|
||||
0.1, // near clipping plane
|
||||
350, // far clipping plane
|
||||
);
|
||||
350 // far clipping plane
|
||||
)
|
||||
|
||||
// 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
|
||||
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) {
|
||||
const light = new DirectionalLight(color, 4);
|
||||
const lightHelper = new DirectionalLightHelper(light, 0);
|
||||
light.position.set(60, 100, 30);
|
||||
const light = new DirectionalLight(color, 4)
|
||||
const lightHelper = new DirectionalLightHelper(light, 0)
|
||||
light.position.set(60, 100, 30)
|
||||
|
||||
// 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 * as THREE from "three";
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
|
||||
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
|
||||
import * as TWEEN from "@tweenjs/tween.js";
|
||||
import { retrieveSettingsForActiveAccount } from "../../../../db";
|
||||
import { getHeaders } from "../../../../libs/endorserServer";
|
||||
import { logger } from "../../../../utils/logger";
|
||||
import axios from 'axios'
|
||||
import * as THREE from 'three'
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader'
|
||||
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import { retrieveSettingsForActiveAccount } from '../../../../db'
|
||||
import { getHeaders } from '../../../../libs/endorserServer'
|
||||
import { logger } from '../../../../utils/logger'
|
||||
|
||||
const ANIMATION_DURATION_SECS = 10;
|
||||
const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/";
|
||||
const ANIMATION_DURATION_SECS = 10
|
||||
const ENDORSER_ENTITY_PREFIX = 'https://endorser.ch/entity/'
|
||||
|
||||
export async function loadLandmarks(vue, world, scene, loop) {
|
||||
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
|
||||
vue.setWorldProperty('animationDurationSeconds', ANIMATION_DURATION_SECS)
|
||||
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
const activeDid = settings.activeDid || "";
|
||||
const apiServer = settings.apiServer;
|
||||
const headers = await getHeaders(activeDid);
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
const activeDid = settings.activeDid || ''
|
||||
const apiServer = settings.apiServer
|
||||
const headers = await getHeaders(activeDid)
|
||||
|
||||
const url = apiServer + "/api/v2/report/claims?claimType=GiveAction";
|
||||
const resp = await axios.get(url, { headers: headers });
|
||||
const url = apiServer + '/api/v2/report/claims?claimType=GiveAction'
|
||||
const resp = await axios.get(url, { headers: headers })
|
||||
if (resp.status === 200) {
|
||||
const landmarks = resp.data.data;
|
||||
const landmarks = resp.data.data
|
||||
|
||||
const minDate = landmarks[landmarks.length - 1].issuedAt;
|
||||
const maxDate = landmarks[0].issuedAt;
|
||||
const minDate = landmarks[landmarks.length - 1].issuedAt
|
||||
const maxDate = landmarks[0].issuedAt
|
||||
|
||||
world.setExposedWorldProperties("startTime", minDate.replace("T", " "));
|
||||
world.setExposedWorldProperties("endTime", maxDate.replace("T", " "));
|
||||
world.setExposedWorldProperties('startTime', minDate.replace('T', ' '))
|
||||
world.setExposedWorldProperties('endTime', maxDate.replace('T', ' '))
|
||||
|
||||
const minTimeMillis = new Date(minDate).getTime();
|
||||
const minTimeMillis = new Date(minDate).getTime()
|
||||
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
|
||||
const fakeRealRatio = (ANIMATION_DURATION_SECS * 1000) / fullTimeMillis;
|
||||
const fakeRealRatio = (ANIMATION_DURATION_SECS * 1000) / fullTimeMillis
|
||||
|
||||
// load plant model first because it takes a second
|
||||
const loader = new GLTFLoader();
|
||||
const loader = new GLTFLoader()
|
||||
// choose the right plant
|
||||
const modelLoc = "/models/lupine_plant/scene.gltf", // push with pokies
|
||||
modScale = 0.1;
|
||||
const modelLoc = '/models/lupine_plant/scene.gltf', // push with pokies
|
||||
modScale = 0.1
|
||||
//const modelLoc = "/models/round_bush/scene.gltf", // green & pink
|
||||
// modScale = 1;
|
||||
//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
|
||||
const locations = landmarks.map((claim) =>
|
||||
locForGive(
|
||||
claim,
|
||||
world.PLATFORM_SIZE,
|
||||
world.PLATFORM_EDGE_FOR_UNKNOWNS,
|
||||
),
|
||||
);
|
||||
locForGive(claim, world.PLATFORM_SIZE, world.PLATFORM_EDGE_FOR_UNKNOWNS)
|
||||
)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
loader.load(
|
||||
modelLoc,
|
||||
function (gltf) {
|
||||
gltf.scene.scale.set(0, 0, 0);
|
||||
gltf.scene.scale.set(0, 0, 0)
|
||||
for (let i = 0; i < landmarks.length; i++) {
|
||||
// claim is a GiveServerRecord (see endorserServer.ts)
|
||||
const claim = landmarks[i];
|
||||
const newPlant = SkeletonUtils.clone(gltf.scene);
|
||||
const claim = landmarks[i]
|
||||
const newPlant = SkeletonUtils.clone(gltf.scene)
|
||||
|
||||
const loc = locations[i];
|
||||
newPlant.position.set(loc.x, 0, loc.z);
|
||||
const loc = locations[i]
|
||||
newPlant.position.set(loc.x, 0, loc.z)
|
||||
|
||||
world.scene.add(newPlant);
|
||||
world.scene.add(newPlant)
|
||||
const timeDelayMillis =
|
||||
fakeRealRatio *
|
||||
(new Date(claim.issuedAt).getTime() - minTimeMillis);
|
||||
(new Date(claim.issuedAt).getTime() - minTimeMillis)
|
||||
new TWEEN.Tween(newPlant.scale)
|
||||
.delay(timeDelayMillis)
|
||||
.to({ x: modScale, y: modScale, z: modScale }, 5000)
|
||||
.start();
|
||||
world.bushes = [...world.bushes, newPlant];
|
||||
.start()
|
||||
world.bushes = [...world.bushes, newPlant]
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
function (error) {
|
||||
logger.error(error);
|
||||
},
|
||||
);
|
||||
logger.error(error)
|
||||
}
|
||||
)
|
||||
|
||||
// calculate when lights shine on appearing claim area
|
||||
for (let i = 0; i < landmarks.length; i++) {
|
||||
// claim is a GiveServerRecord (see endorserServer.ts)
|
||||
const claim = landmarks[i];
|
||||
const claim = landmarks[i]
|
||||
|
||||
const loc = locations[i];
|
||||
const light = createLight();
|
||||
light.position.set(loc.x, 20, loc.z);
|
||||
light.target.position.set(loc.x, 0, loc.z);
|
||||
loop.updatables.push(light);
|
||||
scene.add(light);
|
||||
scene.add(light.target);
|
||||
const loc = locations[i]
|
||||
const light = createLight()
|
||||
light.position.set(loc.x, 20, loc.z)
|
||||
light.target.position.set(loc.x, 0, loc.z)
|
||||
loop.updatables.push(light)
|
||||
scene.add(light)
|
||||
scene.add(light.target)
|
||||
|
||||
// now figure out the timing and shine a light
|
||||
const timeDelayMillis =
|
||||
fakeRealRatio * (new Date(claim.issuedAt).getTime() - minTimeMillis);
|
||||
fakeRealRatio * (new Date(claim.issuedAt).getTime() - minTimeMillis)
|
||||
new TWEEN.Tween(light)
|
||||
.delay(timeDelayMillis)
|
||||
.to({ intensity: 100 }, 10)
|
||||
@@ -110,30 +106,30 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
||||
new TWEEN.Tween(light.position)
|
||||
.to({ y: 5 }, 5000)
|
||||
.onComplete(() => {
|
||||
scene.remove(light);
|
||||
light.dispose();
|
||||
}),
|
||||
scene.remove(light)
|
||||
light.dispose()
|
||||
})
|
||||
)
|
||||
.start();
|
||||
world.lights = [...world.lights, light];
|
||||
.start()
|
||||
world.lights = [...world.lights, light]
|
||||
}
|
||||
} else {
|
||||
logger.error(
|
||||
"Got bad server response status & data of",
|
||||
'Got bad server response status & data of',
|
||||
resp.status,
|
||||
resp.data,
|
||||
);
|
||||
resp.data
|
||||
)
|
||||
vue.setAlert(
|
||||
"Error With Server",
|
||||
"There was an error retrieving your claims from the server.",
|
||||
);
|
||||
'Error With Server',
|
||||
'There was an error retrieving your claims from the server.'
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Got exception contacting server:", error);
|
||||
logger.error('Got exception contacting server:', error)
|
||||
vue.setAlert(
|
||||
"Error With Server",
|
||||
"There was a problem retrieving your claims from the server.",
|
||||
);
|
||||
'Error With 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
|
||||
*/
|
||||
function locForGive(giveClaim, platformWidth, borderWidth) {
|
||||
let loc;
|
||||
let loc
|
||||
if (giveClaim?.claim?.recipient?.identifier) {
|
||||
// this is directly to a person
|
||||
loc = locForEthrDid(giveClaim.claim.recipient.identifier);
|
||||
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 };
|
||||
loc = locForEthrDid(giveClaim.claim.recipient.identifier)
|
||||
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 }
|
||||
} else if (giveClaim?.object?.isPartOf?.identifier) {
|
||||
// this is probably to a project
|
||||
const objId = giveClaim.object.isPartOf.identifier;
|
||||
const objId = giveClaim.object.isPartOf.identifier
|
||||
if (objId.startsWith(ENDORSER_ENTITY_PREFIX)) {
|
||||
loc = locForUlid(objId.substring(ENDORSER_ENTITY_PREFIX.length));
|
||||
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 };
|
||||
loc = locForUlid(objId.substring(ENDORSER_ENTITY_PREFIX.length))
|
||||
loc = { x: loc.x - platformWidth / 2, z: loc.z - platformWidth / 2 }
|
||||
}
|
||||
}
|
||||
if (!loc) {
|
||||
// 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 = {
|
||||
x: leftSide
|
||||
? -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
|
||||
*/
|
||||
function locForUlid(ulid) {
|
||||
const xChars = ulid.substring(0, 13).split("").reverse().join("");
|
||||
const zChars = ulid.substring(13, 26).split("").reverse().join("");
|
||||
const xChars = ulid.substring(0, 13).split('').reverse().join('')
|
||||
const zChars = ulid.substring(13, 26).split('').reverse().join('')
|
||||
|
||||
// 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
|
||||
// because the display is pretty low-fidelity at this point.
|
||||
const rawX = BASE32.indexOf(xChars[1]) * 32 + BASE32.indexOf(xChars[0]);
|
||||
const rawZ = BASE32.indexOf(zChars[1]) * 32 + BASE32.indexOf(zChars[0]);
|
||||
const rawX = BASE32.indexOf(xChars[1]) * 32 + BASE32.indexOf(xChars[0])
|
||||
const rawZ = BASE32.indexOf(zChars[1]) * 32 + BASE32.indexOf(zChars[0])
|
||||
|
||||
const x = (100 * rawX) / 1024;
|
||||
const z = (100 * rawZ) / 1024;
|
||||
return { x, z };
|
||||
const x = (100 * rawX) / 1024
|
||||
const z = (100 * rawZ) / 1024
|
||||
return { x, z }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,24 +215,24 @@ function locForUlid(ulid) {
|
||||
function locForEthrDid(did) {
|
||||
// "did:ethr:0x..."
|
||||
if (did.length < 51) {
|
||||
return { x: 0, z: 0 };
|
||||
return { x: 0, z: 0 }
|
||||
} 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.
|
||||
// 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 x = (xOff * 100) / 256;
|
||||
const xOff = parseInt(Number('0x' + randomness.substring(0, 2)), 10)
|
||||
const x = (xOff * 100) / 256
|
||||
// ... and since we're reserving 20 bytes total for x, start z with character 20,
|
||||
// again with one byte.
|
||||
const zOff = parseInt(Number("0x" + randomness.substring(20, 22)), 10);
|
||||
const z = (zOff * 100) / 256;
|
||||
return { x, z };
|
||||
const zOff = parseInt(Number('0x' + randomness.substring(20, 22)), 10)
|
||||
const z = (zOff * 100) / 256
|
||||
return { x, z }
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
light.tick = () => {};
|
||||
return light;
|
||||
light.tick = () => {}
|
||||
return light
|
||||
}
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from "three";
|
||||
import { PlaneGeometry, MeshLambertMaterial, Mesh, TextureLoader } from 'three'
|
||||
|
||||
export function createTerrain(props) {
|
||||
const loader = new TextureLoader();
|
||||
const height = loader.load("img/textures/leafy-autumn-forest-floor.jpg");
|
||||
const loader = new TextureLoader()
|
||||
const height = loader.load('img/textures/leafy-autumn-forest-floor.jpg')
|
||||
// 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({
|
||||
color: props.color,
|
||||
flatShading: true,
|
||||
map: height,
|
||||
map: height
|
||||
//displacementMap: height,
|
||||
//displacementScale: 5,
|
||||
});
|
||||
})
|
||||
|
||||
const plane = new Mesh(geometry, material);
|
||||
plane.position.set(0, 0, 0);
|
||||
plane.rotation.x -= Math.PI * 0.5;
|
||||
const plane = new Mesh(geometry, material)
|
||||
plane.position.set(0, 0, 0)
|
||||
plane.rotation.x -= Math.PI * 0.5
|
||||
|
||||
//Storing our original vertices position on a new attribute
|
||||
plane.geometry.attributes.position.originalPosition =
|
||||
plane.geometry.attributes.position.array;
|
||||
plane.geometry.attributes.position.array
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
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 {
|
||||
constructor(camera, scene, renderer) {
|
||||
this.camera = camera;
|
||||
this.scene = scene;
|
||||
this.renderer = renderer;
|
||||
this.updatables = [];
|
||||
this.camera = camera
|
||||
this.scene = scene
|
||||
this.renderer = renderer
|
||||
this.updatables = []
|
||||
}
|
||||
|
||||
start() {
|
||||
this.renderer.setAnimationLoop(() => {
|
||||
this.tick();
|
||||
this.tick()
|
||||
// render a frame
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
});
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
})
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.renderer.setAnimationLoop(null);
|
||||
this.renderer.setAnimationLoop(null)
|
||||
}
|
||||
|
||||
tick() {
|
||||
const delta = clock.getDelta();
|
||||
const delta = clock.getDelta()
|
||||
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) => {
|
||||
// These are great for full-screen, which adjusts to a window.
|
||||
const height = window.innerHeight;
|
||||
const width = window.innerWidth - 50;
|
||||
const height = window.innerHeight
|
||||
const width = window.innerWidth - 50
|
||||
// These are better for fitting in a container, which stays that size.
|
||||
//const height = container.scrollHeight;
|
||||
//const width = container.scrollWidth;
|
||||
|
||||
camera.aspect = width / height;
|
||||
camera.updateProjectionMatrix();
|
||||
camera.aspect = width / height
|
||||
camera.updateProjectionMatrix()
|
||||
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
};
|
||||
renderer.setSize(width, height)
|
||||
renderer.setPixelRatio(window.devicePixelRatio)
|
||||
}
|
||||
|
||||
class Resizer {
|
||||
constructor(container, camera, renderer) {
|
||||
// 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
|
||||
setSize(container, camera, renderer);
|
||||
setSize(container, camera, renderer)
|
||||
// perform any custom actions
|
||||
this.onResize();
|
||||
});
|
||||
this.onResize()
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
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 { MathUtils } from "three";
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import { MathUtils } from 'three'
|
||||
|
||||
function createControls(camera, canvas) {
|
||||
const controls = new OrbitControls(camera, canvas);
|
||||
const controls = new OrbitControls(camera, canvas)
|
||||
|
||||
//enable controls?
|
||||
controls.enabled = true;
|
||||
controls.autoRotate = false;
|
||||
controls.enabled = true
|
||||
controls.autoRotate = false
|
||||
//controls.autoRotateSpeed = 0.2;
|
||||
|
||||
// control limits
|
||||
@@ -14,8 +14,8 @@ function createControls(camera, canvas) {
|
||||
// to prevent the user from clipping with the objects.
|
||||
|
||||
// y axis
|
||||
controls.minPolarAngle = MathUtils.degToRad(40); // default
|
||||
controls.maxPolarAngle = MathUtils.degToRad(75);
|
||||
controls.minPolarAngle = MathUtils.degToRad(40) // default
|
||||
controls.maxPolarAngle = MathUtils.degToRad(75)
|
||||
|
||||
// x axis
|
||||
// controls.minAzimuthAngle = ...
|
||||
@@ -23,16 +23,16 @@ function createControls(camera, canvas) {
|
||||
|
||||
//smooth camera:
|
||||
// remember to add to loop updatables to work
|
||||
controls.enableDamping = true;
|
||||
controls.enableDamping = true
|
||||
|
||||
//controls.enableZoom = false;
|
||||
controls.maxDistance = 250;
|
||||
controls.maxDistance = 250
|
||||
|
||||
//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() {
|
||||
const renderer = new WebGLRenderer({ antialias: true });
|
||||
const renderer = new WebGLRenderer({ antialias: true })
|
||||
|
||||
// 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.)
|
||||
renderer.physicallyCorrectLights = true;
|
||||
renderer.physicallyCorrectLights = true
|
||||
|
||||
return renderer;
|
||||
return renderer
|
||||
}
|
||||
|
||||
export { createRenderer };
|
||||
export { createRenderer }
|
||||
|
||||
@@ -6,64 +6,63 @@
|
||||
export enum AppString {
|
||||
// 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.
|
||||
APP_NAME = "Time Safari",
|
||||
APP_NAME = 'Time Safari',
|
||||
|
||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||
LOCAL_ENDORSER_API_SERVER = "http://localhost:3000",
|
||||
PROD_ENDORSER_API_SERVER = 'https://api.endorser.ch',
|
||||
TEST_ENDORSER_API_SERVER = 'https://test-api.endorser.ch',
|
||||
LOCAL_ENDORSER_API_SERVER = 'http://localhost:3000',
|
||||
|
||||
PROD_IMAGE_API_SERVER = "https://image-api.timesafari.app",
|
||||
TEST_IMAGE_API_SERVER = "https://test-image-api.timesafari.app",
|
||||
LOCAL_IMAGE_API_SERVER = "http://localhost:3001",
|
||||
PROD_IMAGE_API_SERVER = 'https://image-api.timesafari.app',
|
||||
TEST_IMAGE_API_SERVER = 'https://test-image-api.timesafari.app',
|
||||
LOCAL_IMAGE_API_SERVER = 'http://localhost:3001',
|
||||
|
||||
PROD_PARTNER_API_SERVER = "https://partner-api.endorser.ch",
|
||||
TEST_PARTNER_API_SERVER = "https://test-partner-api.endorser.ch",
|
||||
PROD_PARTNER_API_SERVER = 'https://partner-api.endorser.ch',
|
||||
TEST_PARTNER_API_SERVER = 'https://test-partner-api.endorser.ch',
|
||||
LOCAL_PARTNER_API_SERVER = LOCAL_ENDORSER_API_SERVER,
|
||||
|
||||
PROD_PUSH_SERVER = "https://timesafari.app",
|
||||
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
||||
TEST2_PUSH_SERVER = "https://timesafari-pwa.anomalistlabs.com",
|
||||
PROD_PUSH_SERVER = 'https://timesafari.app',
|
||||
TEST1_PUSH_SERVER = 'https://test.timesafari.app',
|
||||
TEST2_PUSH_SERVER = 'https://timesafari-pwa.anomalistlabs.com',
|
||||
|
||||
NO_CONTACT_NAME = "(no name)",
|
||||
NO_CONTACT_NAME = '(no name)'
|
||||
}
|
||||
|
||||
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 =
|
||||
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
||||
AppString.TEST_ENDORSER_API_SERVER;
|
||||
AppString.TEST_ENDORSER_API_SERVER
|
||||
|
||||
export const 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 =
|
||||
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
|
||||
AppString.TEST_PARTNER_API_SERVER;
|
||||
AppString.TEST_PARTNER_API_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 =
|
||||
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
|
||||
export const PASSKEYS_ENABLED = !!import.meta.env.VITE_PASSKEYS_ENABLED || false
|
||||
|
||||
/**
|
||||
* The possible values for "group" and "type" are in App.vue.
|
||||
* Some of this comes from the notiwind package, some is custom.
|
||||
*/
|
||||
export interface NotificationIface {
|
||||
group: string; // "alert" | "modal"
|
||||
type: string; // "toast" | "info" | "success" | "warning" | "danger"
|
||||
title: string;
|
||||
text?: string;
|
||||
callback?: (success: boolean) => Promise<void>; // if this triggered an action
|
||||
noText?: string;
|
||||
onCancel?: (stopAsking?: boolean) => Promise<void>;
|
||||
onNo?: (stopAsking?: boolean) => Promise<void>;
|
||||
onYes?: () => Promise<void>;
|
||||
promptToStopAsking?: boolean;
|
||||
yesText?: string;
|
||||
group: string // "alert" | "modal"
|
||||
type: string // "toast" | "info" | "success" | "warning" | "danger"
|
||||
title: string
|
||||
text?: string
|
||||
callback?: (success: boolean) => Promise<void> // if this triggered an action
|
||||
noText?: string
|
||||
onCancel?: (stopAsking?: boolean) => Promise<void>
|
||||
onNo?: (stopAsking?: boolean) => Promise<void>
|
||||
onYes?: () => Promise<void>
|
||||
promptToStopAsking?: boolean
|
||||
yesText?: string
|
||||
}
|
||||
|
||||
172
src/db/index.ts
172
src/db/index.ts
@@ -1,44 +1,44 @@
|
||||
import BaseDexie, { Table } from "dexie";
|
||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||
import * as R from "ramda";
|
||||
import BaseDexie, { Table } from 'dexie'
|
||||
import { encrypted, Encryption } from '@pvermeer/dexie-encrypted-addon'
|
||||
import * as R from 'ramda'
|
||||
|
||||
import { Account, AccountsSchema } from "./tables/accounts";
|
||||
import { Contact, ContactSchema } from "./tables/contacts";
|
||||
import { Log, LogSchema } from "./tables/logs";
|
||||
import { MASTER_SECRET_KEY, Secret, SecretSchema } from "./tables/secret";
|
||||
import { Account, AccountsSchema } from './tables/accounts'
|
||||
import { Contact, ContactSchema } from './tables/contacts'
|
||||
import { Log, LogSchema } from './tables/logs'
|
||||
import { MASTER_SECRET_KEY, Secret, SecretSchema } from './tables/secret'
|
||||
import {
|
||||
MASTER_SETTINGS_KEY,
|
||||
Settings,
|
||||
SettingsSchema,
|
||||
} from "./tables/settings";
|
||||
import { Temp, TempSchema } from "./tables/temp";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app";
|
||||
import { logger } from "../utils/logger";
|
||||
SettingsSchema
|
||||
} from './tables/settings'
|
||||
import { Temp, TempSchema } from './tables/temp'
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from '../constants/app'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
// Define types for tables that hold sensitive and non-sensitive data
|
||||
type SecretTable = { secret: Table<Secret> };
|
||||
type SensitiveTables = { accounts: Table<Account> };
|
||||
type SecretTable = { secret: Table<Secret> }
|
||||
type SensitiveTables = { accounts: Table<Account> }
|
||||
type NonsensitiveTables = {
|
||||
contacts: Table<Contact>;
|
||||
logs: Table<Log>;
|
||||
settings: Table<Settings>;
|
||||
temp: Table<Temp>;
|
||||
};
|
||||
contacts: Table<Contact>
|
||||
logs: Table<Log>
|
||||
settings: Table<Settings>
|
||||
temp: Table<Temp>
|
||||
}
|
||||
|
||||
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
||||
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T
|
||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T
|
||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||
BaseDexie & T;
|
||||
BaseDexie & T
|
||||
|
||||
//// Initialize the DBs, starting with the sensitive ones.
|
||||
|
||||
// Initialize Dexie database for secret, which is then used to encrypt accountsDB
|
||||
export const secretDB = new BaseDexie("TimeSafariSecret") as SecretDexie;
|
||||
secretDB.version(1).stores(SecretSchema);
|
||||
export const secretDB = new BaseDexie('TimeSafariSecret') as SecretDexie
|
||||
secretDB.version(1).stores(SecretSchema)
|
||||
|
||||
// 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
|
||||
// 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.
|
||||
export const accountsDBPromise = useSecretAndInitializeAccountsDB(
|
||||
secretDB,
|
||||
accountsDexie,
|
||||
);
|
||||
accountsDexie
|
||||
)
|
||||
|
||||
//// Now initialize the other DB.
|
||||
|
||||
// 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
|
||||
|
||||
@@ -64,31 +64,31 @@ export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||
db.version(2).stores({
|
||||
...ContactSchema,
|
||||
...LogSchema,
|
||||
...{ settings: "id" }, // old Settings schema
|
||||
});
|
||||
...{ settings: 'id' } // old Settings schema
|
||||
})
|
||||
// v3 added Temp
|
||||
db.version(3).stores(TempSchema);
|
||||
db.version(3).stores(TempSchema)
|
||||
db.version(4)
|
||||
.stores(SettingsSchema)
|
||||
.upgrade((tx) => {
|
||||
return tx
|
||||
.table("settings")
|
||||
.table('settings')
|
||||
.toCollection()
|
||||
.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 = {
|
||||
id: MASTER_SETTINGS_KEY,
|
||||
activeDid: undefined,
|
||||
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
||||
};
|
||||
apiServer: DEFAULT_ENDORSER_API_SERVER
|
||||
}
|
||||
|
||||
// Event handler to initialize the non-sensitive database with default settings
|
||||
db.on("populate", async () => {
|
||||
await db.settings.add(DEFAULT_SETTINGS);
|
||||
});
|
||||
db.on('populate', async () => {
|
||||
await db.settings.add(DEFAULT_SETTINGS)
|
||||
})
|
||||
|
||||
// Manage the encryption key.
|
||||
|
||||
@@ -114,130 +114,130 @@ db.on("populate", async () => {
|
||||
|
||||
async function useSecretAndInitializeAccountsDB(
|
||||
secretDB: SecretDexie,
|
||||
accountsDB: SensitiveDexie,
|
||||
accountsDB: SensitiveDexie
|
||||
): Promise<SensitiveDexie> {
|
||||
return secretDB
|
||||
.open()
|
||||
.then(() => {
|
||||
return secretDB.secret.get(MASTER_SECRET_KEY);
|
||||
return secretDB.secret.get(MASTER_SECRET_KEY)
|
||||
})
|
||||
.then((secretRow?: Secret) => {
|
||||
let secret = secretRow?.secret;
|
||||
let secret = secretRow?.secret
|
||||
if (secret != null) {
|
||||
// they already have it in IndexedDB, so just pass it along
|
||||
return secret;
|
||||
return secret
|
||||
} else {
|
||||
// check localStorage (for users before v 0.3.37)
|
||||
const localSecret = localStorage.getItem("secret");
|
||||
const localSecret = localStorage.getItem('secret')
|
||||
if (localSecret != null) {
|
||||
// they had one, so we want to move it to IndexedDB
|
||||
secret = localSecret;
|
||||
secret = localSecret
|
||||
} else {
|
||||
// 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
|
||||
return secretDB.secret
|
||||
.add({ id: MASTER_SECRET_KEY, secret })
|
||||
.then(() => {
|
||||
return secret;
|
||||
});
|
||||
return secret
|
||||
})
|
||||
}
|
||||
})
|
||||
.then((secret?: string) => {
|
||||
if (secret == null) {
|
||||
throw new Error("No secret found or created.");
|
||||
throw new Error('No secret found or created.')
|
||||
} else {
|
||||
// apply encryption to the sensitive database using the secret key
|
||||
encrypted(accountsDB, { secretKey: secret });
|
||||
accountsDB.version(1).stores(AccountsSchema);
|
||||
accountsDB.open();
|
||||
return accountsDB;
|
||||
encrypted(accountsDB, { secretKey: secret })
|
||||
accountsDB.version(1).stores(AccountsSchema)
|
||||
accountsDB.open()
|
||||
return accountsDB
|
||||
}
|
||||
})
|
||||
.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.");
|
||||
throw error;
|
||||
});
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
// retrieves default settings
|
||||
// calls db.open()
|
||||
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
||||
await db.open();
|
||||
return (await db.settings.get(MASTER_SETTINGS_KEY)) || DEFAULT_SETTINGS;
|
||||
await db.open()
|
||||
return (await db.settings.get(MASTER_SETTINGS_KEY)) || DEFAULT_SETTINGS
|
||||
}
|
||||
|
||||
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
||||
const defaultSettings = await retrieveSettingsForDefaultAccount()
|
||||
if (!defaultSettings.activeDid) {
|
||||
return defaultSettings;
|
||||
return defaultSettings
|
||||
} else {
|
||||
const overrideSettings =
|
||||
(await db.settings
|
||||
.where("accountDid")
|
||||
.where('accountDid')
|
||||
.equals(defaultSettings.activeDid)
|
||||
.first()) || {};
|
||||
return R.mergeDeepRight(defaultSettings, overrideSettings);
|
||||
.first()) || {}
|
||||
return R.mergeDeepRight(defaultSettings, overrideSettings)
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateDefaultSettings(
|
||||
settingsChanges: Settings,
|
||||
settingsChanges: Settings
|
||||
): Promise<void> {
|
||||
delete settingsChanges.accountDid; // just in case
|
||||
delete settingsChanges.accountDid // just in case
|
||||
// ensure there is no "id" that would override the key
|
||||
delete settingsChanges.id;
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, settingsChanges);
|
||||
delete settingsChanges.id
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, settingsChanges)
|
||||
}
|
||||
|
||||
export async function updateAccountSettings(
|
||||
accountDid: string,
|
||||
settingsChanges: Settings,
|
||||
settingsChanges: Settings
|
||||
): Promise<void> {
|
||||
settingsChanges.accountDid = accountDid;
|
||||
delete settingsChanges.id; // key off account, not ID
|
||||
settingsChanges.accountDid = accountDid
|
||||
delete settingsChanges.id // key off account, not ID
|
||||
const result = await db.settings
|
||||
.where("accountDid")
|
||||
.where('accountDid')
|
||||
.equals(settingsChanges.accountDid)
|
||||
.modify(settingsChanges);
|
||||
.modify(settingsChanges)
|
||||
if (result === 0) {
|
||||
if (!settingsChanges.id) {
|
||||
// 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,
|
||||
// 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> {
|
||||
await db.open();
|
||||
const todayKey = new Date().toDateString();
|
||||
await db.open()
|
||||
const todayKey = new Date().toDateString()
|
||||
// only keep one day's worth of logs
|
||||
const previous = await db.logs.get(todayKey);
|
||||
const previous = await db.logs.get(todayKey)
|
||||
if (!previous) {
|
||||
// when this is today's first log, clear out everything previous
|
||||
// to avoid the log table getting too large
|
||||
// (let's limit a different way someday)
|
||||
await db.logs.clear();
|
||||
await db.logs.clear()
|
||||
}
|
||||
const prevMessages = (previous && previous.message) || "";
|
||||
const fullMessage = `${prevMessages}\n${new Date().toISOString()} ${message}`;
|
||||
await db.logs.update(todayKey, { message: fullMessage });
|
||||
const prevMessages = (previous && previous.message) || ''
|
||||
const fullMessage = `${prevMessages}\n${new Date().toISOString()} ${message}`
|
||||
await db.logs.update(todayKey, { message: fullMessage })
|
||||
}
|
||||
|
||||
// similar method is in the sw_scripts/additional-scripts.js file
|
||||
export async function logConsoleAndDb(
|
||||
message: string,
|
||||
isError = false,
|
||||
isError = false
|
||||
): Promise<void> {
|
||||
if (isError) {
|
||||
logger.error(`${new Date().toISOString()} ${message}`);
|
||||
logger.error(`${new Date().toISOString()} ${message}`)
|
||||
} 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
dateCreated: string;
|
||||
dateCreated: string
|
||||
|
||||
/**
|
||||
* The derivation path for the account, if this is from a mnemonic
|
||||
*/
|
||||
derivationPath?: string;
|
||||
derivationPath?: string
|
||||
|
||||
/**
|
||||
* Decentralized Identifier (DID) for the account
|
||||
*/
|
||||
did: string;
|
||||
did: string
|
||||
|
||||
/**
|
||||
* Stringified JSON containing underlying key material, if generated from a mnemonic
|
||||
* Based on the IIdentifier type from Veramo
|
||||
* @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
|
||||
*/
|
||||
mnemonic?: string;
|
||||
mnemonic?: string
|
||||
|
||||
/**
|
||||
* The Webauthn credential ID in hex, if this is from a passkey
|
||||
*/
|
||||
passkeyCredIdHex?: string;
|
||||
passkeyCredIdHex?: string
|
||||
|
||||
/**
|
||||
* The public key in hexadecimal format
|
||||
*/
|
||||
publicKeyHex: string;
|
||||
};
|
||||
publicKeyHex: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for the accounts table in the database.
|
||||
@@ -52,5 +52,5 @@ export type Account = {
|
||||
*/
|
||||
export const AccountsSchema = {
|
||||
accounts:
|
||||
"++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex",
|
||||
};
|
||||
'++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex'
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
export interface ContactMethod {
|
||||
label: string;
|
||||
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
||||
value: string;
|
||||
label: string
|
||||
type: string // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface Contact {
|
||||
//
|
||||
// When adding a property, consider whether it should be added when exporting & sharing contacts.
|
||||
|
||||
did: string;
|
||||
contactMethods?: Array<ContactMethod>;
|
||||
name?: string;
|
||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
||||
notes?: string;
|
||||
profileImageUrl?: string;
|
||||
publicKeyBase64?: string;
|
||||
seesMe?: boolean; // cached value of the server setting
|
||||
registered?: boolean; // cached value of the server setting
|
||||
did: string
|
||||
contactMethods?: Array<ContactMethod>
|
||||
name?: string
|
||||
nextPubKeyHashB64?: string // base64-encoded SHA256 hash of next public key
|
||||
notes?: string
|
||||
profileImageUrl?: string
|
||||
publicKeyBase64?: string
|
||||
seesMe?: boolean // cached value of the server setting
|
||||
registered?: boolean // cached value of the server setting
|
||||
}
|
||||
|
||||
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 {
|
||||
date: string;
|
||||
message: string;
|
||||
date: string
|
||||
message: string
|
||||
}
|
||||
|
||||
export const LogSchema = {
|
||||
// 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.
|
||||
// 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
|
||||
*/
|
||||
id: number;
|
||||
id: number
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
export type BoundingBox = {
|
||||
eastLong: number; // Eastern longitude
|
||||
maxLat: number; // Maximum (Northernmost) latitude
|
||||
minLat: number; // Minimum (Southernmost) latitude
|
||||
westLong: number; // Western longitude
|
||||
};
|
||||
eastLong: number // Eastern longitude
|
||||
maxLat: number // Maximum (Northernmost) latitude
|
||||
minLat: number // Minimum (Southernmost) latitude
|
||||
westLong: number // Western longitude
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings type encompasses user-specific configuration details.
|
||||
*/
|
||||
export type Settings = {
|
||||
// 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
|
||||
accountDid?: string; // not used in the MASTER_SETTINGS_KEY entry
|
||||
accountDid?: string // not used in the MASTER_SETTINGS_KEY entry
|
||||
// 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
|
||||
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
|
||||
finishedOnboarding?: boolean; // the user has completed the onboarding process
|
||||
filterFeedByNearby?: boolean // filter by nearby
|
||||
filterFeedByVisible?: boolean // filter by visible users ie. anyone not hidden
|
||||
finishedOnboarding?: boolean // the user has completed the onboarding process
|
||||
|
||||
firstName?: string; // user's full name, may be null if unwanted for a particular account
|
||||
hideRegisterPromptOnNewContact?: boolean;
|
||||
isRegistered?: boolean;
|
||||
firstName?: string // user's full name, may be null if unwanted for a particular account
|
||||
hideRegisterPromptOnNewContact?: boolean
|
||||
isRegistered?: boolean
|
||||
// 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
|
||||
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects 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
|
||||
|
||||
// The claim list has a most recent one used in notifications that's separate from the last viewed
|
||||
lastNotifiedClaimId?: string;
|
||||
lastViewedClaimId?: string;
|
||||
lastNotifiedClaimId?: string
|
||||
lastViewedClaimId?: string
|
||||
|
||||
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
|
||||
notifyingReminderTime?: string; // set to their chosen time for a daily reminder
|
||||
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
|
||||
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
|
||||
searchBoxes?: Array<{
|
||||
name: string;
|
||||
bbox: BoundingBox;
|
||||
}>;
|
||||
name: string
|
||||
bbox: BoundingBox
|
||||
}>
|
||||
|
||||
showContactGivesInline?: boolean; // Display contact inline or not
|
||||
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
|
||||
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
|
||||
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
||||
warnIfProdServer?: boolean; // Warn if using a production server
|
||||
warnIfTestServer?: boolean; // Warn if using a testing server
|
||||
webPushServer?: string; // Web Push server URL
|
||||
};
|
||||
showContactGivesInline?: boolean // Display contact inline or not
|
||||
showGeneralAdvanced?: boolean // Show advanced features which don't have their own flag
|
||||
showShortcutBvc?: boolean // Show shortcut for Bountiful Voluntaryist Community actions
|
||||
vapid?: string // VAPID (Voluntary Application Server Identification) field for web push
|
||||
warnIfProdServer?: boolean // Warn if using a production server
|
||||
warnIfTestServer?: boolean // Warn if using a testing server
|
||||
webPushServer?: string // Web Push server URL
|
||||
}
|
||||
|
||||
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
|
||||
return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible);
|
||||
return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible)
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema for the Settings table in the database.
|
||||
*/
|
||||
export const SettingsSchema = {
|
||||
settings: "id, &accountDid",
|
||||
};
|
||||
settings: 'id, &accountDid'
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
export type Temp = {
|
||||
id: string;
|
||||
blob?: Blob; // deprecated because webkit (Safari) does not support Blob
|
||||
blobB64?: string; // base64-encoded blob
|
||||
};
|
||||
id: string
|
||||
blob?: Blob // deprecated because webkit (Safari) does not support Blob
|
||||
blobB64?: string // base64-encoded blob
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 path = require("path");
|
||||
const fs = require("fs");
|
||||
const logger = require("../utils/logger");
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
// Check if running in dev mode
|
||||
const isDev = process.argv.includes("--inspect");
|
||||
const isDev = process.argv.includes('--inspect')
|
||||
|
||||
function createWindow() {
|
||||
// Add before createWindow function
|
||||
const preloadPath = path.join(__dirname, "preload.js");
|
||||
logger.log("Checking preload path:", preloadPath);
|
||||
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
||||
const preloadPath = path.join(__dirname, 'preload.js')
|
||||
logger.log('Checking preload path:', preloadPath)
|
||||
logger.log('Preload exists:', fs.existsSync(preloadPath))
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
@@ -21,71 +21,71 @@ function createWindow() {
|
||||
contextIsolation: true,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
preload: path.join(__dirname, 'preload.js')
|
||||
}
|
||||
})
|
||||
|
||||
// Always open DevTools for now
|
||||
mainWindow.webContents.openDevTools();
|
||||
mainWindow.webContents.openDevTools()
|
||||
|
||||
// Intercept requests to fix asset paths
|
||||
mainWindow.webContents.session.webRequest.onBeforeRequest(
|
||||
{
|
||||
urls: [
|
||||
"file://*/*/assets/*",
|
||||
"file://*/assets/*",
|
||||
"file:///assets/*", // Catch absolute paths
|
||||
"<all_urls>", // Catch all URLs as a fallback
|
||||
],
|
||||
'file://*/*/assets/*',
|
||||
'file://*/assets/*',
|
||||
'file:///assets/*', // Catch absolute paths
|
||||
'<all_urls>' // Catch all URLs as a fallback
|
||||
]
|
||||
},
|
||||
(details, callback) => {
|
||||
let url = details.url;
|
||||
let url = details.url
|
||||
|
||||
// Handle paths that don't start with file://
|
||||
if (!url.startsWith("file://") && url.includes("/assets/")) {
|
||||
url = `file://${path.join(__dirname, "www", url)}`;
|
||||
if (!url.startsWith('file://') && url.includes('/assets/')) {
|
||||
url = `file://${path.join(__dirname, 'www', url)}`
|
||||
}
|
||||
|
||||
// Handle absolute paths starting with /assets/
|
||||
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
|
||||
const baseDir = url.includes("dist-electron")
|
||||
if (url.includes('/assets/') && !url.includes('/www/assets/')) {
|
||||
const baseDir = url.includes('dist-electron')
|
||||
? url.substring(
|
||||
0,
|
||||
url.indexOf("/dist-electron") + "/dist-electron".length,
|
||||
url.indexOf('/dist-electron') + '/dist-electron'.length
|
||||
)
|
||||
: `file://${__dirname}`;
|
||||
const assetPath = url.split("/assets/")[1];
|
||||
const newUrl = `${baseDir}/www/assets/${assetPath}`;
|
||||
callback({ redirectURL: newUrl });
|
||||
return;
|
||||
: `file://${__dirname}`
|
||||
const assetPath = url.split('/assets/')[1]
|
||||
const newUrl = `${baseDir}/www/assets/${assetPath}`
|
||||
callback({ redirectURL: newUrl })
|
||||
return
|
||||
}
|
||||
|
||||
callback({}); // No redirect for other URLs
|
||||
},
|
||||
);
|
||||
callback({}) // No redirect for other URLs
|
||||
}
|
||||
)
|
||||
|
||||
if (isDev) {
|
||||
// Debug info
|
||||
logger.log("Debug Info:");
|
||||
logger.log("Running in dev mode:", isDev);
|
||||
logger.log("App is packaged:", app.isPackaged);
|
||||
logger.log("Process resource path:", process.resourcesPath);
|
||||
logger.log("App path:", app.getAppPath());
|
||||
logger.log("__dirname:", __dirname);
|
||||
logger.log("process.cwd():", process.cwd());
|
||||
logger.log('Debug Info:')
|
||||
logger.log('Running in dev mode:', isDev)
|
||||
logger.log('App is packaged:', app.isPackaged)
|
||||
logger.log('Process resource path:', process.resourcesPath)
|
||||
logger.log('App path:', app.getAppPath())
|
||||
logger.log('__dirname:', __dirname)
|
||||
logger.log('process.cwd():', process.cwd())
|
||||
}
|
||||
|
||||
const indexPath = path.join(__dirname, "www", "index.html");
|
||||
const indexPath = path.join(__dirname, 'www', 'index.html')
|
||||
|
||||
if (isDev) {
|
||||
logger.log("Loading index from:", indexPath);
|
||||
logger.log("www path:", path.join(__dirname, "www"));
|
||||
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
|
||||
logger.log('Loading index from:', indexPath)
|
||||
logger.log('www path:', path.join(__dirname, 'www'))
|
||||
logger.log('www assets path:', path.join(__dirname, 'www', 'assets'))
|
||||
}
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
logger.error(`Index file not found at: ${indexPath}`);
|
||||
throw new Error("Index file not found");
|
||||
logger.error(`Index file not found at: ${indexPath}`)
|
||||
throw new Error('Index file not found')
|
||||
}
|
||||
|
||||
// Add CSP headers to allow API connections
|
||||
@@ -94,81 +94,81 @@ function createWindow() {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
"Content-Security-Policy": [
|
||||
'Content-Security-Policy': [
|
||||
"default-src 'self';" +
|
||||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app;" +
|
||||
"img-src 'self' data: https: blob:;" +
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
|
||||
"style-src 'self' 'unsafe-inline';" +
|
||||
"font-src 'self' data:;",
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
"font-src 'self' data:;"
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Load the index.html
|
||||
mainWindow
|
||||
.loadFile(indexPath)
|
||||
.then(() => {
|
||||
logger.log("Successfully loaded index.html");
|
||||
logger.log('Successfully loaded index.html')
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
logger.log("DevTools opened - running in dev mode");
|
||||
mainWindow.webContents.openDevTools()
|
||||
logger.log('DevTools opened - running in dev mode')
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to load index.html:", err);
|
||||
logger.error("Attempted path:", indexPath);
|
||||
});
|
||||
logger.error('Failed to load index.html:', err)
|
||||
logger.error('Attempted path:', indexPath)
|
||||
})
|
||||
|
||||
// Listen for console messages from the renderer
|
||||
mainWindow.webContents.on("console-message", (_event, level, message) => {
|
||||
logger.log("Renderer Console:", message);
|
||||
});
|
||||
mainWindow.webContents.on('console-message', (_event, level, message) => {
|
||||
logger.log('Renderer Console:', message)
|
||||
})
|
||||
|
||||
// Add right after creating the BrowserWindow
|
||||
mainWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
'did-fail-load',
|
||||
(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) => {
|
||||
logger.error("Preload script error:", preloadPath, error);
|
||||
});
|
||||
mainWindow.webContents.on('preload-error', (event, preloadPath, error) => {
|
||||
logger.error('Preload script error:', preloadPath, error)
|
||||
})
|
||||
|
||||
mainWindow.webContents.on(
|
||||
"console-message",
|
||||
'console-message',
|
||||
(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
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
}
|
||||
|
||||
// Handle app ready
|
||||
app.whenReady().then(createWindow);
|
||||
app.whenReady().then(createWindow)
|
||||
|
||||
// Handle all windows closed
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
app.on("activate", () => {
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
createWindow()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// Handle any errors
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error("Uncaught Exception:", error);
|
||||
});
|
||||
process.on('uncaughtException', (error) => {
|
||||
logger.error('Uncaught Exception:', error)
|
||||
})
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
|
||||
const logger = {
|
||||
log: (message, ...args) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
/* eslint-disable no-console */
|
||||
console.log(message, ...args);
|
||||
console.log(message, ...args)
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
},
|
||||
warn: (message, ...args) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
/* eslint-disable no-console */
|
||||
console.warn(message, ...args);
|
||||
console.warn(message, ...args)
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
},
|
||||
error: (message, ...args) => {
|
||||
/* 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 */
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Use a more direct path resolution approach
|
||||
const getPath = (pathType) => {
|
||||
switch (pathType) {
|
||||
case "userData":
|
||||
case 'userData':
|
||||
return (
|
||||
process.env.APPDATA ||
|
||||
(process.platform === "darwin"
|
||||
(process.platform === 'darwin'
|
||||
? `${process.env.HOME}/Library/Application Support`
|
||||
: `${process.env.HOME}/.local/share`)
|
||||
);
|
||||
case "home":
|
||||
return process.env.HOME;
|
||||
case "appPath":
|
||||
return process.resourcesPath;
|
||||
)
|
||||
case 'home':
|
||||
return process.env.HOME
|
||||
case 'appPath':
|
||||
return process.resourcesPath
|
||||
default:
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
logger.log("Preload script starting...");
|
||||
logger.log('Preload script starting...')
|
||||
|
||||
try {
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Path utilities
|
||||
getPath,
|
||||
|
||||
// IPC functions
|
||||
send: (channel, data) => {
|
||||
const validChannels = ["toMain"];
|
||||
const validChannels = ['toMain']
|
||||
if (validChannels.includes(channel)) {
|
||||
ipcRenderer.send(channel, data);
|
||||
ipcRenderer.send(channel, data)
|
||||
}
|
||||
},
|
||||
receive: (channel, func) => {
|
||||
const validChannels = ["fromMain"];
|
||||
const validChannels = ['fromMain']
|
||||
if (validChannels.includes(channel)) {
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
||||
ipcRenderer.on(channel, (event, ...args) => func(...args))
|
||||
}
|
||||
},
|
||||
// Environment info
|
||||
env: {
|
||||
isElectron: true,
|
||||
isDev: process.env.NODE_ENV === "development",
|
||||
isDev: process.env.NODE_ENV === 'development'
|
||||
},
|
||||
// Path utilities
|
||||
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) {
|
||||
logger.error("Error in preload script:", error);
|
||||
logger.error('Error in preload script:', error)
|
||||
}
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { GiverReceiverInputInfo } from "../libs/util";
|
||||
import { ErrorResult, ResultWithType } from "./common";
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { GiverReceiverInputInfo } from '../libs/util'
|
||||
import { ErrorResult, ResultWithType } from './common'
|
||||
|
||||
export interface GiverOutputInfo {
|
||||
action: string;
|
||||
giver?: GiverReceiverInputInfo;
|
||||
description?: string;
|
||||
amount?: number;
|
||||
unitCode?: string;
|
||||
action: string
|
||||
giver?: GiverReceiverInputInfo
|
||||
description?: string
|
||||
amount?: number
|
||||
unitCode?: string
|
||||
}
|
||||
|
||||
export interface ClaimResult {
|
||||
success: { claimId: string; handleId: string };
|
||||
error: { code: string; message: string };
|
||||
success: { claimId: string; handleId: string }
|
||||
error: { code: string; message: string }
|
||||
}
|
||||
|
||||
export interface VerifiableCredential {
|
||||
exp?: number;
|
||||
iat: number;
|
||||
iss: string;
|
||||
exp?: number
|
||||
iat: number
|
||||
iss: string
|
||||
vc: {
|
||||
"@context": string[];
|
||||
type: string[];
|
||||
credentialSubject: VerifiableCredentialSubject;
|
||||
};
|
||||
'@context': string[]
|
||||
type: string[]
|
||||
credentialSubject: VerifiableCredentialSubject
|
||||
}
|
||||
}
|
||||
|
||||
export interface VerifiableCredentialSubject {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
[key: string]: unknown;
|
||||
'@context': string
|
||||
'@type': string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface WorldProperties {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
/**
|
||||
* Could be a DID or a handleId that identifies the provider
|
||||
*/
|
||||
identifier: string;
|
||||
identifier: string
|
||||
/**
|
||||
* Indicates if the provider link has been confirmed
|
||||
*/
|
||||
linkConfirmed: boolean;
|
||||
linkConfirmed: boolean
|
||||
}
|
||||
|
||||
// Type for createAndSubmitClaim result
|
||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult
|
||||
|
||||
// Update SuccessResult to use ClaimResult
|
||||
export interface SuccessResult extends ResultWithType {
|
||||
type: "success";
|
||||
response: AxiosResponse<ClaimResult>;
|
||||
type: 'success'
|
||||
response: AxiosResponse<ClaimResult>
|
||||
}
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
import { GenericVerifiableCredential } from "./common";
|
||||
import { GenericVerifiableCredential } from './common'
|
||||
|
||||
export interface AgreeVerifiableCredential {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
object: Record<string, unknown>;
|
||||
'@context': string
|
||||
'@type': string
|
||||
object: Record<string, unknown>
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id4
|
||||
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
"@type": "GiveAction";
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
|
||||
identifier?: string;
|
||||
image?: string;
|
||||
object?: { amountOfThisGood: number; unitCode: string };
|
||||
provider?: GenericVerifiableCredential;
|
||||
recipient?: { identifier: string };
|
||||
'@context'?: string
|
||||
'@type': 'GiveAction'
|
||||
agent?: { identifier: string }
|
||||
description?: string
|
||||
fulfills?: { '@type': string; identifier?: string; lastClaimId?: string }[]
|
||||
identifier?: string
|
||||
image?: string
|
||||
object?: { amountOfThisGood: number; unitCode: string }
|
||||
provider?: GenericVerifiableCredential
|
||||
recipient?: { identifier: string }
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id8
|
||||
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
"@type": "Offer";
|
||||
description?: string;
|
||||
includesObject?: { amountOfThisGood: number; unitCode: string };
|
||||
'@context'?: string
|
||||
'@type': 'Offer'
|
||||
description?: string
|
||||
includesObject?: { amountOfThisGood: number; unitCode: string }
|
||||
itemOffered?: {
|
||||
description?: string;
|
||||
description?: string
|
||||
isPartOf?: {
|
||||
identifier?: string;
|
||||
lastClaimId?: string;
|
||||
"@type"?: string;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
offeredBy?: { identifier: string };
|
||||
recipient?: { identifier: string };
|
||||
validThrough?: string;
|
||||
identifier?: string
|
||||
lastClaimId?: string
|
||||
'@type'?: string
|
||||
name?: string
|
||||
}
|
||||
}
|
||||
offeredBy?: { identifier: string }
|
||||
recipient?: { identifier: string }
|
||||
validThrough?: string
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id7
|
||||
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context": "https://schema.org";
|
||||
"@type": "PlanAction";
|
||||
name: string;
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
identifier?: string;
|
||||
lastClaimId?: string;
|
||||
'@context': 'https://schema.org'
|
||||
'@type': 'PlanAction'
|
||||
name: string
|
||||
agent?: { identifier: string }
|
||||
description?: string
|
||||
identifier?: string
|
||||
lastClaimId?: string
|
||||
location?: {
|
||||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
||||
};
|
||||
geo: { '@type': 'GeoCoordinates'; latitude: number; longitude: number }
|
||||
}
|
||||
}
|
||||
|
||||
// AKA Registration & RegisterAction
|
||||
export interface RegisterVerifiableCredential {
|
||||
"@context": string;
|
||||
"@type": "RegisterAction";
|
||||
agent: { identifier: string };
|
||||
identifier?: string;
|
||||
object: string;
|
||||
participant?: { identifier: string };
|
||||
'@context': string
|
||||
'@type': 'RegisterAction'
|
||||
agent: { identifier: string }
|
||||
identifier?: string
|
||||
object: string
|
||||
participant?: { identifier: string }
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
// similar to VerifiableCredentialSubject... maybe rename this
|
||||
export interface GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
"@type": string;
|
||||
[key: string]: unknown;
|
||||
'@context'?: string
|
||||
'@type': string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
||||
claim: T;
|
||||
claimType?: string;
|
||||
handleId: string;
|
||||
id: string;
|
||||
issuedAt: string;
|
||||
issuer: string;
|
||||
publicUrls?: Record<string, string>;
|
||||
claim: T
|
||||
claimType?: string
|
||||
handleId: string
|
||||
id: string
|
||||
issuedAt: string
|
||||
issuer: string
|
||||
publicUrls?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface ResultWithType {
|
||||
type: string;
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error?: {
|
||||
message?: string;
|
||||
};
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface InternalError {
|
||||
error: string;
|
||||
userMessage?: string;
|
||||
error: string
|
||||
userMessage?: string
|
||||
}
|
||||
|
||||
export interface ErrorResult extends ResultWithType {
|
||||
type: "error";
|
||||
error: InternalError;
|
||||
type: 'error'
|
||||
error: InternalError
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
*/
|
||||
|
||||
export interface DeepLinkError extends Error {
|
||||
code: string;
|
||||
details?: unknown;
|
||||
code: string
|
||||
details?: unknown
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from "./claims";
|
||||
export * from "./claims-result";
|
||||
export * from "./common";
|
||||
export * from "./limits";
|
||||
export * from "./records";
|
||||
export * from "./user";
|
||||
export * from "./deepLinks";
|
||||
export * from './claims'
|
||||
export * from './claims-result'
|
||||
export * from './common'
|
||||
export * from './limits'
|
||||
export * from './records'
|
||||
export * from './user'
|
||||
export * from './deepLinks'
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
export interface EndorserRateLimits {
|
||||
doneClaimsThisWeek: string;
|
||||
doneRegistrationsThisMonth: string;
|
||||
maxClaimsPerWeek: string;
|
||||
maxRegistrationsPerMonth: string;
|
||||
nextMonthBeginDateTime: string;
|
||||
nextWeekBeginDateTime: string;
|
||||
doneClaimsThisWeek: string
|
||||
doneRegistrationsThisMonth: string
|
||||
maxClaimsPerWeek: string
|
||||
maxRegistrationsPerMonth: string
|
||||
nextMonthBeginDateTime: string
|
||||
nextWeekBeginDateTime: string
|
||||
}
|
||||
|
||||
export interface ImageRateLimits {
|
||||
doneImagesThisWeek: string;
|
||||
maxImagesPerWeek: string;
|
||||
nextWeekBeginDateTime: string;
|
||||
doneImagesThisWeek: string
|
||||
maxImagesPerWeek: 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
|
||||
export interface GiveSummaryRecord {
|
||||
[x: string]: PropertyKey | undefined | GiveVerifiableCredential;
|
||||
type?: string;
|
||||
agentDid: string;
|
||||
amount: number;
|
||||
amountConfirmed: number;
|
||||
description: string;
|
||||
fullClaim: GiveVerifiableCredential;
|
||||
fulfillsHandleId: string;
|
||||
fulfillsPlanHandleId?: string;
|
||||
fulfillsType?: string;
|
||||
handleId: string;
|
||||
issuedAt: string;
|
||||
issuerDid: string;
|
||||
jwtId: string;
|
||||
providerPlanHandleId?: string;
|
||||
recipientDid: string;
|
||||
unit: string;
|
||||
[x: string]: PropertyKey | undefined | GiveVerifiableCredential
|
||||
type?: string
|
||||
agentDid: string
|
||||
amount: number
|
||||
amountConfirmed: number
|
||||
description: string
|
||||
fullClaim: GiveVerifiableCredential
|
||||
fulfillsHandleId: string
|
||||
fulfillsPlanHandleId?: string
|
||||
fulfillsType?: string
|
||||
handleId: string
|
||||
issuedAt: string
|
||||
issuerDid: string
|
||||
jwtId: string
|
||||
providerPlanHandleId?: string
|
||||
recipientDid: string
|
||||
unit: string
|
||||
}
|
||||
|
||||
// a summary record; the VC is found the fullClaim field
|
||||
export interface OfferSummaryRecord {
|
||||
amount: number;
|
||||
amountGiven: number;
|
||||
amountGivenConfirmed: number;
|
||||
fullClaim: OfferVerifiableCredential;
|
||||
fulfillsPlanHandleId: string;
|
||||
handleId: string;
|
||||
issuerDid: string;
|
||||
jwtId: string;
|
||||
nonAmountGivenConfirmed: number;
|
||||
objectDescription: string;
|
||||
offeredByDid: string;
|
||||
recipientDid: string;
|
||||
requirementsMet: boolean;
|
||||
unit: string;
|
||||
validThrough: string;
|
||||
amount: number
|
||||
amountGiven: number
|
||||
amountGivenConfirmed: number
|
||||
fullClaim: OfferVerifiableCredential
|
||||
fulfillsPlanHandleId: string
|
||||
handleId: string
|
||||
issuerDid: string
|
||||
jwtId: string
|
||||
nonAmountGivenConfirmed: number
|
||||
objectDescription: string
|
||||
offeredByDid: string
|
||||
recipientDid: string
|
||||
requirementsMet: boolean
|
||||
unit: string
|
||||
validThrough: string
|
||||
}
|
||||
|
||||
export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
|
||||
planName: string;
|
||||
planName: string
|
||||
}
|
||||
|
||||
// a summary record; the VC is not currently part of this record
|
||||
export interface PlanSummaryRecord {
|
||||
agentDid?: string;
|
||||
description: string;
|
||||
endTime?: string;
|
||||
fulfillsPlanHandleId: string;
|
||||
handleId: string;
|
||||
image?: string;
|
||||
issuerDid: string;
|
||||
locLat?: number;
|
||||
locLon?: number;
|
||||
name?: string;
|
||||
startTime?: string;
|
||||
url?: string;
|
||||
jwtId?: string;
|
||||
agentDid?: string
|
||||
description: string
|
||||
endTime?: string
|
||||
fulfillsPlanHandleId: string
|
||||
handleId: string
|
||||
image?: string
|
||||
issuerDid: string
|
||||
locLat?: number
|
||||
locLon?: number
|
||||
name?: string
|
||||
startTime?: string
|
||||
url?: string
|
||||
jwtId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,23 +71,23 @@ export interface PlanData {
|
||||
/**
|
||||
* Description of the project
|
||||
**/
|
||||
description: string;
|
||||
description: string
|
||||
/**
|
||||
* URL referencing information about the project
|
||||
**/
|
||||
handleId: string;
|
||||
image?: string;
|
||||
handleId: string
|
||||
image?: string
|
||||
/**
|
||||
* The DID of the issuer
|
||||
*/
|
||||
issuerDid: string;
|
||||
issuerDid: string
|
||||
/**
|
||||
* Name of the project
|
||||
**/
|
||||
name: string;
|
||||
name: string
|
||||
/**
|
||||
* The identifier of the project record -- different from jwtId
|
||||
* (Maybe we should use the jwtId to iterate through the records instead.)
|
||||
**/
|
||||
rowId?: string;
|
||||
rowId?: string
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export interface UserInfo {
|
||||
did: string;
|
||||
name: string;
|
||||
publicEncKey: string;
|
||||
registered: boolean;
|
||||
profileImageUrl?: string;
|
||||
nextPublicEncKeyHash?: string;
|
||||
did: string
|
||||
name: string
|
||||
publicEncKey: string
|
||||
registered: boolean
|
||||
profileImageUrl?: string
|
||||
nextPublicEncKeyHash?: string
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import {
|
||||
App as CapacitorApp,
|
||||
AppLaunchUrl,
|
||||
BackButtonListener,
|
||||
} from "../../../node_modules/@capacitor/app";
|
||||
import type { PluginListenerHandle } from "@capacitor/core";
|
||||
BackButtonListener
|
||||
} from '../../../node_modules/@capacitor/app'
|
||||
import type { PluginListenerHandle } from '@capacitor/core'
|
||||
|
||||
/**
|
||||
* Interface defining the app event listener functionality
|
||||
@@ -19,9 +19,9 @@ interface AppInterface {
|
||||
* @returns Promise that resolves with a removable listener handle
|
||||
*/
|
||||
addListener(
|
||||
eventName: "backButton",
|
||||
listenerFunc: BackButtonListener,
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
eventName: 'backButton',
|
||||
listenerFunc: BackButtonListener
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle
|
||||
|
||||
/**
|
||||
* Add listener for app URL open events
|
||||
@@ -30,9 +30,9 @@ interface AppInterface {
|
||||
* @returns Promise that resolves with a removable listener handle
|
||||
*/
|
||||
addListener(
|
||||
eventName: "appUrlOpen",
|
||||
listenerFunc: (data: AppLaunchUrl) => void,
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
eventName: 'appUrlOpen',
|
||||
listenerFunc: (data: AppLaunchUrl) => void
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,19 +41,19 @@ interface AppInterface {
|
||||
*/
|
||||
export const App: AppInterface = {
|
||||
addListener(
|
||||
eventName: "backButton" | "appUrlOpen",
|
||||
listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void),
|
||||
eventName: 'backButton' | 'appUrlOpen',
|
||||
listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void)
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle {
|
||||
if (eventName === "backButton") {
|
||||
if (eventName === 'backButton') {
|
||||
return CapacitorApp.addListener(
|
||||
eventName,
|
||||
listenerFunc as BackButtonListener,
|
||||
) as Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
listenerFunc as BackButtonListener
|
||||
) as Promise<PluginListenerHandle> & PluginListenerHandle
|
||||
} else {
|
||||
return CapacitorApp.addListener(
|
||||
eventName,
|
||||
listenerFunc as (data: AppLaunchUrl) => void,
|
||||
) as Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
listenerFunc as (data: AppLaunchUrl) => void
|
||||
) as Promise<PluginListenerHandle> & PluginListenerHandle
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowLeft,
|
||||
@@ -81,8 +81,8 @@ import {
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUsers,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
faXmark
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
// Initialize Font Awesome library with all required icons
|
||||
library.add(
|
||||
@@ -161,8 +161,8 @@ library.add(
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUsers,
|
||||
faXmark,
|
||||
);
|
||||
faXmark
|
||||
)
|
||||
|
||||
// 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 { getRandomBytesSync } from "ethereum-cryptography/random";
|
||||
import { entropyToMnemonic } from "ethereum-cryptography/bip39";
|
||||
import { wordlist } from "ethereum-cryptography/bip39/wordlists/english";
|
||||
import { HDNode } from "@ethersproject/hdnode";
|
||||
import { IIdentifier } from '@veramo/core'
|
||||
import { getRandomBytesSync } from 'ethereum-cryptography/random'
|
||||
import { entropyToMnemonic } from 'ethereum-cryptography/bip39'
|
||||
import { wordlist } from 'ethereum-cryptography/bip39/wordlists/english'
|
||||
import { HDNode } from '@ethersproject/hdnode'
|
||||
|
||||
import {
|
||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||
createEndorserJwtForDid,
|
||||
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
||||
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
||||
} from "../../libs/endorserServer";
|
||||
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
||||
import { logger } from "../../utils/logger";
|
||||
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI
|
||||
} from '../../libs/endorserServer'
|
||||
import { DEFAULT_DID_PROVIDER_NAME } from '../veramo/setup'
|
||||
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,
|
||||
publicHex: string,
|
||||
privateHex: string,
|
||||
derivationPath: string,
|
||||
): Omit<IIdentifier, keyof "provider"> => {
|
||||
derivationPath: string
|
||||
): Omit<IIdentifier, keyof 'provider'> => {
|
||||
return {
|
||||
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
|
||||
did: DEFAULT_DID_PROVIDER_NAME + ':' + address,
|
||||
keys: [
|
||||
{
|
||||
kid: publicHex,
|
||||
@@ -41,13 +41,13 @@ export const newIdentifier = (
|
||||
meta: { derivationPath: derivationPath },
|
||||
privateKeyHex: privateHex,
|
||||
publicKeyHex: publicHex,
|
||||
type: "Secp256k1",
|
||||
},
|
||||
type: 'Secp256k1'
|
||||
}
|
||||
],
|
||||
provider: DEFAULT_DID_PROVIDER_NAME,
|
||||
services: [],
|
||||
};
|
||||
};
|
||||
services: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -57,22 +57,22 @@ export const newIdentifier = (
|
||||
*/
|
||||
export const deriveAddress = (
|
||||
mnemonic: string,
|
||||
derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH,
|
||||
derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH
|
||||
): [string, string, string, string] => {
|
||||
mnemonic = mnemonic.trim().toLowerCase();
|
||||
mnemonic = mnemonic.trim().toLowerCase()
|
||||
|
||||
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
|
||||
const rootNode: HDNode = hdnode.derivePath(derivationPath);
|
||||
const privateHex = rootNode.privateKey.substring(2); // original starts with '0x'
|
||||
const publicHex = rootNode.publicKey.substring(2); // original starts with '0x'
|
||||
const address = rootNode.address;
|
||||
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic)
|
||||
const rootNode: HDNode = hdnode.derivePath(derivationPath)
|
||||
const privateHex = rootNode.privateKey.substring(2) // original starts with '0x'
|
||||
const publicHex = rootNode.publicKey.substring(2) // original starts with '0x'
|
||||
const address = rootNode.address
|
||||
|
||||
return [address, privateHex, publicHex, derivationPath];
|
||||
};
|
||||
return [address, privateHex, publicHex, derivationPath]
|
||||
}
|
||||
|
||||
export const generateRandomBytes = (numBytes: number): Uint8Array => {
|
||||
return getRandomBytesSync(numBytes);
|
||||
};
|
||||
return getRandomBytesSync(numBytes)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -80,11 +80,11 @@ export const generateRandomBytes = (numBytes: number): Uint8Array => {
|
||||
* @return {*} {string}
|
||||
*/
|
||||
export const generateSeed = (): string => {
|
||||
const entropy: Uint8Array = getRandomBytesSync(32);
|
||||
const mnemonic = entropyToMnemonic(entropy, wordlist);
|
||||
const entropy: Uint8Array = getRandomBytesSync(32)
|
||||
const mnemonic = entropyToMnemonic(entropy, wordlist)
|
||||
|
||||
return mnemonic;
|
||||
};
|
||||
return mnemonic
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an access token, or "" if no DID is provided.
|
||||
@@ -94,14 +94,14 @@ export const generateSeed = (): string => {
|
||||
*/
|
||||
export const accessToken = async (did?: string) => {
|
||||
if (did) {
|
||||
const nowEpoch = Math.floor(Date.now() / 1000);
|
||||
const endEpoch = nowEpoch + 60; // add one minute
|
||||
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did };
|
||||
return createEndorserJwtForDid(did, tokenPayload);
|
||||
const nowEpoch = Math.floor(Date.now() / 1000)
|
||||
const endEpoch = nowEpoch + 60 // add one minute
|
||||
const tokenPayload = { exp: endEpoch, iat: nowEpoch, iss: did }
|
||||
return createEndorserJwtForDid(did, tokenPayload)
|
||||
} else {
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract JWT from various URL formats
|
||||
@@ -109,204 +109,204 @@ export const accessToken = async (did?: string) => {
|
||||
* @return JWT string if found, original text otherwise
|
||||
*/
|
||||
export const getContactJwtFromJwtUrl = (jwtUrlText: string) => {
|
||||
let jwtText = jwtUrlText;
|
||||
let jwtText = jwtUrlText
|
||||
|
||||
// Handle various URL patterns
|
||||
const URL_PATTERNS = [
|
||||
"/contact/confirm/",
|
||||
'/contact/confirm/',
|
||||
CONTACT_IMPORT_CONFIRM_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
|
||||
for (const pattern of URL_PATTERNS) {
|
||||
const patternIndex = jwtText.indexOf(pattern);
|
||||
const patternIndex = jwtText.indexOf(pattern)
|
||||
if (patternIndex > -1) {
|
||||
jwtText = jwtText.substring(patternIndex + pattern.length);
|
||||
break;
|
||||
jwtText = jwtText.substring(patternIndex + pattern.length)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no patterns matched but text starts with 'ey', assume it's already a JWT
|
||||
if (jwtText === jwtUrlText && jwtText.startsWith("ey")) {
|
||||
return jwtText;
|
||||
if (jwtText === jwtUrlText && jwtText.startsWith('ey')) {
|
||||
return jwtText
|
||||
}
|
||||
|
||||
// Clean up any trailing URL parameters or fragments
|
||||
const endIndex = jwtText.indexOf("?");
|
||||
const endIndex = jwtText.indexOf('?')
|
||||
if (endIndex > -1) {
|
||||
jwtText = jwtText.substring(0, endIndex);
|
||||
jwtText = jwtText.substring(0, endIndex)
|
||||
}
|
||||
|
||||
return jwtText;
|
||||
};
|
||||
return jwtText
|
||||
}
|
||||
|
||||
export const nextDerivationPath = (origDerivPath: string) => {
|
||||
let lastStr = origDerivPath.split("/").slice(-1)[0];
|
||||
let lastStr = origDerivPath.split('/').slice(-1)[0]
|
||||
if (lastStr.endsWith("'")) {
|
||||
lastStr = lastStr.slice(0, -1);
|
||||
lastStr = lastStr.slice(0, -1)
|
||||
}
|
||||
const lastNum = parseInt(lastStr, 10);
|
||||
const newLastNum = lastNum + 1;
|
||||
const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : "");
|
||||
const lastNum = parseInt(lastStr, 10)
|
||||
const newLastNum = lastNum + 1
|
||||
const newLastStr = newLastNum.toString() + (lastStr.endsWith("'") ? "'" : '')
|
||||
const newDerivPath = origDerivPath
|
||||
.split("/")
|
||||
.split('/')
|
||||
.slice(0, -1)
|
||||
.concat([newLastStr])
|
||||
.join("/");
|
||||
return newDerivPath;
|
||||
};
|
||||
.join('/')
|
||||
return newDerivPath
|
||||
}
|
||||
|
||||
// Base64 encoding/decoding utilities for browser
|
||||
function base64ToArrayBuffer(base64: string): Uint8Array {
|
||||
const binaryString = atob(base64);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
const binaryString = atob(base64)
|
||||
const bytes = new Uint8Array(binaryString.length)
|
||||
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 {
|
||||
const binary = String.fromCharCode(...new Uint8Array(buffer));
|
||||
return btoa(binary);
|
||||
const binary = String.fromCharCode(...new Uint8Array(buffer))
|
||||
return btoa(binary)
|
||||
}
|
||||
|
||||
const SALT_LENGTH = 16;
|
||||
const IV_LENGTH = 12;
|
||||
const KEY_LENGTH = 256;
|
||||
const ITERATIONS = 100000;
|
||||
const SALT_LENGTH = 16
|
||||
const IV_LENGTH = 12
|
||||
const KEY_LENGTH = 256
|
||||
const ITERATIONS = 100000
|
||||
|
||||
// Encryption helper function
|
||||
export async function encryptMessage(message: string, password: string) {
|
||||
const encoder = new TextEncoder();
|
||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
||||
const encoder = new TextEncoder()
|
||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH))
|
||||
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))
|
||||
|
||||
// Derive key from password using PBKDF2
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
'raw',
|
||||
encoder.encode(password),
|
||||
"PBKDF2",
|
||||
'PBKDF2',
|
||||
false,
|
||||
["deriveBits", "deriveKey"],
|
||||
);
|
||||
['deriveBits', 'deriveKey']
|
||||
)
|
||||
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
name: 'PBKDF2',
|
||||
salt,
|
||||
iterations: ITERATIONS,
|
||||
hash: "SHA-256",
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: "AES-GCM", length: KEY_LENGTH },
|
||||
{ name: 'AES-GCM', length: KEY_LENGTH },
|
||||
false,
|
||||
["encrypt"],
|
||||
);
|
||||
['encrypt']
|
||||
)
|
||||
|
||||
// Encrypt the message
|
||||
const encryptedContent = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv,
|
||||
name: 'AES-GCM',
|
||||
iv
|
||||
},
|
||||
key,
|
||||
encoder.encode(message),
|
||||
);
|
||||
encoder.encode(message)
|
||||
)
|
||||
|
||||
// Return a JSON structure with base64-encoded components
|
||||
const result = {
|
||||
salt: arrayBufferToBase64(salt),
|
||||
iv: arrayBufferToBase64(iv),
|
||||
encrypted: arrayBufferToBase64(encryptedContent),
|
||||
};
|
||||
encrypted: arrayBufferToBase64(encryptedContent)
|
||||
}
|
||||
|
||||
return btoa(JSON.stringify(result));
|
||||
return btoa(JSON.stringify(result))
|
||||
}
|
||||
|
||||
// Decryption helper function
|
||||
export async function decryptMessage(encryptedJson: string, password: string) {
|
||||
const decoder = new TextDecoder();
|
||||
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));
|
||||
const decoder = new TextDecoder()
|
||||
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson))
|
||||
|
||||
// Convert base64 components back to Uint8Arrays
|
||||
const saltArray = base64ToArrayBuffer(salt);
|
||||
const ivArray = base64ToArrayBuffer(iv);
|
||||
const encryptedContent = base64ToArrayBuffer(encrypted);
|
||||
const saltArray = base64ToArrayBuffer(salt)
|
||||
const ivArray = base64ToArrayBuffer(iv)
|
||||
const encryptedContent = base64ToArrayBuffer(encrypted)
|
||||
|
||||
// Derive the same key using PBKDF2 with the extracted salt
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
'raw',
|
||||
new TextEncoder().encode(password),
|
||||
"PBKDF2",
|
||||
'PBKDF2',
|
||||
false,
|
||||
["deriveBits", "deriveKey"],
|
||||
);
|
||||
['deriveBits', 'deriveKey']
|
||||
)
|
||||
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: "PBKDF2",
|
||||
name: 'PBKDF2',
|
||||
salt: saltArray,
|
||||
iterations: ITERATIONS,
|
||||
hash: "SHA-256",
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
keyMaterial,
|
||||
{ name: "AES-GCM", length: KEY_LENGTH },
|
||||
{ name: 'AES-GCM', length: KEY_LENGTH },
|
||||
false,
|
||||
["decrypt"],
|
||||
);
|
||||
['decrypt']
|
||||
)
|
||||
|
||||
// Decrypt the content
|
||||
const decryptedContent = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: "AES-GCM",
|
||||
iv: ivArray,
|
||||
name: 'AES-GCM',
|
||||
iv: ivArray
|
||||
},
|
||||
key,
|
||||
encryptedContent,
|
||||
);
|
||||
encryptedContent
|
||||
)
|
||||
|
||||
// Convert the decrypted content back to a string
|
||||
return decoder.decode(decryptedContent);
|
||||
return decoder.decode(decryptedContent)
|
||||
}
|
||||
|
||||
// Test function to verify encryption/decryption
|
||||
export async function testEncryptionDecryption() {
|
||||
try {
|
||||
const testMessage = "Hello, this is a test message! 🚀";
|
||||
const testPassword = "myTestPassword123";
|
||||
const testMessage = 'Hello, this is a test message! 🚀'
|
||||
const testPassword = 'myTestPassword123'
|
||||
|
||||
logger.log("Original message:", testMessage);
|
||||
logger.log('Original message:', testMessage)
|
||||
|
||||
// Test encryption
|
||||
logger.log("Encrypting...");
|
||||
const encrypted = await encryptMessage(testMessage, testPassword);
|
||||
logger.log("Encrypted result:", encrypted);
|
||||
logger.log('Encrypting...')
|
||||
const encrypted = await encryptMessage(testMessage, testPassword)
|
||||
logger.log('Encrypted result:', encrypted)
|
||||
|
||||
// Test decryption
|
||||
logger.log("Decrypting...");
|
||||
const decrypted = await decryptMessage(encrypted, testPassword);
|
||||
logger.log("Decrypted result:", decrypted);
|
||||
logger.log('Decrypting...')
|
||||
const decrypted = await decryptMessage(encrypted, testPassword)
|
||||
logger.log('Decrypted result:', decrypted)
|
||||
|
||||
// Verify
|
||||
const success = testMessage === decrypted;
|
||||
logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
||||
logger.log("Messages match:", success);
|
||||
const success = testMessage === decrypted
|
||||
logger.log('Test ' + (success ? 'PASSED ✅' : 'FAILED ❌'))
|
||||
logger.log('Messages match:', success)
|
||||
|
||||
// Test with wrong password
|
||||
logger.log("\nTesting with wrong password...");
|
||||
logger.log('\nTesting with wrong password...')
|
||||
try {
|
||||
await decryptMessage(encrypted, "wrongPassword");
|
||||
logger.log("Should not reach here");
|
||||
await decryptMessage(encrypted, 'wrongPassword')
|
||||
logger.log('Should not reach here')
|
||||
} catch (error) {
|
||||
logger.log("Correctly failed with wrong password ✅");
|
||||
logger.log('Correctly failed with wrong password ✅')
|
||||
}
|
||||
|
||||
return success;
|
||||
return success
|
||||
} catch (error) {
|
||||
logger.error("Test failed with error:", error);
|
||||
return false;
|
||||
logger.error('Test failed with error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,37 +10,37 @@
|
||||
* Similar code resides in endorser-ch and image-api
|
||||
*/
|
||||
export const didEthLocalResolver = async (did: string) => {
|
||||
const didRegex = /^did:ethr:(0x[0-9a-fA-F]{40})$/;
|
||||
const match = did.match(didRegex);
|
||||
const didRegex = /^did:ethr:(0x[0-9a-fA-F]{40})$/
|
||||
const match = did.match(didRegex)
|
||||
|
||||
if (match) {
|
||||
const address = match[1]; // Extract eth address: 0x...
|
||||
const publicKeyHex = address; // Use the address directly as a public key placeholder
|
||||
const address = match[1] // Extract eth address: 0x...
|
||||
const publicKeyHex = address // Use the address directly as a public key placeholder
|
||||
|
||||
return {
|
||||
didDocumentMetadata: {},
|
||||
didResolutionMetadata: {
|
||||
contentType: "application/did+ld+json",
|
||||
contentType: 'application/did+ld+json'
|
||||
},
|
||||
didDocument: {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/did/v1",
|
||||
"https://w3id.org/security/suites/secp256k1recovery-2020/v2",
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/did/v1',
|
||||
'https://w3id.org/security/suites/secp256k1recovery-2020/v2'
|
||||
],
|
||||
id: did,
|
||||
verificationMethod: [
|
||||
{
|
||||
id: `${did}#controller`,
|
||||
type: "EcdsaSecp256k1RecoveryMethod2020",
|
||||
type: 'EcdsaSecp256k1RecoveryMethod2020',
|
||||
controller: did,
|
||||
blockchainAccountId: "eip155:1:" + publicKeyHex,
|
||||
},
|
||||
blockchainAccountId: 'eip155:1:' + publicKeyHex
|
||||
}
|
||||
],
|
||||
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 { decode as cborDecode } from "cbor-x";
|
||||
import { bytesToMultibase, multibaseToBytes } from "did-jwt";
|
||||
import { Buffer } from 'buffer/'
|
||||
import { decode as cborDecode } from 'cbor-x'
|
||||
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:";
|
||||
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0";
|
||||
export const PEER_DID_PREFIX = 'did:peer:'
|
||||
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(
|
||||
payloadBytes: Buffer,
|
||||
issuerDid: string,
|
||||
signatureBytes: Uint8Array,
|
||||
signatureBytes: Uint8Array
|
||||
): Promise<boolean> {
|
||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid)
|
||||
|
||||
const WebCrypto = await getWebCrypto();
|
||||
const WebCrypto = await getWebCrypto()
|
||||
const verifyAlgorithm = {
|
||||
name: "ECDSA",
|
||||
hash: { name: "SHA-256" },
|
||||
};
|
||||
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk;
|
||||
name: 'ECDSA',
|
||||
hash: { name: 'SHA-256' }
|
||||
}
|
||||
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk
|
||||
const keyAlgorithm = {
|
||||
name: "ECDSA",
|
||||
namedCurve: publicKeyJwk.crv,
|
||||
};
|
||||
name: 'ECDSA',
|
||||
namedCurve: publicKeyJwk.crv
|
||||
}
|
||||
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
|
||||
"jwk",
|
||||
'jwk',
|
||||
publicKeyJwk,
|
||||
keyAlgorithm,
|
||||
false,
|
||||
["verify"],
|
||||
);
|
||||
['verify']
|
||||
)
|
||||
const verified = await WebCrypto.subtle.verify(
|
||||
verifyAlgorithm,
|
||||
publicKeyCryptoKey,
|
||||
signatureBytes,
|
||||
payloadBytes,
|
||||
);
|
||||
return verified;
|
||||
payloadBytes
|
||||
)
|
||||
return verified
|
||||
}
|
||||
|
||||
export function cborToKeys(publicKeyBytes: Uint8Array) {
|
||||
const jwkObj = cborDecode(publicKeyBytes);
|
||||
const jwkObj = cborDecode(publicKeyBytes)
|
||||
if (
|
||||
jwkObj[1] != 2 || // kty "EC"
|
||||
jwkObj[3] != -7 || // alg "ES256"
|
||||
@@ -56,32 +56,32 @@ export function cborToKeys(publicKeyBytes: Uint8Array) {
|
||||
jwkObj[-2].length != 32 || // x
|
||||
jwkObj[-3].length != 32 // y
|
||||
) {
|
||||
throw new Error("Unable to extract key.");
|
||||
throw new Error('Unable to extract key.')
|
||||
}
|
||||
const publicKeyJwk = {
|
||||
alg: "ES256",
|
||||
crv: "P-256",
|
||||
kty: "EC",
|
||||
alg: 'ES256',
|
||||
crv: 'P-256',
|
||||
kty: 'EC',
|
||||
x: arrayToBase64Url(jwkObj[-2]),
|
||||
y: arrayToBase64Url(jwkObj[-3]),
|
||||
};
|
||||
y: arrayToBase64Url(jwkObj[-3])
|
||||
}
|
||||
const publicKeyBuffer = Buffer.concat([
|
||||
Buffer.from(jwkObj[-2]),
|
||||
Buffer.from(jwkObj[-3]),
|
||||
]);
|
||||
return { publicKeyJwk, publicKeyBuffer };
|
||||
Buffer.from(jwkObj[-3])
|
||||
])
|
||||
return { publicKeyJwk, publicKeyBuffer }
|
||||
}
|
||||
|
||||
export function toBase64Url(anythingB64: string) {
|
||||
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
return anythingB64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
}
|
||||
|
||||
export function arrayToBase64Url(anything: Uint8Array) {
|
||||
return toBase64Url(Buffer.from(anything).toString("base64"));
|
||||
return toBase64Url(Buffer.from(anything).toString('base64'))
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -89,8 +89,8 @@ export function createPeerDid(publicKeyBytes: Uint8Array) {
|
||||
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
|
||||
const methodSpecificId = bytesToMultibase(
|
||||
publicKeyBytes,
|
||||
"base58btc",
|
||||
"p256-pub",
|
||||
);
|
||||
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId;
|
||||
'base58btc',
|
||||
'p256-pub'
|
||||
)
|
||||
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId
|
||||
}
|
||||
|
||||
@@ -6,21 +6,21 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { Buffer } from "buffer/";
|
||||
import * as didJwt from "did-jwt";
|
||||
import { JWTVerified } from "did-jwt";
|
||||
import { Resolver } from "did-resolver";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import * as u8a from "uint8arrays";
|
||||
import { Buffer } from 'buffer/'
|
||||
import * as didJwt from 'did-jwt'
|
||||
import { JWTVerified } from 'did-jwt'
|
||||
import { Resolver } from 'did-resolver'
|
||||
import { IIdentifier } from '@veramo/core'
|
||||
import * as u8a from 'uint8arrays'
|
||||
|
||||
import { didEthLocalResolver } from "./did-eth-local-resolver";
|
||||
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
|
||||
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
|
||||
import { urlBase64ToUint8Array } from "./util";
|
||||
import { didEthLocalResolver } from './did-eth-local-resolver'
|
||||
import { PEER_DID_PREFIX, verifyPeerSignature } from './didPeer'
|
||||
import { base64urlDecodeString, createDidPeerJwt } from './passkeyDidPeer'
|
||||
import { urlBase64ToUint8Array } from './util'
|
||||
|
||||
export const ETHR_DID_PREFIX = "did:ethr:";
|
||||
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
|
||||
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
|
||||
export const ETHR_DID_PREFIX = 'did:ethr:'
|
||||
export const JWT_VERIFY_FAILED_CODE = 'JWT_VERIFY_FAILED'
|
||||
export const UNSUPPORTED_DID_METHOD_CODE = 'UNSUPPORTED_DID_METHOD'
|
||||
|
||||
/**
|
||||
* Meta info about a key
|
||||
@@ -29,51 +29,51 @@ export interface KeyMeta {
|
||||
/**
|
||||
* Decentralized ID for the key
|
||||
*/
|
||||
did: string;
|
||||
did: string
|
||||
/**
|
||||
* Stringified IIDentifier object from Veramo
|
||||
*/
|
||||
identity?: string;
|
||||
identity?: string
|
||||
/**
|
||||
* 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
|
||||
* @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey
|
||||
*/
|
||||
export function isFromPasskey(keyMeta?: KeyMeta): boolean {
|
||||
return !!keyMeta?.passkeyCredIdHex;
|
||||
return !!keyMeta?.passkeyCredIdHex
|
||||
}
|
||||
|
||||
export async function createEndorserJwtForKey(
|
||||
account: KeyMeta,
|
||||
payload: object,
|
||||
expiresIn?: number,
|
||||
expiresIn?: number
|
||||
) {
|
||||
if (account?.identity) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const identity: IIdentifier = JSON.parse(account.identity!);
|
||||
const privateKeyHex = identity.keys[0].privateKeyHex;
|
||||
const signer = await SimpleSigner(privateKeyHex as string);
|
||||
const identity: IIdentifier = JSON.parse(account.identity!)
|
||||
const privateKeyHex = identity.keys[0].privateKeyHex
|
||||
const signer = await SimpleSigner(privateKeyHex as string)
|
||||
const options = {
|
||||
// alg: "ES256K", // "K" is the default, "K-R" is used by the server in tests
|
||||
issuer: account.did,
|
||||
signer: signer,
|
||||
expiresIn: undefined as number | undefined,
|
||||
};
|
||||
if (expiresIn) {
|
||||
options.expiresIn = expiresIn;
|
||||
expiresIn: undefined as number | undefined
|
||||
}
|
||||
return didJwt.createJWT(payload, options);
|
||||
if (expiresIn) {
|
||||
options.expiresIn = expiresIn
|
||||
}
|
||||
return didJwt.createJWT(payload, options)
|
||||
} else if (account?.passkeyCredIdHex) {
|
||||
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload);
|
||||
return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload)
|
||||
} 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
|
||||
*/
|
||||
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) => {
|
||||
const signature = (await signer(data)) as string;
|
||||
return fromJose(signature);
|
||||
};
|
||||
const signature = (await signer(data)) as string
|
||||
return fromJose(signature)
|
||||
}
|
||||
}
|
||||
|
||||
// from did-jwt/util; see SimpleSigner above
|
||||
function fromJose(signature: string): {
|
||||
r: string;
|
||||
s: string;
|
||||
recoveryParam?: number;
|
||||
r: string
|
||||
s: string
|
||||
recoveryParam?: number
|
||||
} {
|
||||
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature);
|
||||
const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature)
|
||||
if (signatureBytes.length < 64 || signatureBytes.length > 65) {
|
||||
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 s = bytesToHex(signatureBytes.slice(32, 64));
|
||||
const r = bytesToHex(signatureBytes.slice(0, 32))
|
||||
const s = bytesToHex(signatureBytes.slice(32, 64))
|
||||
const recoveryParam =
|
||||
signatureBytes.length === 65 ? signatureBytes[64] : undefined;
|
||||
return { r, s, recoveryParam };
|
||||
signatureBytes.length === 65 ? signatureBytes[64] : undefined
|
||||
return { r, s, recoveryParam }
|
||||
}
|
||||
|
||||
// from did-jwt/util; see SimpleSigner above
|
||||
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.
|
||||
@@ -128,21 +128,21 @@ function bytesToHex(b: Uint8Array): string {
|
||||
export function decodeEndorserJwt(jwt: string) {
|
||||
try {
|
||||
// First try the standard did-jwt decode
|
||||
return didJwt.decodeJWT(jwt);
|
||||
return didJwt.decodeJWT(jwt)
|
||||
} catch (error) {
|
||||
// If that fails, try manual decoding
|
||||
try {
|
||||
const parts = jwt.split(".");
|
||||
const parts = jwt.split('.')
|
||||
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 payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
||||
const header = JSON.parse(Buffer.from(parts[0], 'base64url').toString())
|
||||
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString())
|
||||
|
||||
// Validate the header
|
||||
if (header.typ !== "JWT" || !header.alg) {
|
||||
throw new Error("Invalid JWT header format");
|
||||
if (header.typ !== 'JWT' || !header.alg) {
|
||||
throw new Error('Invalid JWT header format')
|
||||
}
|
||||
|
||||
// Return in the same format as didJwt.decodeJWT
|
||||
@@ -150,10 +150,10 @@ export function decodeEndorserJwt(jwt: string) {
|
||||
header,
|
||||
payload,
|
||||
signature: parts[2],
|
||||
data: parts[0] + "." + parts[1],
|
||||
};
|
||||
data: parts[0] + '.' + parts[1]
|
||||
}
|
||||
} 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 }
|
||||
// ... and also if successfully verified by did-jwt (not JWANT): data, doc, signature, signer
|
||||
export async function decodeAndVerifyJwt(
|
||||
jwt: string,
|
||||
): Promise<Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt">> {
|
||||
const pieces = jwt.split(".");
|
||||
const header = JSON.parse(base64urlDecodeString(pieces[0]));
|
||||
const payload = JSON.parse(base64urlDecodeString(pieces[1]));
|
||||
const issuerDid = payload.iss;
|
||||
jwt: string
|
||||
): Promise<Omit<JWTVerified, 'didResolutionResult' | 'signer' | 'jwt'>> {
|
||||
const pieces = jwt.split('.')
|
||||
const header = JSON.parse(base64urlDecodeString(pieces[0]))
|
||||
const payload = JSON.parse(base64urlDecodeString(pieces[1]))
|
||||
const issuerDid = payload.iss
|
||||
if (!issuerDid) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
message: `Missing "iss" field in JWT.`,
|
||||
},
|
||||
});
|
||||
message: `Missing "iss" field in JWT.`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (issuerDid.startsWith(ETHR_DID_PREFIX)) {
|
||||
try {
|
||||
const verified = await didJwt.verifyJWT(jwt, {
|
||||
resolver: ethLocalResolver,
|
||||
});
|
||||
return verified;
|
||||
resolver: ethLocalResolver
|
||||
})
|
||||
return verified
|
||||
} catch (e: unknown) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
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(
|
||||
Buffer.from(payload),
|
||||
issuerDid,
|
||||
urlBase64ToUint8Array(pieces[2]),
|
||||
);
|
||||
urlBase64ToUint8Array(pieces[2])
|
||||
)
|
||||
if (!verified) {
|
||||
return Promise.reject({
|
||||
clientError: {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
message: `JWT failed verification: ` + e.toString(),
|
||||
code: JWT_VERIFY_FAILED_CODE,
|
||||
},
|
||||
});
|
||||
code: JWT_VERIFY_FAILED_CODE
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return { issuer: issuerDid, payload: payload, verified: true };
|
||||
return { issuer: issuerDid, payload: payload, verified: true }
|
||||
}
|
||||
}
|
||||
|
||||
if (issuerDid.startsWith(PEER_DID_PREFIX)) {
|
||||
return Promise.reject({
|
||||
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({
|
||||
clientError: {
|
||||
message: `Unsupported DID method ${issuerDid}`,
|
||||
code: UNSUPPORTED_DID_METHOD_CODE,
|
||||
},
|
||||
});
|
||||
code: UNSUPPORTED_DID_METHOD_CODE
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,39 +1,36 @@
|
||||
import { Buffer } from "buffer/";
|
||||
import { JWTPayload } from "did-jwt";
|
||||
import { DIDResolutionResult } from "did-resolver";
|
||||
import { sha256 } from "ethereum-cryptography/sha256.js";
|
||||
import {
|
||||
startAuthentication,
|
||||
startRegistration,
|
||||
} from "@simplewebauthn/browser";
|
||||
import { Buffer } from 'buffer/'
|
||||
import { JWTPayload } from 'did-jwt'
|
||||
import { DIDResolutionResult } from 'did-resolver'
|
||||
import { sha256 } from 'ethereum-cryptography/sha256.js'
|
||||
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
|
||||
import {
|
||||
generateAuthenticationOptions,
|
||||
generateRegistrationOptions,
|
||||
verifyAuthenticationResponse,
|
||||
verifyRegistrationResponse,
|
||||
} from "@simplewebauthn/server";
|
||||
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse";
|
||||
verifyRegistrationResponse
|
||||
} from '@simplewebauthn/server'
|
||||
import { VerifyAuthenticationResponseOpts } from '@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse'
|
||||
import {
|
||||
Base64URLString,
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
} from "@simplewebauthn/types";
|
||||
PublicKeyCredentialRequestOptionsJSON
|
||||
} from '@simplewebauthn/types'
|
||||
|
||||
import { AppString } from "../../../constants/app";
|
||||
import { unwrapEC2Signature } from "../../../libs/crypto/vc/passkeyHelpers";
|
||||
import { AppString } from '../../../constants/app'
|
||||
import { unwrapEC2Signature } from '../../../libs/crypto/vc/passkeyHelpers'
|
||||
import {
|
||||
arrayToBase64Url,
|
||||
cborToKeys,
|
||||
peerDidToPublicKeyBytes,
|
||||
verifyPeerSignature,
|
||||
} from "../../../libs/crypto/vc/didPeer";
|
||||
import { logger } from "../../../utils/logger";
|
||||
verifyPeerSignature
|
||||
} from '../../../libs/crypto/vc/didPeer'
|
||||
import { logger } from '../../../utils/logger'
|
||||
|
||||
export interface JWK {
|
||||
kty: string;
|
||||
crv: string;
|
||||
x: string;
|
||||
y: string;
|
||||
kty: string
|
||||
crv: string
|
||||
x: string
|
||||
y: string
|
||||
}
|
||||
|
||||
export async function registerCredential(passkeyName?: string) {
|
||||
@@ -41,203 +38,203 @@ export async function registerCredential(passkeyName?: string) {
|
||||
await generateRegistrationOptions({
|
||||
rpName: AppString.APP_NAME,
|
||||
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
|
||||
// (Recommended for smoother UX)
|
||||
attestationType: "none",
|
||||
attestationType: 'none',
|
||||
authenticatorSelection: {
|
||||
// Defaults
|
||||
residentKey: "preferred",
|
||||
userVerification: "preferred",
|
||||
residentKey: 'preferred',
|
||||
userVerification: 'preferred',
|
||||
// Optional
|
||||
authenticatorAttachment: "platform",
|
||||
},
|
||||
});
|
||||
authenticatorAttachment: 'platform'
|
||||
}
|
||||
})
|
||||
// someday, instead of simplwebauthn, we'll go direct: navigator.credentials.create with PublicKeyCredentialCreationOptions
|
||||
// with pubKeyCredParams: { type: "public-key", alg: -7 }
|
||||
const attResp = await startRegistration(options);
|
||||
const attResp = await startRegistration(options)
|
||||
const verification = await verifyRegistrationResponse({
|
||||
response: attResp,
|
||||
expectedChallenge: options.challenge,
|
||||
expectedOrigin: window.location.origin,
|
||||
expectedRPID: window.location.hostname,
|
||||
});
|
||||
expectedRPID: window.location.hostname
|
||||
})
|
||||
|
||||
// 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://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
|
||||
// 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) {
|
||||
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(
|
||||
base64URLStringToArrayBuffer(credIdBase64Url),
|
||||
).toString("hex");
|
||||
base64URLStringToArrayBuffer(credIdBase64Url)
|
||||
).toString('hex')
|
||||
const { publicKeyJwk } = cborToKeys(
|
||||
verification.registrationInfo?.credentialPublicKey as Uint8Array,
|
||||
);
|
||||
verification.registrationInfo?.credentialPublicKey as Uint8Array
|
||||
)
|
||||
|
||||
return {
|
||||
authData: verification.registrationInfo?.attestationObject,
|
||||
credIdHex: credIdHex,
|
||||
publicKeyJwk: publicKeyJwk,
|
||||
publicKeyBytes: verification.registrationInfo
|
||||
?.credentialPublicKey as Uint8Array,
|
||||
};
|
||||
?.credentialPublicKey as Uint8Array
|
||||
}
|
||||
}
|
||||
|
||||
export class PeerSetup {
|
||||
public authenticatorData?: ArrayBuffer;
|
||||
public challenge?: Uint8Array;
|
||||
public clientDataJsonBase64Url?: Base64URLString;
|
||||
public signature?: Base64URLString;
|
||||
public authenticatorData?: ArrayBuffer
|
||||
public challenge?: Uint8Array
|
||||
public clientDataJsonBase64Url?: Base64URLString
|
||||
public signature?: Base64URLString
|
||||
|
||||
public async createJwtSimplewebauthn(
|
||||
issuerDid: string,
|
||||
payload: object,
|
||||
credIdHex: string,
|
||||
expMinutes: number = 1,
|
||||
expMinutes: number = 1
|
||||
) {
|
||||
const credentialId = arrayBufferToBase64URLString(
|
||||
Buffer.from(credIdHex, "hex").buffer,
|
||||
);
|
||||
const issuedAt = Math.floor(Date.now() / 1000);
|
||||
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
|
||||
Buffer.from(credIdHex, 'hex').buffer
|
||||
)
|
||||
const issuedAt = Math.floor(Date.now() / 1000)
|
||||
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60 // some minutes from now
|
||||
const fullPayload = {
|
||||
...payload,
|
||||
exp: expiryTime,
|
||||
iat: issuedAt,
|
||||
iss: issuerDid,
|
||||
};
|
||||
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
|
||||
iss: issuerDid
|
||||
}
|
||||
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)))
|
||||
// const payloadHash: Uint8Array = sha256(this.challenge);
|
||||
const options: PublicKeyCredentialRequestOptionsJSON =
|
||||
await generateAuthenticationOptions({
|
||||
challenge: this.challenge,
|
||||
rpID: window.location.hostname,
|
||||
allowCredentials: [{ id: credentialId }],
|
||||
});
|
||||
allowCredentials: [{ id: credentialId }]
|
||||
})
|
||||
// console.log("simple authentication options", options);
|
||||
|
||||
const clientAuth = await startAuthentication(options);
|
||||
const clientAuth = await startAuthentication(options)
|
||||
// console.log("simple credential get", clientAuth);
|
||||
|
||||
const authenticatorDataBase64Url = clientAuth.response.authenticatorData;
|
||||
const authenticatorDataBase64Url = clientAuth.response.authenticatorData
|
||||
this.authenticatorData = Buffer.from(
|
||||
clientAuth.response.authenticatorData,
|
||||
"base64",
|
||||
).buffer;
|
||||
this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON;
|
||||
'base64'
|
||||
).buffer
|
||||
this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON
|
||||
// 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
|
||||
const header: JWTPayload = { typ: "JWANT", alg: "ES256" };
|
||||
const header: JWTPayload = { typ: 'JWANT', alg: 'ES256' }
|
||||
const headerBase64 = Buffer.from(JSON.stringify(header))
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
|
||||
const dataInJwt = {
|
||||
AuthenticationDataB64URL: authenticatorDataBase64Url,
|
||||
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
|
||||
exp: expiryTime,
|
||||
iat: issuedAt,
|
||||
iss: issuerDid,
|
||||
};
|
||||
const dataInJwtString = JSON.stringify(dataInJwt);
|
||||
iss: issuerDid
|
||||
}
|
||||
const dataInJwtString = JSON.stringify(dataInJwt)
|
||||
const payloadBase64 = Buffer.from(dataInJwtString)
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
|
||||
const signature = clientAuth.response.signature;
|
||||
const signature = clientAuth.response.signature
|
||||
|
||||
return headerBase64 + "." + payloadBase64 + "." + signature;
|
||||
return headerBase64 + '.' + payloadBase64 + '.' + signature
|
||||
}
|
||||
|
||||
public async createJwtNavigator(
|
||||
issuerDid: string,
|
||||
payload: object,
|
||||
credIdHex: string,
|
||||
expMinutes: number = 1,
|
||||
expMinutes: number = 1
|
||||
) {
|
||||
const issuedAt = Math.floor(Date.now() / 1000);
|
||||
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60; // some minutes from now
|
||||
const issuedAt = Math.floor(Date.now() / 1000)
|
||||
const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60 // some minutes from now
|
||||
const fullPayload = {
|
||||
...payload,
|
||||
exp: expiryTime,
|
||||
iat: issuedAt,
|
||||
iss: issuerDid,
|
||||
};
|
||||
const dataToSignString = JSON.stringify(fullPayload);
|
||||
const dataToSignBuffer = Buffer.from(dataToSignString);
|
||||
const credentialId = Buffer.from(credIdHex, "hex");
|
||||
iss: issuerDid
|
||||
}
|
||||
const dataToSignString = JSON.stringify(fullPayload)
|
||||
const dataToSignBuffer = Buffer.from(dataToSignString)
|
||||
const credentialId = Buffer.from(credIdHex, 'hex')
|
||||
|
||||
// console.log("lower credentialId", credentialId);
|
||||
this.challenge = new Uint8Array(dataToSignBuffer);
|
||||
this.challenge = new Uint8Array(dataToSignBuffer)
|
||||
const options = {
|
||||
publicKey: {
|
||||
allowCredentials: [
|
||||
{
|
||||
id: credentialId,
|
||||
type: "public-key" as const,
|
||||
},
|
||||
type: 'public-key' as const
|
||||
}
|
||||
],
|
||||
challenge: this.challenge.buffer,
|
||||
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);
|
||||
|
||||
this.authenticatorData = credential?.response.authenticatorData;
|
||||
this.authenticatorData = credential?.response.authenticatorData
|
||||
const authenticatorDataBase64Url = arrayBufferToBase64URLString(
|
||||
this.authenticatorData as ArrayBuffer,
|
||||
);
|
||||
this.authenticatorData as ArrayBuffer
|
||||
)
|
||||
|
||||
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
|
||||
const header: JWTPayload = { typ: "JWANT", alg: "ES256" };
|
||||
const header: JWTPayload = { typ: 'JWANT', alg: 'ES256' }
|
||||
const headerBase64 = Buffer.from(JSON.stringify(header))
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
|
||||
const dataInJwt = {
|
||||
AuthenticationDataB64URL: authenticatorDataBase64Url,
|
||||
ClientDataJSONB64URL: this.clientDataJsonBase64Url,
|
||||
exp: expiryTime,
|
||||
iat: issuedAt,
|
||||
iss: issuerDid,
|
||||
};
|
||||
const dataInJwtString = JSON.stringify(dataInJwt);
|
||||
iss: issuerDid
|
||||
}
|
||||
const dataInJwtString = JSON.stringify(dataInJwt)
|
||||
const payloadBase64 = Buffer.from(dataInJwtString)
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
|
||||
const origSignature = Buffer.from(credential?.response.signature).toString(
|
||||
"base64",
|
||||
);
|
||||
'base64'
|
||||
)
|
||||
this.signature = origSignature
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "");
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
|
||||
const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature;
|
||||
return jwt;
|
||||
const jwt = headerBase64 + '.' + payloadBase64 + '.' + this.signature
|
||||
return jwt
|
||||
}
|
||||
|
||||
// To use this, add the asn1-ber library and add this import:
|
||||
@@ -302,11 +299,11 @@ export class PeerSetup {
|
||||
export async function createDidPeerJwt(
|
||||
did: string,
|
||||
credIdHex: string,
|
||||
payload: object,
|
||||
payload: object
|
||||
): Promise<string> {
|
||||
const peerSetup = new PeerSetup();
|
||||
const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex);
|
||||
return jwt;
|
||||
const peerSetup = new PeerSetup()
|
||||
const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex)
|
||||
return jwt
|
||||
}
|
||||
|
||||
// I'd love to use this but it doesn't verify.
|
||||
@@ -320,26 +317,26 @@ export async function verifyJwtP256(
|
||||
authenticatorData: ArrayBuffer,
|
||||
challenge: Uint8Array,
|
||||
clientDataJsonBase64Url: Base64URLString,
|
||||
signature: Base64URLString,
|
||||
signature: Base64URLString
|
||||
) {
|
||||
const authDataFromBase = Buffer.from(authenticatorData);
|
||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
||||
const sigBuffer = Buffer.from(signature, "base64");
|
||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
||||
const authDataFromBase = Buffer.from(authenticatorData)
|
||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, 'base64')
|
||||
const sigBuffer = Buffer.from(signature, 'base64')
|
||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer)
|
||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid)
|
||||
|
||||
// Hash the client data
|
||||
const hash = sha256(clientDataFromBase);
|
||||
const hash = sha256(clientDataFromBase)
|
||||
|
||||
// Construct the preimage
|
||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
||||
const preimage = Buffer.concat([authDataFromBase, hash])
|
||||
|
||||
const isValid = p256.verify(
|
||||
finalSigBuffer,
|
||||
new Uint8Array(preimage),
|
||||
publicKeyBytes,
|
||||
);
|
||||
return isValid;
|
||||
publicKeyBytes
|
||||
)
|
||||
return isValid
|
||||
}
|
||||
|
||||
export async function verifyJwtSimplewebauthn(
|
||||
@@ -348,37 +345,37 @@ export async function verifyJwtSimplewebauthn(
|
||||
authenticatorData: ArrayBuffer,
|
||||
challenge: Uint8Array,
|
||||
clientDataJsonBase64Url: Base64URLString,
|
||||
signature: Base64URLString,
|
||||
signature: Base64URLString
|
||||
) {
|
||||
const authData = arrayToBase64Url(Buffer.from(authenticatorData));
|
||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
||||
const authData = arrayToBase64Url(Buffer.from(authenticatorData))
|
||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid)
|
||||
const credId = arrayBufferToBase64URLString(
|
||||
Buffer.from(credIdHex, "hex").buffer,
|
||||
);
|
||||
Buffer.from(credIdHex, 'hex').buffer
|
||||
)
|
||||
const authOpts: VerifyAuthenticationResponseOpts = {
|
||||
authenticator: {
|
||||
credentialID: credId,
|
||||
credentialPublicKey: publicKeyBytes,
|
||||
counter: 0,
|
||||
counter: 0
|
||||
},
|
||||
expectedChallenge: arrayToBase64Url(challenge),
|
||||
expectedOrigin: window.location.origin,
|
||||
expectedRPID: window.location.hostname,
|
||||
response: {
|
||||
authenticatorAttachment: "platform",
|
||||
authenticatorAttachment: 'platform',
|
||||
clientExtensionResults: {},
|
||||
id: credId,
|
||||
rawId: credId,
|
||||
response: {
|
||||
authenticatorData: authData,
|
||||
clientDataJSON: clientDataJsonBase64Url,
|
||||
signature: signature,
|
||||
signature: signature
|
||||
},
|
||||
type: "public-key",
|
||||
},
|
||||
};
|
||||
const verification = await verifyAuthenticationResponse(authOpts);
|
||||
return verification.verified;
|
||||
type: 'public-key'
|
||||
}
|
||||
}
|
||||
const verification = await verifyAuthenticationResponse(authOpts)
|
||||
return verification.verified
|
||||
}
|
||||
|
||||
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
|
||||
@@ -388,27 +385,27 @@ export async function verifyJwtWebCrypto(
|
||||
authenticatorData: ArrayBuffer,
|
||||
challenge: Uint8Array,
|
||||
clientDataJsonBase64Url: Base64URLString,
|
||||
signature: Base64URLString,
|
||||
signature: Base64URLString
|
||||
) {
|
||||
const authDataFromBase = Buffer.from(authenticatorData);
|
||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
||||
const sigBuffer = Buffer.from(signature, "base64");
|
||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
||||
const authDataFromBase = Buffer.from(authenticatorData)
|
||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, 'base64')
|
||||
const sigBuffer = Buffer.from(signature, 'base64')
|
||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer)
|
||||
|
||||
// Hash the client data
|
||||
const hash = sha256(clientDataFromBase);
|
||||
const hash = sha256(clientDataFromBase)
|
||||
|
||||
// Construct the preimage
|
||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
||||
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
|
||||
const preimage = Buffer.concat([authDataFromBase, hash])
|
||||
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
||||
if (!did.startsWith("did:peer:0z")) {
|
||||
if (!did.startsWith('did:peer:0z')) {
|
||||
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
|
||||
// (another reference is the @aviarytech/did-peer resolver)
|
||||
@@ -420,131 +417,131 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
||||
* - change type to JsonWebKey2020
|
||||
*/
|
||||
|
||||
const id = did.split(":")[2];
|
||||
const multibase = id.slice(1);
|
||||
const encnumbasis = multibase.slice(1);
|
||||
const id = did.split(':')[2]
|
||||
const multibase = id.slice(1)
|
||||
const encnumbasis = multibase.slice(1)
|
||||
const didDocument = {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/did/v1",
|
||||
"https://w3id.org/security/suites/secp256k1-2019/v1",
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/did/v1',
|
||||
'https://w3id.org/security/suites/secp256k1-2019/v1'
|
||||
],
|
||||
assertionMethod: [did + "#" + encnumbasis],
|
||||
authentication: [did + "#" + encnumbasis],
|
||||
capabilityDelegation: [did + "#" + encnumbasis],
|
||||
capabilityInvocation: [did + "#" + encnumbasis],
|
||||
assertionMethod: [did + '#' + encnumbasis],
|
||||
authentication: [did + '#' + encnumbasis],
|
||||
capabilityDelegation: [did + '#' + encnumbasis],
|
||||
capabilityInvocation: [did + '#' + encnumbasis],
|
||||
id: did,
|
||||
keyAgreement: undefined,
|
||||
service: undefined,
|
||||
verificationMethod: [
|
||||
{
|
||||
controller: did,
|
||||
id: did + "#" + encnumbasis,
|
||||
id: did + '#' + encnumbasis,
|
||||
publicKeyMultibase: multibase,
|
||||
type: "EcdsaSecp256k1VerificationKey2019",
|
||||
},
|
||||
],
|
||||
};
|
||||
type: 'EcdsaSecp256k1VerificationKey2019'
|
||||
}
|
||||
]
|
||||
}
|
||||
return {
|
||||
didDocument,
|
||||
didDocumentMetadata: {},
|
||||
didResolutionMetadata: { contentType: "application/did+ld+json" },
|
||||
};
|
||||
didResolutionMetadata: { contentType: 'application/did+ld+json' }
|
||||
}
|
||||
}
|
||||
|
||||
// convert COSE public key to PEM format
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function COSEtoPEM(cose: Buffer) {
|
||||
// const alg = cose.get(3); // Algorithm
|
||||
const x = cose[-2]; // x-coordinate
|
||||
const y = cose[-3]; // y-coordinate
|
||||
const x = cose[-2] // x-coordinate
|
||||
const y = cose[-3] // y-coordinate
|
||||
|
||||
// Ensure the coordinates are in the correct format
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @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
|
||||
const pem = `-----BEGIN PUBLIC KEY-----
|
||||
${pubKeyBuffer.toString("base64")}
|
||||
-----END PUBLIC KEY-----`;
|
||||
${pubKeyBuffer.toString('base64')}
|
||||
-----END PUBLIC KEY-----`
|
||||
|
||||
return pem;
|
||||
return pem
|
||||
}
|
||||
|
||||
// tried the base64url library but got an error using their Buffer
|
||||
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
|
||||
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
|
||||
function base64urlDecodeArrayBuffer(input: string) {
|
||||
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
|
||||
const str = atob(input + pad);
|
||||
const bytes = new Uint8Array(str.length);
|
||||
input = input.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const pad = input.length % 4 === 0 ? '' : '===='.slice(input.length % 4)
|
||||
const str = atob(input + pad)
|
||||
const bytes = new Uint8Array(str.length)
|
||||
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
|
||||
function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) {
|
||||
const str = String.fromCharCode(...new Uint8Array(buffer));
|
||||
return base64urlEncodeString(str);
|
||||
const str = String.fromCharCode(...new Uint8Array(buffer))
|
||||
return base64urlEncodeString(str)
|
||||
}
|
||||
|
||||
// from @simplewebauthn/browser
|
||||
function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let str = "";
|
||||
const bytes = new Uint8Array(buffer)
|
||||
let str = ''
|
||||
for (const charCode of bytes) {
|
||||
str += String.fromCharCode(charCode);
|
||||
str += String.fromCharCode(charCode)
|
||||
}
|
||||
const base64String = btoa(str);
|
||||
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
||||
const base64String = btoa(str)
|
||||
return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
|
||||
}
|
||||
|
||||
// from @simplewebauthn/browser
|
||||
function base64URLStringToArrayBuffer(base64URLString: string) {
|
||||
const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padLength = (4 - (base64.length % 4)) % 4;
|
||||
const padded = base64.padEnd(base64.length + padLength, "=");
|
||||
const binary = atob(padded);
|
||||
const buffer = new ArrayBuffer(binary.length);
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/')
|
||||
const padLength = (4 - (base64.length % 4)) % 4
|
||||
const padded = base64.padEnd(base64.length + padLength, '=')
|
||||
const binary = atob(padded)
|
||||
const buffer = new ArrayBuffer(binary.length)
|
||||
const bytes = new Uint8Array(buffer)
|
||||
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
|
||||
async function pemToCryptoKey(pem: string) {
|
||||
const binaryDerString = atob(
|
||||
pem
|
||||
.split("\n")
|
||||
.filter((x) => !x.includes("-----"))
|
||||
.join(""),
|
||||
);
|
||||
const binaryDer = new Uint8Array(binaryDerString.length);
|
||||
.split('\n')
|
||||
.filter((x) => !x.includes('-----'))
|
||||
.join('')
|
||||
)
|
||||
const binaryDer = new Uint8Array(binaryDerString.length)
|
||||
for (let i = 0; i < binaryDerString.length; i++) {
|
||||
binaryDer[i] = binaryDerString.charCodeAt(i);
|
||||
binaryDer[i] = binaryDerString.charCodeAt(i)
|
||||
}
|
||||
// console.log("binaryDer", binaryDer.buffer);
|
||||
return await window.crypto.subtle.importKey(
|
||||
"spki",
|
||||
'spki',
|
||||
binaryDer.buffer,
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
hash: "SHA-256",
|
||||
name: 'RSASSA-PKCS1-v1_5',
|
||||
hash: 'SHA-256'
|
||||
},
|
||||
true,
|
||||
["verify"],
|
||||
);
|
||||
['verify']
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts
|
||||
import { AsnParser } from "@peculiar/asn1-schema";
|
||||
import { ECDSASigValue } from "@peculiar/asn1-ecc";
|
||||
import { AsnParser } from '@peculiar/asn1-schema'
|
||||
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.
|
||||
@@ -8,21 +8,21 @@ import { ECDSASigValue } from "@peculiar/asn1-ecc";
|
||||
* See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
|
||||
*/
|
||||
export function unwrapEC2Signature(signature: Uint8Array): Uint8Array {
|
||||
const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
|
||||
let rBytes = new Uint8Array(parsedSignature.r);
|
||||
let sBytes = new Uint8Array(parsedSignature.s);
|
||||
const parsedSignature = AsnParser.parse(signature, ECDSASigValue)
|
||||
let rBytes = new Uint8Array(parsedSignature.r)
|
||||
let sBytes = new Uint8Array(parsedSignature.s)
|
||||
|
||||
if (shouldRemoveLeadingZero(rBytes)) {
|
||||
rBytes = rBytes.slice(1);
|
||||
rBytes = rBytes.slice(1)
|
||||
}
|
||||
|
||||
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"
|
||||
*/
|
||||
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
|
||||
@@ -41,21 +41,21 @@ function shouldRemoveLeadingZero(bytes: Uint8Array): boolean {
|
||||
* Combine multiple Uint8Arrays into a single Uint8Array
|
||||
*/
|
||||
export function isoUint8ArrayConcat(arrays: Uint8Array[]): Uint8Array {
|
||||
let pointer = 0;
|
||||
const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0);
|
||||
let pointer = 0
|
||||
const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0)
|
||||
|
||||
const toReturn = new Uint8Array(totalLength);
|
||||
const toReturn = new Uint8Array(totalLength)
|
||||
|
||||
arrays.forEach((arr) => {
|
||||
toReturn.set(arr, pointer);
|
||||
pointer += arr.length;
|
||||
});
|
||||
toReturn.set(arr, pointer)
|
||||
pointer += arr.length
|
||||
})
|
||||
|
||||
return toReturn;
|
||||
return toReturn
|
||||
}
|
||||
|
||||
// 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 }> {
|
||||
/**
|
||||
* 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(
|
||||
(resolve, reject) => {
|
||||
if (webCrypto) {
|
||||
return resolve(webCrypto);
|
||||
return resolve(webCrypto)
|
||||
}
|
||||
/**
|
||||
* Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
|
||||
* support (and Node v20+)
|
||||
*/
|
||||
const _globalThisCrypto =
|
||||
_getWebCryptoInternals.stubThisGlobalThisCrypto();
|
||||
_getWebCryptoInternals.stubThisGlobalThisCrypto()
|
||||
if (_globalThisCrypto) {
|
||||
webCrypto = _globalThisCrypto;
|
||||
return resolve(webCrypto);
|
||||
webCrypto = _globalThisCrypto
|
||||
return resolve(webCrypto)
|
||||
}
|
||||
// We tried to access it both in Node and globally, so bail out
|
||||
return reject(new MissingWebCrypto());
|
||||
},
|
||||
);
|
||||
return toResolve;
|
||||
return reject(new MissingWebCrypto())
|
||||
}
|
||||
)
|
||||
return toResolve
|
||||
}
|
||||
class MissingWebCrypto extends Error {
|
||||
constructor() {
|
||||
const message = "An instance of the Crypto API could not be located";
|
||||
super(message);
|
||||
this.name = "MissingWebCrypto";
|
||||
const message = 'An instance of the Crypto API could not be located'
|
||||
super(message)
|
||||
this.name = 'MissingWebCrypto'
|
||||
}
|
||||
}
|
||||
// Make it possible to stub return values during testing
|
||||
@@ -100,6 +100,6 @@ const _getWebCryptoInternals = {
|
||||
stubThisGlobalThisCrypto: () => globalThis.crypto,
|
||||
// Make it possible to reset the `webCrypto` at the top of the file
|
||||
setCachedCrypto: (newCrypto: { subtle: SubtleCrypto }) => {
|
||||
webCrypto = newCrypto;
|
||||
},
|
||||
};
|
||||
webCrypto = newCrypto
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
||||
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
||||
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
||||
const rawData = window.atob(base64);
|
||||
const outputArray = new Uint8Array(rawData.length);
|
||||
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
|
||||
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
|
||||
const rawData = window.atob(base64)
|
||||
const outputArray = new Uint8Array(rawData.length)
|
||||
|
||||
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 {
|
||||
description: string;
|
||||
locLat?: number;
|
||||
locLon?: number;
|
||||
locLat2?: number;
|
||||
locLon2?: number;
|
||||
issuerDid: string;
|
||||
rowId?: string; // set on profile retrieved from server
|
||||
description: string
|
||||
locLat?: number
|
||||
locLon?: number
|
||||
locLat2?: number
|
||||
locLon2?: number
|
||||
issuerDid: string
|
||||
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
|
||||
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { Buffer } from "buffer";
|
||||
import * as R from "ramda";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
import { Buffer } from 'buffer'
|
||||
import * as R from 'ramda'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from '../constants/app'
|
||||
import {
|
||||
accountsDBPromise,
|
||||
retrieveSettingsForActiveAccount,
|
||||
updateAccountSettings,
|
||||
updateDefaultSettings,
|
||||
} from "../db/index";
|
||||
import { Account } from "../db/tables/accounts";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
||||
import { deriveAddress, generateSeed, newIdentifier } from "../libs/crypto";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
updateDefaultSettings
|
||||
} from '../db/index'
|
||||
import { Account } from '../db/tables/accounts'
|
||||
import { Contact } from '../db/tables/contacts'
|
||||
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from '../db/tables/settings'
|
||||
import { deriveAddress, generateSeed, newIdentifier } from '../libs/crypto'
|
||||
import * as serverUtil from '../libs/endorserServer'
|
||||
import {
|
||||
containsHiddenDid,
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
GiveSummaryRecord,
|
||||
OfferVerifiableCredential,
|
||||
} from "../libs/endorserServer";
|
||||
import { KeyMeta } from "../libs/crypto/vc";
|
||||
import { createPeerDid } from "../libs/crypto/vc/didPeer";
|
||||
import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer";
|
||||
import { logger } from "../utils/logger";
|
||||
OfferVerifiableCredential
|
||||
} from '../libs/endorserServer'
|
||||
import { KeyMeta } from '../libs/crypto/vc'
|
||||
import { createPeerDid } from '../libs/crypto/vc/didPeer'
|
||||
import { registerCredential } from '../libs/crypto/vc/passkeyDidPeer'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
export interface GiverReceiverInputInfo {
|
||||
did?: string;
|
||||
name?: string;
|
||||
did?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export enum OnboardPage {
|
||||
Home = "HOME",
|
||||
Discover = "DISCOVER",
|
||||
Create = "CREATE",
|
||||
Contact = "CONTACT",
|
||||
Account = "ACCOUNT",
|
||||
Home = 'HOME',
|
||||
Discover = 'DISCOVER',
|
||||
Create = 'CREATE',
|
||||
Contact = 'CONTACT',
|
||||
Account = 'ACCOUNT'
|
||||
}
|
||||
|
||||
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.";
|
||||
export const SHARED_PHOTO_BASE64_KEY = "shared-photo-base64";
|
||||
'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'
|
||||
|
||||
/* eslint-disable prettier/prettier */
|
||||
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>> = {
|
||||
BTC: {
|
||||
name: "Bitcoin",
|
||||
faIcon: "bitcoin-sign",
|
||||
name: 'Bitcoin',
|
||||
faIcon: 'bitcoin-sign'
|
||||
},
|
||||
HUR: {
|
||||
name: "hours",
|
||||
faIcon: "clock",
|
||||
name: 'hours',
|
||||
faIcon: 'clock'
|
||||
},
|
||||
USD: {
|
||||
name: "US Dollars",
|
||||
faIcon: "dollar",
|
||||
},
|
||||
};
|
||||
name: 'US Dollars',
|
||||
faIcon: 'dollar'
|
||||
}
|
||||
}
|
||||
|
||||
export function iconForUnitCode(unitCode: string) {
|
||||
return UNIT_CODES[unitCode]?.faIcon || "question";
|
||||
return UNIT_CODES[unitCode]?.faIcon || 'question'
|
||||
}
|
||||
|
||||
// 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.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return !isNaN(str) && !isNaN(parseFloat(str));
|
||||
return !isNaN(str) && !isNaN(parseFloat(str))
|
||||
}
|
||||
|
||||
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
|
||||
**/
|
||||
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) => {
|
||||
return claimType === "GiveAction";
|
||||
};
|
||||
return claimType === 'GiveAction'
|
||||
}
|
||||
|
||||
export const isGiveAction = (
|
||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>
|
||||
) => {
|
||||
return isGiveClaimType(veriClaim.claimType);
|
||||
};
|
||||
return isGiveClaimType(veriClaim.claimType)
|
||||
}
|
||||
|
||||
export const shortDid = (did: string) => {
|
||||
if (did.startsWith("did:peer:")) {
|
||||
if (did.startsWith('did:peer:')) {
|
||||
return (
|
||||
did.substring(0, "did:peer:".length + 2) +
|
||||
"..." +
|
||||
did.substring("did:peer:".length + 18, "did:peer:".length + 25) +
|
||||
"..."
|
||||
);
|
||||
} else if (did.startsWith("did:ethr:")) {
|
||||
return did.substring(0, "did:ethr:".length + 9) + "...";
|
||||
did.substring(0, 'did:peer:'.length + 2) +
|
||||
'...' +
|
||||
did.substring('did:peer:'.length + 18, 'did:peer:'.length + 25) +
|
||||
'...'
|
||||
)
|
||||
} else if (did.startsWith('did:ethr:')) {
|
||||
return did.substring(0, 'did:ethr:'.length + 9) + '...'
|
||||
} else {
|
||||
return did.substring(0, did.indexOf(":", 4) + 7) + "...";
|
||||
return did.substring(0, did.indexOf(':', 4) + 7) + '...'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const nameForDid = (
|
||||
activeDid: string,
|
||||
contacts: Array<Contact>,
|
||||
did: string,
|
||||
did: string
|
||||
): string => {
|
||||
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 = (
|
||||
contact?: Contact,
|
||||
capitalize?: boolean,
|
||||
capitalize?: boolean
|
||||
): string => {
|
||||
return (
|
||||
(contact?.name as string) ||
|
||||
(capitalize ? "This" : "this") + " unnamed user"
|
||||
);
|
||||
};
|
||||
(capitalize ? 'This' : 'this') + ' unnamed user'
|
||||
)
|
||||
}
|
||||
|
||||
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
|
||||
fn();
|
||||
fn()
|
||||
useClipboard()
|
||||
.copy(text)
|
||||
.then(() => setTimeout(fn, 2000));
|
||||
};
|
||||
.then(() => setTimeout(fn, 2000))
|
||||
}
|
||||
|
||||
export interface ConfirmerData {
|
||||
confirmerIdList: string[];
|
||||
confsVisibleToIdList: string[];
|
||||
numConfsNotVisible: number;
|
||||
confirmerIdList: string[]
|
||||
confsVisibleToIdList: string[]
|
||||
numConfsNotVisible: number
|
||||
}
|
||||
|
||||
// // 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,
|
||||
claimId: string,
|
||||
claimIssuerId: string,
|
||||
userDid: string,
|
||||
userDid: string
|
||||
): Promise<ConfirmerData | undefined> {
|
||||
const confirmUrl =
|
||||
apiServer +
|
||||
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
|
||||
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId));
|
||||
const confirmHeaders = await serverUtil.getHeaders(userDid);
|
||||
'/api/report/issuersWhoClaimedOrConfirmed?claimId=' +
|
||||
encodeURIComponent(serverUtil.stripEndorserPrefix(claimId))
|
||||
const confirmHeaders = await serverUtil.getHeaders(userDid)
|
||||
const response = await axios.get(confirmUrl, {
|
||||
headers: confirmHeaders,
|
||||
});
|
||||
headers: confirmHeaders
|
||||
})
|
||||
if (response.status === 200) {
|
||||
const resultList1 = response.data.result || [];
|
||||
const resultList1 = response.data.result || []
|
||||
//const publicUrls = resultList.publicUrls || [];
|
||||
delete resultList1.publicUrls;
|
||||
delete resultList1.publicUrls
|
||||
// exclude hidden DIDs
|
||||
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1);
|
||||
const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1)
|
||||
// exclude the issuer
|
||||
const resultList3 = R.reject(
|
||||
(did: string) => did === claimIssuerId,
|
||||
resultList2,
|
||||
);
|
||||
const confirmerIdList = resultList3;
|
||||
let numConfsNotVisible = resultList1.length - resultList2.length;
|
||||
resultList2
|
||||
)
|
||||
const confirmerIdList = resultList3
|
||||
let numConfsNotVisible = resultList1.length - resultList2.length
|
||||
if (resultList3.length === resultList2.length) {
|
||||
// the issuer was not in the "visible" list so they must be hidden
|
||||
// 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 = {
|
||||
confirmerIdList,
|
||||
confsVisibleToIdList,
|
||||
numConfsNotVisible,
|
||||
};
|
||||
return result;
|
||||
numConfsNotVisible
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
logger.error(
|
||||
"Bad response status of",
|
||||
'Bad response status of',
|
||||
response.status,
|
||||
"for confirmers:",
|
||||
response,
|
||||
);
|
||||
return undefined;
|
||||
'for confirmers:',
|
||||
response
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ export function isGiveRecordTheUserCanConfirm(
|
||||
isRegistered: boolean,
|
||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
||||
activeDid: string,
|
||||
confirmerIdList: string[] = [],
|
||||
confirmerIdList: string[] = []
|
||||
): boolean {
|
||||
return (
|
||||
isRegistered &&
|
||||
@@ -253,7 +253,7 @@ export function isGiveRecordTheUserCanConfirm(
|
||||
!confirmerIdList.includes(activeDid) &&
|
||||
veriClaim.issuer !== activeDid &&
|
||||
!containsHiddenDid(veriClaim.claim)
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function notifyWhyCannotConfirm(
|
||||
@@ -262,101 +262,101 @@ export function notifyWhyCannotConfirm(
|
||||
claimType: string | undefined,
|
||||
giveDetails: GiveSummaryRecord | undefined,
|
||||
activeDid: string,
|
||||
confirmerIdList: string[] = [],
|
||||
confirmerIdList: string[] = []
|
||||
) {
|
||||
if (!isRegistered) {
|
||||
notifyFun(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Not Registered",
|
||||
text: "Someone needs to register you before you can confirm.",
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
title: 'Not Registered',
|
||||
text: 'Someone needs to register you before you can confirm.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
3000
|
||||
)
|
||||
} else if (!isGiveClaimType(claimType)) {
|
||||
notifyFun(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Not A Give",
|
||||
text: "This is not a giving action to confirm.",
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
title: 'Not A Give',
|
||||
text: 'This is not a giving action to confirm.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
3000
|
||||
)
|
||||
} else if (confirmerIdList.includes(activeDid)) {
|
||||
notifyFun(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Already Confirmed",
|
||||
text: "You already confirmed this claim.",
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
title: 'Already Confirmed',
|
||||
text: 'You already confirmed this claim.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
3000
|
||||
)
|
||||
} else if (giveDetails?.issuerDid == activeDid) {
|
||||
notifyFun(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Cannot Confirm",
|
||||
text: "You cannot confirm this because you issued this claim.",
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
title: 'Cannot Confirm',
|
||||
text: 'You cannot confirm this because you issued this claim.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
3000
|
||||
)
|
||||
} else if (serverUtil.containsHiddenDid(giveDetails?.fullClaim)) {
|
||||
notifyFun(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Cannot Confirm",
|
||||
text: "You cannot confirm this because some people are hidden.",
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
title: 'Cannot Confirm',
|
||||
text: 'You cannot confirm this because some people are hidden.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
3000
|
||||
)
|
||||
} else {
|
||||
notifyFun(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
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.",
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
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.'
|
||||
},
|
||||
3000,
|
||||
);
|
||||
3000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string); // potential problem if it returns an ArrayBuffer?
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => resolve(reader.result as string) // potential problem if it returns an ArrayBuffer?
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
|
||||
// Extract the content type and the Base64 data
|
||||
const [metadata, base64] = base64DataUrl.split(",");
|
||||
const contentTypeMatch = metadata.match(/data:(.*?);base64/);
|
||||
const contentType = contentTypeMatch ? contentTypeMatch[1] : "";
|
||||
const [metadata, base64] = base64DataUrl.split(',')
|
||||
const contentTypeMatch = metadata.match(/data:(.*?);base64/)
|
||||
const contentType = contentTypeMatch ? contentTypeMatch[1] : ''
|
||||
|
||||
const byteCharacters = atob(base64);
|
||||
const byteArrays = [];
|
||||
const byteCharacters = atob(base64)
|
||||
const byteArrays = []
|
||||
|
||||
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++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
byteNumbers[i] = slice.charCodeAt(i)
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
const byteArray = new Uint8Array(byteNumbers)
|
||||
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
|
||||
*/
|
||||
export function offerGiverDid(
|
||||
veriClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
||||
veriClaim: GenericCredWrapper<OfferVerifiableCredential>
|
||||
): string | undefined {
|
||||
let giver;
|
||||
let giver
|
||||
if (
|
||||
veriClaim.claim.offeredBy?.identifier &&
|
||||
!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)) {
|
||||
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
|
||||
*/
|
||||
export const canFulfillOffer = (
|
||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>
|
||||
) => {
|
||||
return (
|
||||
veriClaim.claimType === "Offer" &&
|
||||
veriClaim.claimType === 'Offer' &&
|
||||
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferVerifiableCredential>)
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
// return object with paths and arrays of DIDs for any keys ending in "VisibleToDid"
|
||||
export function findAllVisibleToDids(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
input: any,
|
||||
humanReadable = false,
|
||||
humanReadable = false
|
||||
): Record<string, Array<string>> {
|
||||
if (Array.isArray(input)) {
|
||||
const result: Record<string, Array<string>> = {};
|
||||
const result: Record<string, Array<string>> = {}
|
||||
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) {
|
||||
const pathKey = humanReadable
|
||||
? "#" + (i + 1) + " " + key
|
||||
: "[" + i + "]" + key;
|
||||
result[pathKey] = inside[key];
|
||||
? '#' + (i + 1) + ' ' + key
|
||||
: '[' + i + ']' + key
|
||||
result[pathKey] = inside[key]
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
} else if (input instanceof Object) {
|
||||
// regular map (non-array) object
|
||||
const result: Record<string, Array<string>> = {};
|
||||
const result: Record<string, Array<string>> = {}
|
||||
for (const key in input) {
|
||||
if (key.endsWith("VisibleToDids")) {
|
||||
const newKey = key.slice(0, -"VisibleToDids".length);
|
||||
const pathKey = humanReadable ? newKey : "." + newKey;
|
||||
result[pathKey] = input[key];
|
||||
if (key.endsWith('VisibleToDids')) {
|
||||
const newKey = key.slice(0, -'VisibleToDids'.length)
|
||||
const pathKey = humanReadable ? newKey : '.' + newKey
|
||||
result[pathKey] = input[key]
|
||||
} else {
|
||||
const inside = findAllVisibleToDids(input[key], humanReadable);
|
||||
const inside = findAllVisibleToDids(input[key], humanReadable)
|
||||
for (const insideKey in inside) {
|
||||
const pathKey = humanReadable
|
||||
? key + "'s " + insideKey
|
||||
: "." + key + insideKey;
|
||||
result[pathKey] = inside[insideKey];
|
||||
: '.' + key + insideKey
|
||||
result[pathKey] = inside[insideKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return result
|
||||
} else {
|
||||
return {};
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,151 +461,151 @@ export interface AccountKeyInfo extends Account, KeyMeta {}
|
||||
|
||||
export const retrieveAccountCount = async (): Promise<number> => {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
return await accountsDB.accounts.count();
|
||||
};
|
||||
const accountsDB = await accountsDBPromise
|
||||
return await accountsDB.accounts.count()
|
||||
}
|
||||
|
||||
export const retrieveAccountDids = async (): Promise<string[]> => {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const allAccounts = await accountsDB.accounts.toArray();
|
||||
const allDids = allAccounts.map((acc) => acc.did);
|
||||
return allDids;
|
||||
};
|
||||
const accountsDB = await accountsDBPromise
|
||||
const allAccounts = await accountsDB.accounts.toArray()
|
||||
const allDids = allAccounts.map((acc) => acc.did)
|
||||
return allDids
|
||||
}
|
||||
|
||||
// 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.
|
||||
export const retrieveAccountMetadata = async (
|
||||
activeDid: string,
|
||||
activeDid: string
|
||||
): Promise<AccountKeyInfo | undefined> => {
|
||||
// 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
|
||||
.where("did")
|
||||
.where('did')
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
.first()) as Account
|
||||
if (account) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
return metadata;
|
||||
const { identity, mnemonic, ...metadata } = account
|
||||
return metadata
|
||||
} else {
|
||||
return undefined;
|
||||
return undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const array = await accountsDB.accounts.toArray();
|
||||
const accountsDB = await accountsDBPromise
|
||||
const array = await accountsDB.accounts.toArray()
|
||||
return array.map((account) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { identity, mnemonic, ...metadata } = account;
|
||||
return metadata;
|
||||
});
|
||||
};
|
||||
const { identity, mnemonic, ...metadata } = account
|
||||
return metadata
|
||||
})
|
||||
}
|
||||
|
||||
export const retrieveFullyDecryptedAccount = async (
|
||||
activeDid: string,
|
||||
activeDid: string
|
||||
): Promise<AccountKeyInfo | undefined> => {
|
||||
// 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
|
||||
.where("did")
|
||||
.where('did')
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
return account;
|
||||
};
|
||||
.first()) as Account
|
||||
return account
|
||||
}
|
||||
|
||||
// let's try and eliminate this
|
||||
export const retrieveAllFullyDecryptedAccounts = async (): Promise<
|
||||
Array<AccountKeyInfo>
|
||||
> => {
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const allAccounts = await accountsDB.accounts.toArray();
|
||||
return allAccounts;
|
||||
};
|
||||
const accountsDB = await accountsDBPromise
|
||||
const allAccounts = await accountsDB.accounts.toArray()
|
||||
return allAccounts
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
||||
const mnemonic = generateSeed();
|
||||
const mnemonic = generateSeed()
|
||||
// address is 0x... ETH address, without "did:eth:"
|
||||
const [address, privateHex, publicHex, derivationPath] =
|
||||
deriveAddress(mnemonic);
|
||||
deriveAddress(mnemonic)
|
||||
|
||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||
const identity = JSON.stringify(newId);
|
||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath)
|
||||
const identity = JSON.stringify(newId)
|
||||
|
||||
// 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({
|
||||
dateCreated: new Date().toISOString(),
|
||||
derivationPath: derivationPath,
|
||||
did: newId.did,
|
||||
identity: identity,
|
||||
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");
|
||||
await updateAccountSettings(newId.did, { isRegistered: false });
|
||||
await updateAccountSettings(newId.did, { isRegistered: false })
|
||||
|
||||
return newId.did;
|
||||
};
|
||||
return newId.did
|
||||
}
|
||||
|
||||
export const registerAndSavePasskey = async (
|
||||
keyName: string,
|
||||
keyName: string
|
||||
): Promise<Account> => {
|
||||
const cred = await registerCredential(keyName);
|
||||
const publicKeyBytes = cred.publicKeyBytes;
|
||||
const did = createPeerDid(publicKeyBytes as Uint8Array);
|
||||
const passkeyCredIdHex = cred.credIdHex as string;
|
||||
const cred = await registerCredential(keyName)
|
||||
const publicKeyBytes = cred.publicKeyBytes
|
||||
const did = createPeerDid(publicKeyBytes as Uint8Array)
|
||||
const passkeyCredIdHex = cred.credIdHex as string
|
||||
|
||||
const account = {
|
||||
dateCreated: new Date().toISOString(),
|
||||
did,
|
||||
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
|
||||
const accountsDB = await accountsDBPromise;
|
||||
await accountsDB.accounts.add(account);
|
||||
return account;
|
||||
};
|
||||
const accountsDB = await accountsDBPromise
|
||||
await accountsDB.accounts.add(account)
|
||||
return account
|
||||
}
|
||||
|
||||
export const registerSaveAndActivatePasskey = async (
|
||||
keyName: string,
|
||||
keyName: string
|
||||
): Promise<Account> => {
|
||||
const account = await registerAndSavePasskey(keyName);
|
||||
await updateDefaultSettings({ activeDid: account.did });
|
||||
await updateAccountSettings(account.did, { isRegistered: false });
|
||||
return account;
|
||||
};
|
||||
const account = await registerAndSavePasskey(keyName)
|
||||
await updateDefaultSettings({ activeDid: account.did })
|
||||
await updateAccountSettings(account.did, { isRegistered: false })
|
||||
return account
|
||||
}
|
||||
|
||||
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
return (
|
||||
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
|
||||
60
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
// 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.
|
||||
export const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION";
|
||||
export const DIRECT_PUSH_TITLE = 'DIRECT_NOTIFICATION'
|
||||
|
||||
export const sendTestThroughPushServer = async (
|
||||
subscriptionJSON: PushSubscriptionJSON,
|
||||
skipFilter: boolean,
|
||||
skipFilter: boolean
|
||||
): Promise<AxiosResponse> => {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
|
||||
const settings = await retrieveSettingsForActiveAccount()
|
||||
let pushUrl: string = DEFAULT_PUSH_SERVER as string
|
||||
if (settings?.webPushServer) {
|
||||
pushUrl = settings.webPushServer;
|
||||
pushUrl = settings.webPushServer
|
||||
}
|
||||
|
||||
const newPayload = {
|
||||
@@ -613,20 +613,20 @@ export const sendTestThroughPushServer = async (
|
||||
// ... overridden with the following
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
message: `Test, where you will see this message ${ skipFilter ? "un" : "" }filtered.`,
|
||||
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
|
||||
};
|
||||
logger.log("Sending a test web push message:", newPayload);
|
||||
const payloadStr = JSON.stringify(newPayload);
|
||||
title: skipFilter ? DIRECT_PUSH_TITLE : 'Your Web Push'
|
||||
}
|
||||
logger.log('Sending a test web push message:', newPayload)
|
||||
const payloadStr = JSON.stringify(newPayload)
|
||||
const response = await axios.post(
|
||||
pushUrl + "/web-push/send-test",
|
||||
pushUrl + '/web-push/send-test',
|
||||
payloadStr,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
logger.log("Got response from web push server:", response);
|
||||
return response;
|
||||
};
|
||||
logger.log('Got response from web push server:', response)
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// see also ../constants/app.ts and
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
import { initializeApp } from "./main.common";
|
||||
import { App } from "./lib/capacitor/app";
|
||||
import router from "./router";
|
||||
import { handleApiError } from "./services/api";
|
||||
import { AxiosError } from "axios";
|
||||
import { DeepLinkHandler } from "./services/deepLinks";
|
||||
import { logConsoleAndDb } from "./db";
|
||||
import { logger } from "./utils/logger";
|
||||
import { initializeApp } from './main.common'
|
||||
import { App } from './lib/capacitor/app'
|
||||
import router from './router'
|
||||
import { handleApiError } from './services/api'
|
||||
import { AxiosError } from 'axios'
|
||||
import { DeepLinkHandler } from './services/deepLinks'
|
||||
import { logConsoleAndDb } from './db'
|
||||
import { logger } from './utils/logger'
|
||||
|
||||
logger.log("[Capacitor] Starting initialization");
|
||||
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
||||
logger.log('[Capacitor] Starting initialization')
|
||||
logger.log('[Capacitor] Platform:', process.env.VITE_PLATFORM)
|
||||
|
||||
const app = initializeApp();
|
||||
const app = initializeApp()
|
||||
|
||||
// Initialize API error handling for unhandled promise rejections
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
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
|
||||
@@ -69,22 +69,22 @@ const deepLinkHandler = new DeepLinkHandler(router);
|
||||
*/
|
||||
const handleDeepLink = async (data: { url: string }) => {
|
||||
try {
|
||||
await router.isReady();
|
||||
await deepLinkHandler.handleDeepLink(data.url);
|
||||
await router.isReady()
|
||||
await deepLinkHandler.handleDeepLink(data.url)
|
||||
} catch (error) {
|
||||
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
|
||||
logConsoleAndDb('[DeepLink] Error handling deep link: ' + error, true)
|
||||
handleApiError(
|
||||
{
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
} as AxiosError,
|
||||
"deep-link",
|
||||
);
|
||||
'deep-link'
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Register deep link handler with Capacitor
|
||||
App.addListener("appUrlOpen", handleDeepLink);
|
||||
App.addListener('appUrlOpen', handleDeepLink)
|
||||
|
||||
logger.log("[Capacitor] Mounting app");
|
||||
app.mount("#app");
|
||||
logger.log("[Capacitor] App mounted");
|
||||
logger.log('[Capacitor] Mounting app')
|
||||
app.mount('#app')
|
||||
logger.log('[Capacitor] App mounted')
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
import { createPinia } from "pinia";
|
||||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import axios from "axios";
|
||||
import VueAxios from "vue-axios";
|
||||
import Notifications from "notiwind";
|
||||
import "./assets/styles/tailwind.css";
|
||||
import { FontAwesomeIcon } from "./lib/fontawesome";
|
||||
import Camera from "simple-vue-camera";
|
||||
import { logger } from "./utils/logger";
|
||||
import { createPinia } from 'pinia'
|
||||
import { App as VueApp, ComponentPublicInstance, createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import axios from 'axios'
|
||||
import VueAxios from 'vue-axios'
|
||||
import Notifications from 'notiwind'
|
||||
import './assets/styles/tailwind.css'
|
||||
import { FontAwesomeIcon } from './lib/fontawesome'
|
||||
import Camera from 'simple-vue-camera'
|
||||
import { logger } from './utils/logger'
|
||||
|
||||
// Global Error Handler
|
||||
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 = (
|
||||
err: unknown,
|
||||
instance: ComponentPublicInstance | null,
|
||||
info: string,
|
||||
info: string
|
||||
) => {
|
||||
logger.error("[App Error] Global Error Handler:", {
|
||||
logger.error('[App Error] Global Error Handler:', {
|
||||
error: err,
|
||||
info,
|
||||
component: instance?.$options.name || "unknown",
|
||||
});
|
||||
component: instance?.$options.name || 'unknown'
|
||||
})
|
||||
alert(
|
||||
(err instanceof Error ? err.message : "Something bad happened") +
|
||||
" - Try reloading or restarting the app.",
|
||||
);
|
||||
};
|
||||
(err instanceof Error ? err.message : 'Something bad happened') +
|
||||
' - Try reloading or restarting the app.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Function to initialize the app
|
||||
export function initializeApp() {
|
||||
logger.log("[App Init] Starting app initialization");
|
||||
logger.log("[App Init] Platform:", process.env.VITE_PLATFORM);
|
||||
logger.log('[App Init] Starting app initialization')
|
||||
logger.log('[App Init] Platform:', process.env.VITE_PLATFORM)
|
||||
|
||||
const app = createApp(App);
|
||||
logger.log("[App Init] Vue app created");
|
||||
const app = createApp(App)
|
||||
logger.log('[App Init] Vue app created')
|
||||
|
||||
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera);
|
||||
logger.log("[App Init] Components registered");
|
||||
app.component('FontAwesome', FontAwesomeIcon).component('camera', Camera)
|
||||
logger.log('[App Init] Components registered')
|
||||
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
logger.log("[App Init] Pinia store initialized");
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
logger.log('[App Init] Pinia store initialized')
|
||||
|
||||
app.use(VueAxios, axios);
|
||||
logger.log("[App Init] Axios initialized");
|
||||
app.use(VueAxios, axios)
|
||||
logger.log('[App Init] Axios initialized')
|
||||
|
||||
app.use(router);
|
||||
logger.log("[App Init] Router initialized");
|
||||
app.use(router)
|
||||
logger.log('[App Init] Router initialized')
|
||||
|
||||
app.use(Notifications);
|
||||
logger.log("[App Init] Notifications initialized");
|
||||
app.use(Notifications)
|
||||
logger.log('[App Init] Notifications initialized')
|
||||
|
||||
setupGlobalErrorHandler(app);
|
||||
logger.log("[App Init] App initialization complete");
|
||||
setupGlobalErrorHandler(app)
|
||||
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();
|
||||
app.mount("#app");
|
||||
const app = initializeApp()
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { initializeApp } from "./main.common";
|
||||
import { initializeApp } from './main.common'
|
||||
|
||||
const app = initializeApp();
|
||||
app.mount("#app");
|
||||
const app = initializeApp()
|
||||
app.mount('#app')
|
||||
|
||||
68
src/main.ts
68
src/main.ts
@@ -1,14 +1,14 @@
|
||||
import { createPinia } from "pinia";
|
||||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import "./registerServiceWorker";
|
||||
import router from "./router";
|
||||
import axios from "axios";
|
||||
import VueAxios from "vue-axios";
|
||||
import Notifications from "notiwind";
|
||||
import "./assets/styles/tailwind.css";
|
||||
import { createPinia } from 'pinia'
|
||||
import { App as VueApp, ComponentPublicInstance, createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import './registerServiceWorker'
|
||||
import router from './router'
|
||||
import axios from 'axios'
|
||||
import VueAxios from 'vue-axios'
|
||||
import Notifications from 'notiwind'
|
||||
import './assets/styles/tailwind.css'
|
||||
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowLeft,
|
||||
@@ -86,8 +86,8 @@ import {
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUsers,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
faXmark
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faArrowDown,
|
||||
@@ -166,12 +166,12 @@ library.add(
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUsers,
|
||||
faXmark,
|
||||
);
|
||||
faXmark
|
||||
)
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import Camera from "simple-vue-camera";
|
||||
import { logger } from "./utils/logger";
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import Camera from 'simple-vue-camera'
|
||||
import { logger } from './utils/logger'
|
||||
|
||||
// Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
|
||||
function setupGlobalErrorHandler(app: VueApp) {
|
||||
@@ -179,35 +179,35 @@ function setupGlobalErrorHandler(app: VueApp) {
|
||||
app.config.errorHandler = (
|
||||
err: Error,
|
||||
instance: ComponentPublicInstance | null,
|
||||
info: string,
|
||||
info: string
|
||||
) => {
|
||||
logger.error(
|
||||
"Ouch! Global Error Handler.",
|
||||
"Error:",
|
||||
'Ouch! Global Error Handler.',
|
||||
'Error:',
|
||||
err,
|
||||
"- Error toString:",
|
||||
'- Error toString:',
|
||||
err.toString(),
|
||||
"- Info:",
|
||||
'- Info:',
|
||||
info,
|
||||
"- Instance:",
|
||||
instance,
|
||||
);
|
||||
'- Instance:',
|
||||
instance
|
||||
)
|
||||
// Want to show a nice notiwind notification but can't figure out how.
|
||||
alert(
|
||||
(err.message || "Something bad happened") +
|
||||
" - Try reloading or restarting the app.",
|
||||
);
|
||||
};
|
||||
(err.message || 'Something bad happened') +
|
||||
' - Try reloading or restarting the app.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const app = createApp(App)
|
||||
.component("fa", FontAwesomeIcon)
|
||||
.component("camera", Camera)
|
||||
.component('fa', FontAwesomeIcon)
|
||||
.component('camera', Camera)
|
||||
.use(createPinia())
|
||||
.use(VueAxios, axios)
|
||||
.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 "./registerServiceWorker"; // Web PWA support
|
||||
import { initializeApp } from './main.common'
|
||||
import './registerServiceWorker' // Web PWA support
|
||||
|
||||
const app = initializeApp();
|
||||
app.mount("#app");
|
||||
const app = initializeApp()
|
||||
app.mount('#app')
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
/* 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
|
||||
if (
|
||||
process.env.VITE_PWA_ENABLED === "true" &&
|
||||
process.env.NODE_ENV === "production"
|
||||
process.env.VITE_PWA_ENABLED === 'true' &&
|
||||
process.env.NODE_ENV === 'production'
|
||||
) {
|
||||
register(`${process.env.BASE_URL}sw.js`, {
|
||||
ready() {
|
||||
console.log("Service worker is active.");
|
||||
console.log('Service worker is active.')
|
||||
},
|
||||
registered() {
|
||||
console.log("Service worker has been registered.");
|
||||
console.log('Service worker has been registered.')
|
||||
},
|
||||
cached() {
|
||||
console.log("Content has been cached for offline use.");
|
||||
console.log('Content has been cached for offline use.')
|
||||
},
|
||||
updatefound() {
|
||||
console.log("New content is downloading.");
|
||||
console.log('New content is downloading.')
|
||||
},
|
||||
updated() {
|
||||
console.log("New content is available; please refresh.");
|
||||
console.log('New content is available; please refresh.')
|
||||
},
|
||||
offline() {
|
||||
console.log(
|
||||
"No internet connection found. App is running in offline mode.",
|
||||
);
|
||||
'No internet connection found. App is running in offline mode.'
|
||||
)
|
||||
},
|
||||
error(error) {
|
||||
console.error("Error during service worker registration:", error);
|
||||
},
|
||||
});
|
||||
console.error('Error during service worker registration:', error)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
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,
|
||||
NavigationGuardNext,
|
||||
RouteLocationNormalized,
|
||||
RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
import { accountsDBPromise } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
RouteRecordRaw
|
||||
} from 'vue-router'
|
||||
import { accountsDBPromise } from '../db/index'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -18,317 +18,317 @@ import { logger } from "../utils/logger";
|
||||
const enterOrStart = async (
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
next: NavigationGuardNext,
|
||||
next: NavigationGuardNext
|
||||
) => {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
const accountsDB = await accountsDBPromise;
|
||||
const num_accounts = await accountsDB.accounts.count();
|
||||
const accountsDB = await accountsDBPromise
|
||||
const num_accounts = await accountsDB.accounts.count()
|
||||
|
||||
if (num_accounts > 0) {
|
||||
next();
|
||||
next()
|
||||
} else {
|
||||
next({ name: "start" });
|
||||
next({ name: 'start' })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: "/account",
|
||||
name: "account",
|
||||
component: () => import("../views/AccountViewView.vue"),
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
component: () => import('../views/AccountViewView.vue')
|
||||
},
|
||||
{
|
||||
path: "/claim/:id?",
|
||||
name: "claim",
|
||||
component: () => import("../views/ClaimView.vue"),
|
||||
path: '/claim/:id?',
|
||||
name: 'claim',
|
||||
component: () => import('../views/ClaimView.vue')
|
||||
},
|
||||
{
|
||||
path: "/claim-add-raw/:id?",
|
||||
name: "claim-add-raw",
|
||||
component: () => import("../views/ClaimAddRawView.vue"),
|
||||
path: '/claim-add-raw/:id?',
|
||||
name: 'claim-add-raw',
|
||||
component: () => import('../views/ClaimAddRawView.vue')
|
||||
},
|
||||
{
|
||||
path: "/claim-cert/:id",
|
||||
name: "claim-cert",
|
||||
component: () => import("../views/ClaimCertificateView.vue"),
|
||||
path: '/claim-cert/:id',
|
||||
name: 'claim-cert',
|
||||
component: () => import('../views/ClaimCertificateView.vue')
|
||||
},
|
||||
{
|
||||
path: "/confirm-contact",
|
||||
name: "confirm-contact",
|
||||
component: () => import("../views/ConfirmContactView.vue"),
|
||||
path: '/confirm-contact',
|
||||
name: 'confirm-contact',
|
||||
component: () => import('../views/ConfirmContactView.vue')
|
||||
},
|
||||
{
|
||||
path: "/confirm-gift/:id?",
|
||||
name: "confirm-gift",
|
||||
component: () => import("../views/ConfirmGiftView.vue"),
|
||||
path: '/confirm-gift/:id?',
|
||||
name: 'confirm-gift',
|
||||
component: () => import('../views/ConfirmGiftView.vue')
|
||||
},
|
||||
{
|
||||
path: "/contact-amounts",
|
||||
name: "contact-amounts",
|
||||
component: () => import("../views/ContactAmountsView.vue"),
|
||||
path: '/contact-amounts',
|
||||
name: 'contact-amounts',
|
||||
component: () => import('../views/ContactAmountsView.vue')
|
||||
},
|
||||
{
|
||||
path: "/contact-edit/:did",
|
||||
name: "contact-edit",
|
||||
component: () => import("../views/ContactEditView.vue"),
|
||||
path: '/contact-edit/:did',
|
||||
name: 'contact-edit',
|
||||
component: () => import('../views/ContactEditView.vue')
|
||||
},
|
||||
{
|
||||
path: "/contact-gift",
|
||||
name: "contact-gift",
|
||||
component: () => import("../views/ContactGiftingView.vue"),
|
||||
path: '/contact-gift',
|
||||
name: 'contact-gift',
|
||||
component: () => import('../views/ContactGiftingView.vue')
|
||||
},
|
||||
{
|
||||
path: "/contact-import/:jwt?",
|
||||
name: "contact-import",
|
||||
component: () => import("../views/ContactImportView.vue"),
|
||||
path: '/contact-import/:jwt?',
|
||||
name: 'contact-import',
|
||||
component: () => import('../views/ContactImportView.vue')
|
||||
},
|
||||
{
|
||||
path: "/contact-qr",
|
||||
name: "contact-qr",
|
||||
component: () => import("../views/ContactQRScanShowView.vue"),
|
||||
path: '/contact-qr',
|
||||
name: 'contact-qr',
|
||||
component: () => import('../views/ContactQRScanShowView.vue')
|
||||
},
|
||||
{
|
||||
path: "/contacts",
|
||||
name: "contacts",
|
||||
component: () => import("../views/ContactsView.vue"),
|
||||
path: '/contacts',
|
||||
name: 'contacts',
|
||||
component: () => import('../views/ContactsView.vue')
|
||||
},
|
||||
{
|
||||
path: "/did/:did?",
|
||||
name: "did",
|
||||
component: () => import("../views/DIDView.vue"),
|
||||
path: '/did/:did?',
|
||||
name: 'did',
|
||||
component: () => import('../views/DIDView.vue')
|
||||
},
|
||||
{
|
||||
path: "/discover",
|
||||
name: "discover",
|
||||
component: () => import("../views/DiscoverView.vue"),
|
||||
path: '/discover',
|
||||
name: 'discover',
|
||||
component: () => import('../views/DiscoverView.vue')
|
||||
},
|
||||
{
|
||||
path: "/gifted-details",
|
||||
name: "gifted-details",
|
||||
component: () => import("../views/GiftedDetailsView.vue"),
|
||||
path: '/gifted-details',
|
||||
name: 'gifted-details',
|
||||
component: () => import('../views/GiftedDetailsView.vue')
|
||||
},
|
||||
{
|
||||
path: "/help",
|
||||
name: "help",
|
||||
component: () => import("../views/HelpView.vue"),
|
||||
path: '/help',
|
||||
name: 'help',
|
||||
component: () => import('../views/HelpView.vue')
|
||||
},
|
||||
{
|
||||
path: "/help-notifications",
|
||||
name: "help-notifications",
|
||||
component: () => import("../views/HelpNotificationsView.vue"),
|
||||
path: '/help-notifications',
|
||||
name: 'help-notifications',
|
||||
component: () => import('../views/HelpNotificationsView.vue')
|
||||
},
|
||||
{
|
||||
path: "/help-notification-types",
|
||||
name: "help-notification-types",
|
||||
component: () => import("../views/HelpNotificationTypesView.vue"),
|
||||
path: '/help-notification-types',
|
||||
name: 'help-notification-types',
|
||||
component: () => import('../views/HelpNotificationTypesView.vue')
|
||||
},
|
||||
{
|
||||
path: "/help-onboarding",
|
||||
name: "help-onboarding",
|
||||
component: () => import("../views/HelpOnboardingView.vue"),
|
||||
path: '/help-onboarding',
|
||||
name: 'help-onboarding',
|
||||
component: () => import('../views/HelpOnboardingView.vue')
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: () => import("../views/HomeView.vue"),
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/HomeView.vue')
|
||||
},
|
||||
{
|
||||
path: "/identity-switcher",
|
||||
name: "identity-switcher",
|
||||
component: () => import("../views/IdentitySwitcherView.vue"),
|
||||
path: '/identity-switcher',
|
||||
name: 'identity-switcher',
|
||||
component: () => import('../views/IdentitySwitcherView.vue')
|
||||
},
|
||||
{
|
||||
path: "/import-account",
|
||||
name: "import-account",
|
||||
component: () => import("../views/ImportAccountView.vue"),
|
||||
path: '/import-account',
|
||||
name: 'import-account',
|
||||
component: () => import('../views/ImportAccountView.vue')
|
||||
},
|
||||
{
|
||||
path: "/import-derive",
|
||||
name: "import-derive",
|
||||
component: () => import("../views/ImportDerivedAccountView.vue"),
|
||||
path: '/import-derive',
|
||||
name: 'import-derive',
|
||||
component: () => import('../views/ImportDerivedAccountView.vue')
|
||||
},
|
||||
{
|
||||
path: "/invite-one",
|
||||
name: "invite-one",
|
||||
component: () => import("../views/InviteOneView.vue"),
|
||||
path: '/invite-one',
|
||||
name: 'invite-one',
|
||||
component: () => import('../views/InviteOneView.vue')
|
||||
},
|
||||
{
|
||||
path: "/invite-one-accept/:jwt?",
|
||||
name: "InviteOneAcceptView",
|
||||
component: () => import("../views/InviteOneAcceptView.vue"),
|
||||
path: '/invite-one-accept/:jwt?',
|
||||
name: 'InviteOneAcceptView',
|
||||
component: () => import('../views/InviteOneAcceptView.vue')
|
||||
},
|
||||
{
|
||||
path: "/logs",
|
||||
name: "logs",
|
||||
component: () => import("../views/LogView.vue"),
|
||||
path: '/logs',
|
||||
name: 'logs',
|
||||
component: () => import('../views/LogView.vue')
|
||||
},
|
||||
{
|
||||
path: "/new-activity",
|
||||
name: "new-activity",
|
||||
component: () => import("../views/NewActivityView.vue"),
|
||||
path: '/new-activity',
|
||||
name: 'new-activity',
|
||||
component: () => import('../views/NewActivityView.vue')
|
||||
},
|
||||
{
|
||||
path: "/new-edit-account",
|
||||
name: "new-edit-account",
|
||||
component: () => import("../views/NewEditAccountView.vue"),
|
||||
path: '/new-edit-account',
|
||||
name: 'new-edit-account',
|
||||
component: () => import('../views/NewEditAccountView.vue')
|
||||
},
|
||||
{
|
||||
path: "/new-edit-project",
|
||||
name: "new-edit-project",
|
||||
component: () => import("../views/NewEditProjectView.vue"),
|
||||
path: '/new-edit-project',
|
||||
name: 'new-edit-project',
|
||||
component: () => import('../views/NewEditProjectView.vue')
|
||||
},
|
||||
{
|
||||
path: "/new-identifier",
|
||||
name: "new-identifier",
|
||||
component: () => import("../views/NewIdentifierView.vue"),
|
||||
path: '/new-identifier',
|
||||
name: 'new-identifier',
|
||||
component: () => import('../views/NewIdentifierView.vue')
|
||||
},
|
||||
{
|
||||
path: "/offer-details/:id?",
|
||||
name: "offer-details",
|
||||
component: () => import("../views/OfferDetailsView.vue"),
|
||||
path: '/offer-details/:id?',
|
||||
name: 'offer-details',
|
||||
component: () => import('../views/OfferDetailsView.vue')
|
||||
},
|
||||
{
|
||||
path: "/onboard-meeting-list",
|
||||
name: "onboard-meeting-list",
|
||||
component: () => import("../views/OnboardMeetingListView.vue"),
|
||||
path: '/onboard-meeting-list',
|
||||
name: 'onboard-meeting-list',
|
||||
component: () => import('../views/OnboardMeetingListView.vue')
|
||||
},
|
||||
{
|
||||
path: "/onboard-meeting-members/:groupId",
|
||||
name: "onboard-meeting-members",
|
||||
component: () => import("../views/OnboardMeetingMembersView.vue"),
|
||||
path: '/onboard-meeting-members/:groupId',
|
||||
name: 'onboard-meeting-members',
|
||||
component: () => import('../views/OnboardMeetingMembersView.vue')
|
||||
},
|
||||
{
|
||||
path: "/onboard-meeting-setup",
|
||||
name: "onboard-meeting-setup",
|
||||
component: () => import("../views/OnboardMeetingSetupView.vue"),
|
||||
path: '/onboard-meeting-setup',
|
||||
name: 'onboard-meeting-setup',
|
||||
component: () => import('../views/OnboardMeetingSetupView.vue')
|
||||
},
|
||||
{
|
||||
path: "/project/:id?",
|
||||
name: "project",
|
||||
component: () => import("../views/ProjectViewView.vue"),
|
||||
path: '/project/:id?',
|
||||
name: 'project',
|
||||
component: () => import('../views/ProjectViewView.vue')
|
||||
},
|
||||
{
|
||||
path: "/projects",
|
||||
name: "projects",
|
||||
component: () => import("../views/ProjectsView.vue"),
|
||||
beforeEnter: enterOrStart,
|
||||
path: '/projects',
|
||||
name: 'projects',
|
||||
component: () => import('../views/ProjectsView.vue'),
|
||||
beforeEnter: enterOrStart
|
||||
},
|
||||
{
|
||||
path: "/quick-action-bvc",
|
||||
name: "quick-action-bvc",
|
||||
component: () => import("../views/QuickActionBvcView.vue"),
|
||||
path: '/quick-action-bvc',
|
||||
name: 'quick-action-bvc',
|
||||
component: () => import('../views/QuickActionBvcView.vue')
|
||||
},
|
||||
{
|
||||
path: "/quick-action-bvc-begin",
|
||||
name: "quick-action-bvc-begin",
|
||||
component: () => import("../views/QuickActionBvcBeginView.vue"),
|
||||
path: '/quick-action-bvc-begin',
|
||||
name: 'quick-action-bvc-begin',
|
||||
component: () => import('../views/QuickActionBvcBeginView.vue')
|
||||
},
|
||||
{
|
||||
path: "/quick-action-bvc-end",
|
||||
name: "quick-action-bvc-end",
|
||||
component: () => import("../views/QuickActionBvcEndView.vue"),
|
||||
path: '/quick-action-bvc-end',
|
||||
name: 'quick-action-bvc-end',
|
||||
component: () => import('../views/QuickActionBvcEndView.vue')
|
||||
},
|
||||
{
|
||||
path: "/recent-offers-to-user",
|
||||
name: "recent-offers-to-user",
|
||||
component: () => import("../views/RecentOffersToUserView.vue"),
|
||||
path: '/recent-offers-to-user',
|
||||
name: 'recent-offers-to-user',
|
||||
component: () => import('../views/RecentOffersToUserView.vue')
|
||||
},
|
||||
{
|
||||
path: "/recent-offers-to-user-projects",
|
||||
name: "recent-offers-to-user-projects",
|
||||
component: () => import("../views/RecentOffersToUserProjectsView.vue"),
|
||||
path: '/recent-offers-to-user-projects',
|
||||
name: 'recent-offers-to-user-projects',
|
||||
component: () => import('../views/RecentOffersToUserProjectsView.vue')
|
||||
},
|
||||
{
|
||||
path: "/scan-contact",
|
||||
name: "scan-contact",
|
||||
component: () => import("../views/ContactScanView.vue"),
|
||||
path: '/scan-contact',
|
||||
name: 'scan-contact',
|
||||
component: () => import('../views/ContactScanView.vue')
|
||||
},
|
||||
{
|
||||
path: "/search-area",
|
||||
name: "search-area",
|
||||
component: () => import("../views/SearchAreaView.vue"),
|
||||
path: '/search-area',
|
||||
name: 'search-area',
|
||||
component: () => import('../views/SearchAreaView.vue')
|
||||
},
|
||||
{
|
||||
path: "/seed-backup",
|
||||
name: "seed-backup",
|
||||
component: () => import("../views/SeedBackupView.vue"),
|
||||
path: '/seed-backup',
|
||||
name: 'seed-backup',
|
||||
component: () => import('../views/SeedBackupView.vue')
|
||||
},
|
||||
{
|
||||
path: "/share-my-contact-info",
|
||||
name: "share-my-contact-info",
|
||||
component: () => import("../views/ShareMyContactInfoView.vue"),
|
||||
path: '/share-my-contact-info',
|
||||
name: 'share-my-contact-info',
|
||||
component: () => import('../views/ShareMyContactInfoView.vue')
|
||||
},
|
||||
{
|
||||
path: "/shared-photo",
|
||||
name: "shared-photo",
|
||||
component: () => import("../views/SharedPhotoView.vue"),
|
||||
path: '/shared-photo',
|
||||
name: 'shared-photo',
|
||||
component: () => import('../views/SharedPhotoView.vue')
|
||||
},
|
||||
|
||||
// /share-target is also an endpoint in the service worker
|
||||
|
||||
{
|
||||
path: "/start",
|
||||
name: "start",
|
||||
component: () => import("../views/StartView.vue"),
|
||||
path: '/start',
|
||||
name: 'start',
|
||||
component: () => import('../views/StartView.vue')
|
||||
},
|
||||
{
|
||||
path: "/statistics",
|
||||
name: "statistics",
|
||||
component: () => import("../views/StatisticsView.vue"),
|
||||
path: '/statistics',
|
||||
name: 'statistics',
|
||||
component: () => import('../views/StatisticsView.vue')
|
||||
},
|
||||
{
|
||||
path: "/test",
|
||||
name: "test",
|
||||
component: () => import("../views/TestView.vue"),
|
||||
path: '/test',
|
||||
name: 'test',
|
||||
component: () => import('../views/TestView.vue')
|
||||
},
|
||||
{
|
||||
path: "/user-profile/:id?",
|
||||
name: "user-profile",
|
||||
component: () => import("../views/UserProfileView.vue"),
|
||||
path: '/user-profile/:id?',
|
||||
name: 'user-profile',
|
||||
component: () => import('../views/UserProfileView.vue')
|
||||
},
|
||||
{
|
||||
path: "/deep-link-error",
|
||||
name: "deep-link-error",
|
||||
component: () => import("../views/DeepLinkErrorView.vue"),
|
||||
path: '/deep-link-error',
|
||||
name: 'deep-link-error',
|
||||
component: () => import('../views/DeepLinkErrorView.vue'),
|
||||
meta: {
|
||||
title: "Invalid Deep Link",
|
||||
requiresAuth: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
title: 'Invalid Deep Link',
|
||||
requiresAuth: false
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const isElectron = window.location.protocol === "file:";
|
||||
const isElectron = window.location.protocol === 'file:'
|
||||
const initialPath = isElectron
|
||||
? window.location.pathname.split("/dist-electron/www/")[1] || "/"
|
||||
: window.location.pathname;
|
||||
? window.location.pathname.split('/dist-electron/www/')[1] || '/'
|
||||
: window.location.pathname
|
||||
|
||||
const history = isElectron
|
||||
? createMemoryHistory() // Memory history for Electron
|
||||
: createWebHistory("/"); // Add base path for web apps
|
||||
: createWebHistory('/') // Add base path for web apps
|
||||
|
||||
/** @type {*} */
|
||||
const router = createRouter({
|
||||
history,
|
||||
routes,
|
||||
});
|
||||
routes
|
||||
})
|
||||
|
||||
// Replace initial URL to start at `/` if necessary
|
||||
router.replace(initialPath || "/");
|
||||
router.replace(initialPath || '/')
|
||||
|
||||
const errorHandler = (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: any,
|
||||
to: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized,
|
||||
from: RouteLocationNormalized
|
||||
) => {
|
||||
// Handle the error here
|
||||
logger.error("Caught in top level error handler:", error, to, from);
|
||||
alert("Something is very wrong. Try reloading or restarting the app.");
|
||||
logger.error('Caught in top level error handler:', error, to, from)
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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) => {
|
||||
// console.log("Navigating to view:", to.name);
|
||||
@@ -336,4 +336,4 @@ router.onError(errorHandler); // Assign the error handler to the router instance
|
||||
// next();
|
||||
// });
|
||||
|
||||
export default router;
|
||||
export default router
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
*/
|
||||
export interface ImageResult {
|
||||
/** The image data as a Blob object */
|
||||
blob: Blob;
|
||||
blob: Blob
|
||||
/** The filename associated with the image */
|
||||
fileName: string;
|
||||
fileName: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15,17 +15,17 @@ export interface ImageResult {
|
||||
*/
|
||||
export interface PlatformCapabilities {
|
||||
/** Whether the platform supports native file system access */
|
||||
hasFileSystem: boolean;
|
||||
hasFileSystem: boolean
|
||||
/** Whether the platform supports native camera access */
|
||||
hasCamera: boolean;
|
||||
hasCamera: boolean
|
||||
/** Whether the platform is a mobile device */
|
||||
isMobile: boolean;
|
||||
isMobile: boolean
|
||||
/** Whether the platform is iOS specifically */
|
||||
isIOS: boolean;
|
||||
isIOS: boolean
|
||||
/** Whether the platform supports native file download */
|
||||
hasFileDownload: boolean;
|
||||
hasFileDownload: boolean
|
||||
/** 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
|
||||
* @returns Object describing what features are available on this platform
|
||||
*/
|
||||
getCapabilities(): PlatformCapabilities;
|
||||
getCapabilities(): PlatformCapabilities
|
||||
|
||||
// File system operations
|
||||
/**
|
||||
@@ -47,7 +47,7 @@ export interface PlatformService {
|
||||
* @param path - The path to the file to read
|
||||
* @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.
|
||||
@@ -55,7 +55,7 @@ export interface PlatformService {
|
||||
* @param content - The content to write to the file
|
||||
* @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.
|
||||
@@ -63,41 +63,41 @@ export interface PlatformService {
|
||||
* @param content - The content to write to the file
|
||||
* @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.
|
||||
* @param path - The path to the file to delete
|
||||
* @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.
|
||||
* @param directory - The directory path to list
|
||||
* @returns Promise resolving to an array of filenames
|
||||
*/
|
||||
listFiles(directory: string): Promise<string[]>;
|
||||
listFiles(directory: string): Promise<string[]>
|
||||
|
||||
// Camera operations
|
||||
/**
|
||||
* Activates the device camera to take a picture.
|
||||
* @returns Promise resolving to the captured image result
|
||||
*/
|
||||
takePicture(): Promise<ImageResult>;
|
||||
takePicture(): Promise<ImageResult>
|
||||
|
||||
/**
|
||||
* Opens a file picker to select an existing image.
|
||||
* @returns Promise resolving to the selected image result
|
||||
*/
|
||||
pickImage(): Promise<ImageResult>;
|
||||
pickImage(): Promise<ImageResult>
|
||||
|
||||
/**
|
||||
* Handles deep link URLs for the application.
|
||||
* @param url - The deep link URL to handle
|
||||
* @returns Promise that resolves when the deep link has been handled
|
||||
*/
|
||||
handleDeepLink(url: string): Promise<void>;
|
||||
handleDeepLink(url: string): Promise<void>
|
||||
|
||||
// Clipboard operations
|
||||
/**
|
||||
@@ -105,11 +105,11 @@ export interface PlatformService {
|
||||
* @param text - The text to write to the clipboard
|
||||
* @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.
|
||||
* @returns Promise resolving to the clipboard text
|
||||
*/
|
||||
readFromClipboard(): Promise<string>;
|
||||
readFromClipboard(): Promise<string>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { PlatformService } from "./PlatformService";
|
||||
import { WebPlatformService } from "./platforms/WebPlatformService";
|
||||
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
||||
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
||||
import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
|
||||
import { PlatformService } from './PlatformService'
|
||||
import { WebPlatformService } from './platforms/WebPlatformService'
|
||||
import { CapacitorPlatformService } from './platforms/CapacitorPlatformService'
|
||||
import { ElectronPlatformService } from './platforms/ElectronPlatformService'
|
||||
import { PyWebViewPlatformService } from './platforms/PyWebViewPlatformService'
|
||||
|
||||
/**
|
||||
* Factory class for creating platform-specific service implementations.
|
||||
@@ -22,7 +22,7 @@ import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
|
||||
* ```
|
||||
*/
|
||||
export class PlatformServiceFactory {
|
||||
private static instance: PlatformService | null = null;
|
||||
private static instance: PlatformService | null = null
|
||||
|
||||
/**
|
||||
* Gets or creates the singleton instance of PlatformService.
|
||||
@@ -32,27 +32,27 @@ export class PlatformServiceFactory {
|
||||
*/
|
||||
public static getInstance(): PlatformService {
|
||||
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) {
|
||||
case "capacitor":
|
||||
PlatformServiceFactory.instance = new CapacitorPlatformService();
|
||||
break;
|
||||
case "electron":
|
||||
PlatformServiceFactory.instance = new ElectronPlatformService();
|
||||
break;
|
||||
case "pywebview":
|
||||
PlatformServiceFactory.instance = new PyWebViewPlatformService();
|
||||
break;
|
||||
case "web":
|
||||
case 'capacitor':
|
||||
PlatformServiceFactory.instance = new CapacitorPlatformService()
|
||||
break
|
||||
case 'electron':
|
||||
PlatformServiceFactory.instance = new ElectronPlatformService()
|
||||
break
|
||||
case 'pywebview':
|
||||
PlatformServiceFactory.instance = new PyWebViewPlatformService()
|
||||
break
|
||||
case 'web':
|
||||
default:
|
||||
PlatformServiceFactory.instance = new WebPlatformService();
|
||||
break;
|
||||
PlatformServiceFactory.instance = new WebPlatformService()
|
||||
break
|
||||
}
|
||||
|
||||
return PlatformServiceFactory.instance;
|
||||
return PlatformServiceFactory.instance
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,154 +2,154 @@ import {
|
||||
BarcodeScanner,
|
||||
BarcodeFormat,
|
||||
LensFacing,
|
||||
ScanResult,
|
||||
} from "@capacitor-mlkit/barcode-scanning";
|
||||
import type { QRScannerService, ScanListener } from "./types";
|
||||
import { logger } from "../../utils/logger";
|
||||
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>> = [];
|
||||
private scanListener: ScanListener | null = null
|
||||
private isScanning = false
|
||||
private listenerHandles: Array<() => Promise<void>> = []
|
||||
|
||||
async checkPermissions() {
|
||||
try {
|
||||
const { camera } = await BarcodeScanner.checkPermissions();
|
||||
return camera === "granted";
|
||||
const { camera } = await BarcodeScanner.checkPermissions()
|
||||
return camera === 'granted'
|
||||
} catch (error) {
|
||||
logger.error("Error checking camera permissions:", error);
|
||||
return false;
|
||||
logger.error('Error checking camera permissions:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async requestPermissions() {
|
||||
try {
|
||||
const { camera } = await BarcodeScanner.requestPermissions();
|
||||
return camera === "granted";
|
||||
const { camera } = await BarcodeScanner.requestPermissions()
|
||||
return camera === 'granted'
|
||||
} catch (error) {
|
||||
logger.error("Error requesting camera permissions:", error);
|
||||
return false;
|
||||
logger.error('Error requesting camera permissions:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async isSupported() {
|
||||
try {
|
||||
const { supported } = await BarcodeScanner.isSupported();
|
||||
return supported;
|
||||
const { supported } = await BarcodeScanner.isSupported()
|
||||
return supported
|
||||
} catch (error) {
|
||||
logger.error("Error checking barcode scanner support:", error);
|
||||
return false;
|
||||
logger.error('Error checking barcode scanner support:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async startScan() {
|
||||
if (this.isScanning) {
|
||||
logger.warn("Scanner is already active");
|
||||
return;
|
||||
logger.warn('Scanner is already active')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// First register listeners before starting scan
|
||||
await this.registerListeners();
|
||||
await this.registerListeners()
|
||||
|
||||
this.isScanning = true;
|
||||
this.isScanning = true
|
||||
await BarcodeScanner.startScan({
|
||||
formats: [BarcodeFormat.QrCode],
|
||||
lensFacing: LensFacing.Back,
|
||||
});
|
||||
lensFacing: LensFacing.Back
|
||||
})
|
||||
} catch (error) {
|
||||
// Ensure cleanup on error
|
||||
this.isScanning = false;
|
||||
await this.removeListeners();
|
||||
logger.error("Error starting barcode scan:", error);
|
||||
throw error;
|
||||
this.isScanning = false
|
||||
await this.removeListeners()
|
||||
logger.error('Error starting barcode scan:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async stopScan() {
|
||||
if (!this.isScanning) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// First stop the scan
|
||||
await BarcodeScanner.stopScan();
|
||||
await BarcodeScanner.stopScan()
|
||||
} catch (error) {
|
||||
logger.error("Error stopping barcode scan:", error);
|
||||
logger.error('Error stopping barcode scan:', error)
|
||||
} finally {
|
||||
// Always cleanup state even if stop fails
|
||||
this.isScanning = false;
|
||||
await this.removeListeners();
|
||||
this.isScanning = false
|
||||
await this.removeListeners()
|
||||
}
|
||||
}
|
||||
|
||||
private async registerListeners() {
|
||||
// Clear any existing listeners first
|
||||
await this.removeListeners();
|
||||
await this.removeListeners()
|
||||
|
||||
const scanHandle = await BarcodeScanner.addListener(
|
||||
"barcodesScanned",
|
||||
'barcodesScanned',
|
||||
(result: ScanResult) => {
|
||||
if (result.barcodes.length > 0) {
|
||||
const barcode = result.barcodes[0];
|
||||
const barcode = result.barcodes[0]
|
||||
if (barcode.rawValue && this.scanListener) {
|
||||
this.scanListener.onScan(barcode.rawValue);
|
||||
this.scanListener.onScan(barcode.rawValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
const errorHandle = await BarcodeScanner.addListener(
|
||||
"scanError",
|
||||
'scanError',
|
||||
(error) => {
|
||||
logger.error("Scan error:", error);
|
||||
logger.error('Scan error:', error)
|
||||
if (this.scanListener?.onError) {
|
||||
this.scanListener.onError(
|
||||
new Error(error.message || "Unknown scan error"),
|
||||
);
|
||||
new Error(error.message || 'Unknown scan error')
|
||||
)
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
this.listenerHandles.push(
|
||||
async () => await scanHandle.remove(),
|
||||
async () => await errorHandle.remove(),
|
||||
);
|
||||
async () => await errorHandle.remove()
|
||||
)
|
||||
}
|
||||
|
||||
private async removeListeners() {
|
||||
try {
|
||||
// Remove all registered listener handles
|
||||
await Promise.all(this.listenerHandles.map((handle) => handle()));
|
||||
this.listenerHandles = [];
|
||||
await Promise.all(this.listenerHandles.map((handle) => handle()))
|
||||
this.listenerHandles = []
|
||||
} catch (error) {
|
||||
logger.error("Error removing listeners:", error);
|
||||
logger.error('Error removing listeners:', error)
|
||||
}
|
||||
}
|
||||
|
||||
addListener(listener: ScanListener) {
|
||||
if (this.scanListener) {
|
||||
logger.warn("Scanner listener already exists, removing old listener");
|
||||
this.cleanup();
|
||||
logger.warn('Scanner listener already exists, removing old listener')
|
||||
this.cleanup()
|
||||
}
|
||||
this.scanListener = listener;
|
||||
this.scanListener = listener
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
try {
|
||||
// Stop scan first if active
|
||||
if (this.isScanning) {
|
||||
await this.stopScan();
|
||||
await this.stopScan()
|
||||
}
|
||||
|
||||
// Remove listeners
|
||||
await this.removeListeners();
|
||||
await this.removeListeners()
|
||||
|
||||
// Clear state
|
||||
this.scanListener = null;
|
||||
this.isScanning = false;
|
||||
this.scanListener = null
|
||||
this.isScanning = false
|
||||
} catch (error) {
|
||||
logger.error("Error during cleanup:", error);
|
||||
logger.error('Error during cleanup:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +1,88 @@
|
||||
import {
|
||||
BarcodeScanner,
|
||||
BarcodeFormat,
|
||||
LensFacing,
|
||||
} from "@capacitor-mlkit/barcode-scanning";
|
||||
import type { PluginListenerHandle } from "@capacitor/core";
|
||||
import { QRScannerService, ScanListener } from "./types";
|
||||
LensFacing
|
||||
} from '@capacitor-mlkit/barcode-scanning'
|
||||
import type { PluginListenerHandle } from '@capacitor/core'
|
||||
import { QRScannerService, ScanListener } from './types'
|
||||
|
||||
export class NativeQRScanner implements QRScannerService {
|
||||
private scanListener: ScanListener | null = null;
|
||||
private isScanning = false;
|
||||
private listenerHandle: PluginListenerHandle | null = null;
|
||||
private scanListener: ScanListener | null = null
|
||||
private isScanning = false
|
||||
private listenerHandle: PluginListenerHandle | null = null
|
||||
|
||||
async checkPermissions(): Promise<boolean> {
|
||||
const { camera } = await BarcodeScanner.checkPermissions();
|
||||
return camera === "granted";
|
||||
const { camera } = await BarcodeScanner.checkPermissions()
|
||||
return camera === 'granted'
|
||||
}
|
||||
|
||||
async requestPermissions(): Promise<boolean> {
|
||||
const { camera } = await BarcodeScanner.requestPermissions();
|
||||
return camera === "granted";
|
||||
const { camera } = await BarcodeScanner.requestPermissions()
|
||||
return camera === 'granted'
|
||||
}
|
||||
|
||||
async isSupported(): Promise<boolean> {
|
||||
const { supported } = await BarcodeScanner.isSupported();
|
||||
return supported;
|
||||
const { supported } = await BarcodeScanner.isSupported()
|
||||
return supported
|
||||
}
|
||||
|
||||
async startScan(): Promise<void> {
|
||||
if (this.isScanning) {
|
||||
throw new Error("Scanner is already running");
|
||||
throw new Error('Scanner is already running')
|
||||
}
|
||||
|
||||
try {
|
||||
this.isScanning = true;
|
||||
this.isScanning = true
|
||||
await BarcodeScanner.startScan({
|
||||
formats: [BarcodeFormat.QrCode],
|
||||
lensFacing: LensFacing.Back,
|
||||
});
|
||||
lensFacing: LensFacing.Back
|
||||
})
|
||||
|
||||
this.listenerHandle = await BarcodeScanner.addListener(
|
||||
"barcodesScanned",
|
||||
'barcodesScanned',
|
||||
async (result) => {
|
||||
if (result.barcodes.length > 0 && this.scanListener) {
|
||||
const barcode = result.barcodes[0];
|
||||
this.scanListener.onScan(barcode.rawValue);
|
||||
await this.stopScan();
|
||||
const barcode = result.barcodes[0]
|
||||
this.scanListener.onScan(barcode.rawValue)
|
||||
await this.stopScan()
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
this.isScanning = false;
|
||||
this.isScanning = false
|
||||
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> {
|
||||
if (!this.isScanning) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await BarcodeScanner.stopScan();
|
||||
this.isScanning = false;
|
||||
await BarcodeScanner.stopScan()
|
||||
this.isScanning = false
|
||||
} catch (error) {
|
||||
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 {
|
||||
this.scanListener = listener;
|
||||
this.scanListener = listener
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
await this.stopScan();
|
||||
await this.stopScan()
|
||||
if (this.listenerHandle) {
|
||||
await this.listenerHandle.remove();
|
||||
this.listenerHandle = null;
|
||||
await this.listenerHandle.remove()
|
||||
this.listenerHandle = null
|
||||
}
|
||||
this.scanListener = null;
|
||||
this.scanListener = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { CapacitorQRScanner } from "./CapacitorQRScanner";
|
||||
import { WebQRScanner } from "./WebQRScanner";
|
||||
import type { QRScannerService } from "./types";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { CapacitorQRScanner } from './CapacitorQRScanner'
|
||||
import type { QRScannerService } from './types'
|
||||
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 {
|
||||
private static instance: QRScannerService | null = null;
|
||||
private static instance: QRScannerService | null = null
|
||||
|
||||
static getInstance(): QRScannerService {
|
||||
if (!this.instance) {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
logger.log("Creating native QR scanner instance");
|
||||
this.instance = new CapacitorQRScanner();
|
||||
// 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 {
|
||||
logger.log("Creating web QR scanner instance");
|
||||
this.instance = new WebQRScanner();
|
||||
throw new Error(
|
||||
'No QR scanner implementation available for this platform'
|
||||
)
|
||||
}
|
||||
}
|
||||
return this.instance;
|
||||
return this.instance
|
||||
}
|
||||
|
||||
static async cleanup() {
|
||||
if (this.instance) {
|
||||
await this.instance.cleanup();
|
||||
this.instance = null;
|
||||
await this.instance.cleanup()
|
||||
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 {
|
||||
rawValue: string;
|
||||
rawValue: string
|
||||
}
|
||||
|
||||
export interface QRScannerState {
|
||||
isSupported: boolean;
|
||||
granted: boolean;
|
||||
denied: boolean;
|
||||
isProcessing: boolean;
|
||||
processingStatus: string;
|
||||
processingDetails: string;
|
||||
error: string;
|
||||
status: string;
|
||||
isSupported: boolean
|
||||
granted: boolean
|
||||
denied: boolean
|
||||
isProcessing: boolean
|
||||
processingStatus: string
|
||||
processingDetails: string
|
||||
error: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface ScanListener {
|
||||
onScan: (result: string) => void;
|
||||
onError?: (error: Error) => void;
|
||||
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>;
|
||||
checkPermissions(): Promise<boolean>
|
||||
requestPermissions(): Promise<boolean>
|
||||
isSupported(): Promise<boolean>
|
||||
startScan(): Promise<void>
|
||||
stopScan(): Promise<void>
|
||||
addListener(listener: ScanListener): void
|
||||
cleanup(): Promise<void>
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* @module api
|
||||
*/
|
||||
|
||||
import { AxiosError } from "axios";
|
||||
import { logger } from "../utils/logger";
|
||||
import { AxiosError } from 'axios'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
/**
|
||||
* 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) => {
|
||||
if (process.env.VITE_PLATFORM === "capacitor") {
|
||||
if (process.env.VITE_PLATFORM === 'capacitor') {
|
||||
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
@@ -44,16 +44,16 @@ export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||
config: {
|
||||
url: error.config?.url,
|
||||
method: error.config?.method,
|
||||
headers: error.config?.headers,
|
||||
},
|
||||
});
|
||||
headers: error.config?.headers
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Specific handling for rate limits
|
||||
if (error.response?.status === 400) {
|
||||
logger.warn(`[Rate Limit] ${endpoint}`);
|
||||
return null;
|
||||
logger.warn(`[Rate Limit] ${endpoint}`)
|
||||
return null
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -45,29 +45,29 @@
|
||||
* await handler.handleDeepLink("timesafari://claim/123?view=details");
|
||||
*/
|
||||
|
||||
import { Router } from "vue-router";
|
||||
import { Router } from 'vue-router'
|
||||
import {
|
||||
deepLinkSchemas,
|
||||
baseUrlSchema,
|
||||
routeSchema,
|
||||
DeepLinkRoute,
|
||||
} from "../types/deepLinks";
|
||||
import { logConsoleAndDb } from "../db";
|
||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||
DeepLinkRoute
|
||||
} from '../types/deepLinks'
|
||||
import { logConsoleAndDb } from '../db'
|
||||
import type { DeepLinkError } from '../interfaces/deepLinks'
|
||||
|
||||
/**
|
||||
* Handles processing and routing of deep links in the application.
|
||||
* Provides validation, error handling, and routing for deep link URLs.
|
||||
*/
|
||||
export class DeepLinkHandler {
|
||||
private router: Router;
|
||||
private router: Router
|
||||
|
||||
/**
|
||||
* Creates a new DeepLinkHandler instance.
|
||||
* @param router - Vue Router instance for navigation
|
||||
*/
|
||||
constructor(router: Router) {
|
||||
this.router = router;
|
||||
this.router = router
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,33 +79,33 @@ export class DeepLinkHandler {
|
||||
* @returns Parsed URL components (path, params, query)
|
||||
*/
|
||||
private parseDeepLink(url: string) {
|
||||
const parts = url.split("://");
|
||||
const parts = url.split('://')
|
||||
if (parts.length !== 2) {
|
||||
throw { code: "INVALID_URL", message: "Invalid URL format" };
|
||||
throw { code: 'INVALID_URL', message: 'Invalid URL format' }
|
||||
}
|
||||
|
||||
// Validate base URL structure
|
||||
baseUrlSchema.parse({
|
||||
scheme: parts[0],
|
||||
path: parts[1],
|
||||
queryParams: {}, // Will be populated below
|
||||
});
|
||||
queryParams: {} // Will be populated below
|
||||
})
|
||||
|
||||
const [path, queryString] = parts[1].split("?");
|
||||
const [routePath, param] = path.split("/");
|
||||
const [path, queryString] = parts[1].split('?')
|
||||
const [routePath, param] = path.split('/')
|
||||
|
||||
const query: Record<string, string> = {};
|
||||
const query: Record<string, string> = {}
|
||||
if (queryString) {
|
||||
new URLSearchParams(queryString).forEach((value, key) => {
|
||||
query[key] = value;
|
||||
});
|
||||
query[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
path: routePath,
|
||||
params: param ? { id: param } : {},
|
||||
query,
|
||||
};
|
||||
query
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,25 +117,25 @@ export class DeepLinkHandler {
|
||||
*/
|
||||
async handleDeepLink(url: string): Promise<void> {
|
||||
try {
|
||||
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
||||
const { path, params, query } = this.parseDeepLink(url);
|
||||
logConsoleAndDb('[DeepLink] Processing URL: ' + url, false)
|
||||
const { path, params, query } = this.parseDeepLink(url)
|
||||
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
||||
const sanitizedParams = Object.fromEntries(
|
||||
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
||||
);
|
||||
await this.validateAndRoute(path, sanitizedParams, query);
|
||||
Object.entries(params).map(([key, value]) => [key, value ?? ''])
|
||||
)
|
||||
await this.validateAndRoute(path, sanitizedParams, query)
|
||||
} catch (error) {
|
||||
const deepLinkError = error as DeepLinkError;
|
||||
const deepLinkError = error as DeepLinkError
|
||||
logConsoleAndDb(
|
||||
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
||||
true,
|
||||
);
|
||||
true
|
||||
)
|
||||
|
||||
throw {
|
||||
code: deepLinkError.code || "UNKNOWN_ERROR",
|
||||
code: deepLinkError.code || 'UNKNOWN_ERROR',
|
||||
message: deepLinkError.message,
|
||||
details: deepLinkError.details,
|
||||
};
|
||||
details: deepLinkError.details
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,80 +151,80 @@ export class DeepLinkHandler {
|
||||
private async validateAndRoute(
|
||||
path: string,
|
||||
params: Record<string, string>,
|
||||
query: Record<string, string>,
|
||||
query: Record<string, string>
|
||||
): Promise<void> {
|
||||
const routeMap: Record<string, string> = {
|
||||
"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",
|
||||
};
|
||||
'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'
|
||||
}
|
||||
|
||||
// First try to validate the route path
|
||||
let routeName: string;
|
||||
let routeName: string
|
||||
|
||||
try {
|
||||
// Validate route exists
|
||||
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
|
||||
routeName = routeMap[validRoute];
|
||||
const validRoute = routeSchema.parse(path) as DeepLinkRoute
|
||||
routeName = routeMap[validRoute]
|
||||
} catch (error) {
|
||||
// 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
|
||||
await this.router.replace({
|
||||
name: "deep-link-error",
|
||||
name: 'deep-link-error',
|
||||
query: {
|
||||
originalPath: path,
|
||||
errorCode: "INVALID_ROUTE",
|
||||
message: `The link you followed (${path}) is not supported`,
|
||||
},
|
||||
});
|
||||
errorCode: 'INVALID_ROUTE',
|
||||
message: `The link you followed (${path}) is not supported`
|
||||
}
|
||||
})
|
||||
|
||||
throw {
|
||||
code: "INVALID_ROUTE",
|
||||
message: `Unsupported route: ${path}`,
|
||||
};
|
||||
code: 'INVALID_ROUTE',
|
||||
message: `Unsupported route: ${path}`
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with parameter validation as before...
|
||||
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
|
||||
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]
|
||||
|
||||
try {
|
||||
const validatedParams = await schema.parseAsync({
|
||||
...params,
|
||||
...query,
|
||||
});
|
||||
...query
|
||||
})
|
||||
|
||||
await this.router.replace({
|
||||
name: routeName,
|
||||
params: validatedParams,
|
||||
query,
|
||||
});
|
||||
query
|
||||
})
|
||||
} catch (error) {
|
||||
// For parameter validation errors, provide specific error feedback
|
||||
await this.router.replace({
|
||||
name: "deep-link-error",
|
||||
name: 'deep-link-error',
|
||||
query: {
|
||||
originalPath: path,
|
||||
errorCode: "INVALID_PARAMETERS",
|
||||
message: `The link parameters are invalid: ${(error as Error).message}`,
|
||||
},
|
||||
});
|
||||
errorCode: 'INVALID_PARAMETERS',
|
||||
message: `The link parameters are invalid: ${(error as Error).message}`
|
||||
}
|
||||
})
|
||||
|
||||
throw {
|
||||
code: "INVALID_PARAMETERS",
|
||||
code: 'INVALID_PARAMETERS',
|
||||
message: (error as Error).message,
|
||||
details: error,
|
||||
};
|
||||
details: error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* @module plan
|
||||
*/
|
||||
|
||||
import axios from "axios";
|
||||
import { logger } from "../utils/logger";
|
||||
import axios from 'axios'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
/**
|
||||
* Response interface for plan loading operations.
|
||||
@@ -14,13 +14,13 @@ import { logger } from "../utils/logger";
|
||||
*/
|
||||
interface PlanResponse {
|
||||
/** The response data payload */
|
||||
data?: unknown;
|
||||
data?: unknown
|
||||
/** HTTP status code of the response */
|
||||
status?: number;
|
||||
status?: number
|
||||
/** Error message in case of failure */
|
||||
error?: string;
|
||||
error?: string
|
||||
/** Response headers */
|
||||
headers?: unknown;
|
||||
headers?: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,23 +52,23 @@ interface PlanResponse {
|
||||
*/
|
||||
export const loadPlanWithRetry = async (
|
||||
handle: string,
|
||||
retries = 3,
|
||||
retries = 3
|
||||
): Promise<PlanResponse> => {
|
||||
try {
|
||||
logger.log(`[Plan Service] Loading plan ${handle}, attempt 1/${retries}`);
|
||||
logger.log(`[Plan Service] Loading plan ${handle}, attempt 1/${retries}`)
|
||||
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
|
||||
const response = await loadPlan(handle);
|
||||
const response = await loadPlan(handle)
|
||||
logger.log(`[Plan Service] Plan ${handle} loaded successfully:`, {
|
||||
status: response?.status,
|
||||
headers: response?.headers,
|
||||
data: response?.data,
|
||||
});
|
||||
data: response?.data
|
||||
})
|
||||
|
||||
return response;
|
||||
return response
|
||||
} catch (error: unknown) {
|
||||
logger.error(`[Plan Service] Error loading plan ${handle}:`, {
|
||||
message: (error as Error).message,
|
||||
@@ -82,24 +82,24 @@ export const loadPlanWithRetry = async (
|
||||
url: (error as { config?: { url?: string } })?.config?.url,
|
||||
method: (error as { config?: { method?: string } })?.config?.method,
|
||||
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) {
|
||||
logger.log(
|
||||
`[Plan Service] Retrying plan ${handle}, ${retries - 1} attempts remaining`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return loadPlanWithRetry(handle, retries - 1);
|
||||
`[Plan Service] Retrying plan ${handle}, ${retries - 1} attempts remaining`
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
return loadPlanWithRetry(handle, retries - 1)
|
||||
}
|
||||
|
||||
return {
|
||||
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.
|
||||
@@ -118,23 +118,23 @@ export const loadPlanWithRetry = async (
|
||||
* - Plans: /api/plans/{handle}
|
||||
*/
|
||||
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/plans/${handle}`;
|
||||
: `/api/plans/${handle}`
|
||||
|
||||
logger.log(`[Plan Service] Using endpoint: ${endpoint}`);
|
||||
logger.log(`[Plan Service] Using endpoint: ${endpoint}`)
|
||||
|
||||
try {
|
||||
const response = await axios.get(endpoint);
|
||||
return response;
|
||||
const response = await axios.get(endpoint)
|
||||
return response
|
||||
} catch (error: unknown) {
|
||||
logger.error(`[Plan Service] API request failed for ${handle}:`, {
|
||||
endpoint,
|
||||
error: (error as Error).message,
|
||||
response: (error as { response?: { data?: unknown } })?.response?.data,
|
||||
});
|
||||
throw error;
|
||||
response: (error as { response?: { data?: unknown } })?.response?.data
|
||||
})
|
||||
throw error
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
ImageResult,
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} from "../PlatformService";
|
||||
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
||||
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
|
||||
import { Share } from "@capacitor/share";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { Clipboard } from "@capacitor/clipboard";
|
||||
PlatformCapabilities
|
||||
} from '../PlatformService'
|
||||
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem'
|
||||
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'
|
||||
import { Share } from '@capacitor/share'
|
||||
import { logger } from '../../utils/logger'
|
||||
import { Clipboard } from '@capacitor/clipboard'
|
||||
|
||||
/**
|
||||
* Platform service implementation for Capacitor (mobile) platform.
|
||||
@@ -28,8 +28,8 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
isMobile: true,
|
||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
hasFileDownload: false,
|
||||
needsFileHandlingInstructions: true,
|
||||
};
|
||||
needsFileHandlingInstructions: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,102 +40,102 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
private async checkStoragePermissions(): Promise<void> {
|
||||
try {
|
||||
const logData = {
|
||||
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
platform: this.getCapabilities().isIOS ? 'iOS' : 'Android',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
logger.log(
|
||||
"Checking storage permissions",
|
||||
JSON.stringify(logData, null, 2),
|
||||
);
|
||||
'Checking storage permissions',
|
||||
JSON.stringify(logData, null, 2)
|
||||
)
|
||||
|
||||
if (this.getCapabilities().isIOS) {
|
||||
// iOS uses different permission model
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Try to access a test directory to check permissions
|
||||
try {
|
||||
await Filesystem.stat({
|
||||
path: "/storage/emulated/0/Download",
|
||||
directory: Directory.Documents,
|
||||
});
|
||||
path: '/storage/emulated/0/Download',
|
||||
directory: Directory.Documents
|
||||
})
|
||||
logger.log(
|
||||
"Storage permissions already granted",
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
return;
|
||||
'Storage permissions already granted',
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2)
|
||||
)
|
||||
return
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
const err = error as Error
|
||||
const errorLogData = {
|
||||
error: {
|
||||
message: err.message,
|
||||
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
|
||||
if (err.message === "File does not exist") {
|
||||
if (err.message === 'File does not exist') {
|
||||
logger.log(
|
||||
"Directory does not exist (expected), proceeding with write",
|
||||
JSON.stringify(errorLogData, null, 2),
|
||||
);
|
||||
return;
|
||||
'Directory does not exist (expected), proceeding with write',
|
||||
JSON.stringify(errorLogData, null, 2)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for actual permission errors
|
||||
if (
|
||||
err.message.includes("permission") ||
|
||||
err.message.includes("access")
|
||||
err.message.includes('permission') ||
|
||||
err.message.includes('access')
|
||||
) {
|
||||
logger.log(
|
||||
"Permission check failed, requesting permissions",
|
||||
JSON.stringify(errorLogData, null, 2),
|
||||
);
|
||||
'Permission check failed, requesting permissions',
|
||||
JSON.stringify(errorLogData, null, 2)
|
||||
)
|
||||
|
||||
// The Filesystem plugin will automatically request permissions when needed
|
||||
// We just need to try the operation again
|
||||
try {
|
||||
await Filesystem.stat({
|
||||
path: "/storage/emulated/0/Download",
|
||||
directory: Directory.Documents,
|
||||
});
|
||||
path: '/storage/emulated/0/Download',
|
||||
directory: Directory.Documents
|
||||
})
|
||||
logger.log(
|
||||
"Storage permissions granted after request",
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
return;
|
||||
'Storage permissions granted after request',
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2)
|
||||
)
|
||||
return
|
||||
} catch (retryError: unknown) {
|
||||
const retryErr = retryError as Error;
|
||||
const retryErr = retryError as 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
|
||||
logger.log(
|
||||
"Unexpected error during permission check",
|
||||
JSON.stringify(errorLogData, null, 2),
|
||||
);
|
||||
return;
|
||||
'Unexpected error during permission check',
|
||||
JSON.stringify(errorLogData, null, 2)
|
||||
)
|
||||
return
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
const err = error as Error
|
||||
const errorLogData = {
|
||||
error: {
|
||||
message: err.message,
|
||||
name: err.name,
|
||||
stack: err.stack,
|
||||
stack: err.stack
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
logger.error(
|
||||
"Error checking/requesting permissions",
|
||||
JSON.stringify(errorLogData, null, 2),
|
||||
);
|
||||
throw new Error(`Failed to obtain storage permissions: ${err.message}`);
|
||||
'Error checking/requesting permissions',
|
||||
JSON.stringify(errorLogData, null, 2)
|
||||
)
|
||||
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> {
|
||||
const file = await Filesystem.readFile({
|
||||
path,
|
||||
directory: Directory.Data,
|
||||
});
|
||||
directory: Directory.Data
|
||||
})
|
||||
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 = {
|
||||
targetFileName: fileName,
|
||||
contentLength: content.length,
|
||||
platform: this.getCapabilities().isIOS ? "iOS" : "Android",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
platform: this.getCapabilities().isIOS ? 'iOS' : 'Android',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
logger.log(
|
||||
"Starting writeFile operation",
|
||||
JSON.stringify(logData, null, 2),
|
||||
);
|
||||
'Starting writeFile operation',
|
||||
JSON.stringify(logData, null, 2)
|
||||
)
|
||||
|
||||
// For Android, we need to handle content URIs differently
|
||||
if (this.getCapabilities().isIOS) {
|
||||
@@ -204,44 +204,44 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
path: fileName,
|
||||
data: content,
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
encoding: Encoding.UTF8
|
||||
})
|
||||
|
||||
const writeSuccessLogData = {
|
||||
path: writeResult.uri,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
logger.log(
|
||||
"File write successful",
|
||||
JSON.stringify(writeSuccessLogData, null, 2),
|
||||
);
|
||||
'File write successful',
|
||||
JSON.stringify(writeSuccessLogData, null, 2)
|
||||
)
|
||||
|
||||
// Offer to share the file
|
||||
try {
|
||||
await Share.share({
|
||||
title: "TimeSafari Backup",
|
||||
text: "Here is your TimeSafari backup file.",
|
||||
title: 'TimeSafari Backup',
|
||||
text: 'Here is your TimeSafari backup file.',
|
||||
url: writeResult.uri,
|
||||
dialogTitle: "Share your backup",
|
||||
});
|
||||
dialogTitle: 'Share your backup'
|
||||
})
|
||||
|
||||
logger.log(
|
||||
"Share dialog shown",
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
'Share dialog shown',
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2)
|
||||
)
|
||||
} catch (shareError) {
|
||||
// Log share error but don't fail the operation
|
||||
logger.error(
|
||||
"Share dialog failed",
|
||||
'Share dialog failed',
|
||||
JSON.stringify(
|
||||
{
|
||||
error: shareError,
|
||||
timestamp: new Date().toISOString(),
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
2
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// For Android, first write to app's Documents directory
|
||||
@@ -249,61 +249,61 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
path: fileName,
|
||||
data: content,
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
encoding: Encoding.UTF8
|
||||
})
|
||||
|
||||
const writeSuccessLogData = {
|
||||
path: writeResult.uri,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
logger.log(
|
||||
"File write successful to app storage",
|
||||
JSON.stringify(writeSuccessLogData, null, 2),
|
||||
);
|
||||
'File write successful to app storage',
|
||||
JSON.stringify(writeSuccessLogData, null, 2)
|
||||
)
|
||||
|
||||
// Then share the file to let user choose where to save it
|
||||
try {
|
||||
await Share.share({
|
||||
title: "TimeSafari Backup",
|
||||
text: "Here is your TimeSafari backup file.",
|
||||
title: 'TimeSafari Backup',
|
||||
text: 'Here is your TimeSafari backup file.',
|
||||
url: writeResult.uri,
|
||||
dialogTitle: "Save your backup",
|
||||
});
|
||||
dialogTitle: 'Save your backup'
|
||||
})
|
||||
|
||||
logger.log(
|
||||
"Share dialog shown for Android",
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
'Share dialog shown for Android',
|
||||
JSON.stringify({ timestamp: new Date().toISOString() }, null, 2)
|
||||
)
|
||||
} catch (shareError) {
|
||||
// Log share error but don't fail the operation
|
||||
logger.error(
|
||||
"Share dialog failed for Android",
|
||||
'Share dialog failed for Android',
|
||||
JSON.stringify(
|
||||
{
|
||||
error: shareError,
|
||||
timestamp: new Date().toISOString(),
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
2
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
const err = error as Error
|
||||
const finalErrorLogData = {
|
||||
error: {
|
||||
message: err.message,
|
||||
name: err.name,
|
||||
stack: err.stack,
|
||||
stack: err.stack
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
logger.error(
|
||||
"Error in writeFile operation:",
|
||||
JSON.stringify(finalErrorLogData, null, 2),
|
||||
);
|
||||
throw new Error(`Failed to save file: ${err.message}`);
|
||||
'Error in writeFile operation:',
|
||||
JSON.stringify(finalErrorLogData, null, 2)
|
||||
)
|
||||
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
|
||||
*/
|
||||
async writeAndShareFile(fileName: string, content: string): Promise<void> {
|
||||
const timestamp = new Date().toISOString();
|
||||
const timestamp = new Date().toISOString()
|
||||
const logData = {
|
||||
action: "writeAndShareFile",
|
||||
action: 'writeAndShareFile',
|
||||
fileName,
|
||||
contentLength: content.length,
|
||||
timestamp,
|
||||
};
|
||||
logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
|
||||
timestamp
|
||||
}
|
||||
logger.log('[CapacitorPlatformService]', JSON.stringify(logData, null, 2))
|
||||
|
||||
try {
|
||||
const { uri } = await Filesystem.writeFile({
|
||||
@@ -332,32 +332,32 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
data: content,
|
||||
directory: Directory.Data,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
recursive: true
|
||||
})
|
||||
|
||||
logger.log("[CapacitorPlatformService] File write successful:", {
|
||||
logger.log('[CapacitorPlatformService] File write successful:', {
|
||||
uri,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
await Share.share({
|
||||
title: "TimeSafari Backup",
|
||||
text: "Here is your backup file.",
|
||||
title: 'TimeSafari Backup',
|
||||
text: 'Here is your backup file.',
|
||||
url: uri,
|
||||
dialogTitle: "Share your backup file",
|
||||
});
|
||||
dialogTitle: 'Share your backup file'
|
||||
})
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
const err = error as Error
|
||||
const errLog = {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
logger.error(
|
||||
"[CapacitorPlatformService] Error writing or sharing file:",
|
||||
JSON.stringify(errLog, null, 2),
|
||||
);
|
||||
throw new Error(`Failed to write or share file: ${err.message}`);
|
||||
'[CapacitorPlatformService] Error writing or sharing file:',
|
||||
JSON.stringify(errLog, null, 2)
|
||||
)
|
||||
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> {
|
||||
await Filesystem.deleteFile({
|
||||
path,
|
||||
directory: Directory.Data,
|
||||
});
|
||||
directory: Directory.Data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,11 +382,11 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
async listFiles(directory: string): Promise<string[]> {
|
||||
const result = await Filesystem.readdir({
|
||||
path: directory,
|
||||
directory: Directory.Data,
|
||||
});
|
||||
directory: Directory.Data
|
||||
})
|
||||
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,
|
||||
allowEditing: true,
|
||||
resultType: CameraResultType.Base64,
|
||||
source: CameraSource.Camera,
|
||||
});
|
||||
source: CameraSource.Camera
|
||||
})
|
||||
|
||||
const blob = await this.processImageData(image.base64String);
|
||||
const blob = await this.processImageData(image.base64String)
|
||||
return {
|
||||
blob,
|
||||
fileName: `photo_${Date.now()}.${image.format || "jpg"}`,
|
||||
};
|
||||
fileName: `photo_${Date.now()}.${image.format || 'jpg'}`
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error taking picture with Capacitor:", error);
|
||||
throw new Error("Failed to take picture");
|
||||
logger.error('Error taking picture with Capacitor:', error)
|
||||
throw new Error('Failed to take picture')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,17 +427,17 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
quality: 90,
|
||||
allowEditing: true,
|
||||
resultType: CameraResultType.Base64,
|
||||
source: CameraSource.Photos,
|
||||
});
|
||||
source: CameraSource.Photos
|
||||
})
|
||||
|
||||
const blob = await this.processImageData(image.base64String);
|
||||
const blob = await this.processImageData(image.base64String)
|
||||
return {
|
||||
blob,
|
||||
fileName: `photo_${Date.now()}.${image.format || "jpg"}`,
|
||||
};
|
||||
fileName: `photo_${Date.now()}.${image.format || 'jpg'}`
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error picking image with Capacitor:", error);
|
||||
throw new Error("Failed to pick image");
|
||||
logger.error('Error picking image with Capacitor:', error)
|
||||
throw new Error('Failed to pick image')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,22 +449,22 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
*/
|
||||
private async processImageData(base64String?: string): Promise<Blob> {
|
||||
if (!base64String) {
|
||||
throw new Error("No image data received");
|
||||
throw new Error('No image data received')
|
||||
}
|
||||
|
||||
// Convert base64 to blob
|
||||
const byteCharacters = atob(base64String);
|
||||
const byteArrays = [];
|
||||
const byteCharacters = atob(base64String)
|
||||
const byteArrays = []
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||
const slice = byteCharacters.slice(offset, offset + 512);
|
||||
const byteNumbers = new Array(slice.length);
|
||||
const slice = byteCharacters.slice(offset, offset + 512)
|
||||
const byteNumbers = new Array(slice.length)
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i);
|
||||
byteNumbers[i] = slice.charCodeAt(i)
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
byteArrays.push(byteArray);
|
||||
const byteArray = new Uint8Array(byteNumbers)
|
||||
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> {
|
||||
// Capacitor handles deep links automatically
|
||||
// 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> {
|
||||
try {
|
||||
await Clipboard.write({
|
||||
string: text,
|
||||
});
|
||||
string: text
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error("Error writing to clipboard:", error);
|
||||
throw new Error("Failed to write to clipboard");
|
||||
logger.error('Error writing to clipboard:', error)
|
||||
throw new Error('Failed to write to clipboard')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,11 +500,11 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
*/
|
||||
async readFromClipboard(): Promise<string> {
|
||||
try {
|
||||
const { value } = await Clipboard.read();
|
||||
return value;
|
||||
const { value } = await Clipboard.read()
|
||||
return value
|
||||
} catch (error) {
|
||||
logger.error("Error reading from clipboard:", error);
|
||||
throw new Error("Failed to read from clipboard");
|
||||
logger.error('Error reading from clipboard:', error)
|
||||
throw new Error('Failed to read from clipboard')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
ImageResult,
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} from "../PlatformService";
|
||||
import { logger } from "../../utils/logger";
|
||||
PlatformCapabilities
|
||||
} from '../PlatformService'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
/**
|
||||
* Platform service implementation for Electron (desktop) platform.
|
||||
@@ -29,8 +29,8 @@ export class ElectronPlatformService implements PlatformService {
|
||||
isMobile: false,
|
||||
isIOS: false,
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
async takePicture(): Promise<ImageResult> {
|
||||
logger.error("takePicture not implemented in Electron platform");
|
||||
throw new Error("Not implemented");
|
||||
logger.error('takePicture not implemented in Electron platform')
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,8 +94,8 @@ export class ElectronPlatformService implements PlatformService {
|
||||
* @todo Implement file picker using Electron's dialog API
|
||||
*/
|
||||
async pickImage(): Promise<ImageResult> {
|
||||
logger.error("pickImage not implemented in Electron platform");
|
||||
throw new Error("Not implemented");
|
||||
logger.error('pickImage not implemented in Electron platform')
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +105,7 @@ export class ElectronPlatformService implements PlatformService {
|
||||
* @todo Implement deep link handling using Electron's protocol handler
|
||||
*/
|
||||
async handleDeepLink(_url: string): Promise<void> {
|
||||
logger.error("handleDeepLink not implemented in Electron platform");
|
||||
throw new Error("Not implemented");
|
||||
logger.error('handleDeepLink not implemented in Electron platform')
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
ImageResult,
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} from "../PlatformService";
|
||||
import { logger } from "../../utils/logger";
|
||||
PlatformCapabilities
|
||||
} from '../PlatformService'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
/**
|
||||
* Platform service implementation for PyWebView platform.
|
||||
@@ -30,8 +30,8 @@ export class PyWebViewPlatformService implements PlatformService {
|
||||
isMobile: false,
|
||||
isIOS: false,
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
async takePicture(): Promise<ImageResult> {
|
||||
logger.error("takePicture not implemented in PyWebView platform");
|
||||
throw new Error("Not implemented");
|
||||
logger.error('takePicture not implemented in PyWebView platform')
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,8 +95,8 @@ export class PyWebViewPlatformService implements PlatformService {
|
||||
* @todo Implement file picker using pywebview's file dialog API
|
||||
*/
|
||||
async pickImage(): Promise<ImageResult> {
|
||||
logger.error("pickImage not implemented in PyWebView platform");
|
||||
throw new Error("Not implemented");
|
||||
logger.error('pickImage not implemented in PyWebView platform')
|
||||
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
|
||||
*/
|
||||
async handleDeepLink(_url: string): Promise<void> {
|
||||
logger.error("handleDeepLink not implemented in PyWebView platform");
|
||||
throw new Error("Not implemented");
|
||||
logger.error('handleDeepLink not implemented in PyWebView platform')
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
ImageResult,
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} from "../PlatformService";
|
||||
import { logger } from "../../utils/logger";
|
||||
PlatformCapabilities
|
||||
} from '../PlatformService'
|
||||
import { logger } from '../../utils/logger'
|
||||
|
||||
/**
|
||||
* 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),
|
||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
hasFileDownload: true,
|
||||
needsFileHandlingInstructions: false,
|
||||
};
|
||||
needsFileHandlingInstructions: false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +40,7 @@ export class WebPlatformService implements PlatformService {
|
||||
* @throws Error indicating file system access is not available
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.capture = "environment";
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
input.capture = 'environment'
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (file) {
|
||||
try {
|
||||
const blob = await this.processImageFile(file);
|
||||
const blob = await this.processImageFile(file)
|
||||
resolve({
|
||||
blob,
|
||||
fileName: file.name || "photo.jpg",
|
||||
});
|
||||
fileName: file.name || 'photo.jpg'
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error("Error processing camera image:", error);
|
||||
reject(new Error("Failed to process camera image"));
|
||||
logger.error('Error processing camera image:', error)
|
||||
reject(new Error('Failed to process camera image'))
|
||||
}
|
||||
} 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> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (file) {
|
||||
try {
|
||||
const blob = await this.processImageFile(file);
|
||||
const blob = await this.processImageFile(file)
|
||||
resolve({
|
||||
blob,
|
||||
fileName: file.name || "photo.jpg",
|
||||
});
|
||||
fileName: file.name || 'photo.jpg'
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error("Error processing picked image:", error);
|
||||
reject(new Error("Failed to process picked image"));
|
||||
logger.error('Error processing picked image:', error)
|
||||
reject(new Error('Failed to process picked image'))
|
||||
}
|
||||
} 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> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const dataUrl = event.target?.result as string;
|
||||
const dataUrl = event.target?.result as string
|
||||
// Convert to blob to ensure consistent format
|
||||
fetch(dataUrl)
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => resolve(blob))
|
||||
.catch((error) => {
|
||||
logger.error("Error converting data URL to blob:", error);
|
||||
reject(error);
|
||||
});
|
||||
};
|
||||
logger.error('Error converting data URL to blob:', error)
|
||||
reject(error)
|
||||
})
|
||||
}
|
||||
reader.onerror = (error) => {
|
||||
logger.error("Error reading file:", error);
|
||||
reject(error);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
logger.error('Error reading file:', error)
|
||||
reject(error)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +190,7 @@ export class WebPlatformService implements PlatformService {
|
||||
* @returns false, as this is not Capacitor
|
||||
*/
|
||||
isCapacitor(): boolean {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +198,7 @@ export class WebPlatformService implements PlatformService {
|
||||
* @returns false, as this is not Electron
|
||||
*/
|
||||
isElectron(): boolean {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,7 +206,7 @@ export class WebPlatformService implements PlatformService {
|
||||
* @returns false, as this is not PyWebView
|
||||
*/
|
||||
isPyWebView(): boolean {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,7 +214,7 @@ export class WebPlatformService implements PlatformService {
|
||||
* @returns true, as this is the web implementation
|
||||
*/
|
||||
isWeb(): boolean {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,7 +226,7 @@ export class WebPlatformService implements PlatformService {
|
||||
*/
|
||||
async handleDeepLink(_url: string): Promise<void> {
|
||||
// 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> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch (error) {
|
||||
logger.error("Error writing to clipboard:", error);
|
||||
throw new Error("Failed to write to clipboard");
|
||||
logger.error('Error writing to clipboard:', error)
|
||||
throw new Error('Failed to write to clipboard')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,10 +249,10 @@ export class WebPlatformService implements PlatformService {
|
||||
*/
|
||||
async readFromClipboard(): Promise<string> {
|
||||
try {
|
||||
return await navigator.clipboard.readText();
|
||||
return await navigator.clipboard.readText()
|
||||
} catch (error) {
|
||||
logger.error("Error reading from clipboard:", error);
|
||||
throw new Error("Failed to read from clipboard");
|
||||
logger.error('Error reading from clipboard:', error)
|
||||
throw new Error('Failed to read from clipboard')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
import axios from "axios";
|
||||
import * as didJwt from "did-jwt";
|
||||
import { AppString } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db";
|
||||
import { SERVICE_ID } from "../libs/endorserServer";
|
||||
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
import axios from 'axios'
|
||||
import * as didJwt from 'did-jwt'
|
||||
import { AppString } from '../constants/app'
|
||||
import { retrieveSettingsForActiveAccount } from '../db'
|
||||
import { SERVICE_ID } from '../libs/endorserServer'
|
||||
import { deriveAddress, newIdentifier } from '../libs/crypto'
|
||||
import { logger } from '../utils/logger'
|
||||
/**
|
||||
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
|
||||
*/
|
||||
export async function testServerRegisterUser() {
|
||||
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
|
||||
const vcClaim = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "RegisterAction",
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'RegisterAction',
|
||||
agent: { did: identity0.did },
|
||||
object: SERVICE_ID,
|
||||
participant: { did: settings.activeDid },
|
||||
};
|
||||
participant: { did: settings.activeDid }
|
||||
}
|
||||
// Make a payload for the claim
|
||||
const vcPayload = {
|
||||
sub: "RegisterAction",
|
||||
sub: 'RegisterAction',
|
||||
vc: {
|
||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
||||
type: ["VerifiableCredential"],
|
||||
credentialSubject: vcClaim,
|
||||
},
|
||||
};
|
||||
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
||||
type: ['VerifiableCredential'],
|
||||
credentialSubject: vcClaim
|
||||
}
|
||||
}
|
||||
// create a signature using private key of identity
|
||||
// eslint-disable-next-line
|
||||
const privateKeyHex: string = identity0.keys[0].privateKeyHex!;
|
||||
const signer = await didJwt.SimpleSigner(privateKeyHex);
|
||||
const alg = undefined;
|
||||
const signer = await didJwt.SimpleSigner(privateKeyHex)
|
||||
const alg = undefined
|
||||
// create a JWT for the request
|
||||
const vcJwt: string = await didJwt.createJWT(vcPayload, {
|
||||
alg: alg,
|
||||
issuer: identity0.did,
|
||||
signer: signer,
|
||||
});
|
||||
signer: signer
|
||||
})
|
||||
|
||||
// Make the xhr request payload
|
||||
|
||||
const payload = JSON.stringify({ jwtEncoded: vcJwt });
|
||||
const payload = JSON.stringify({ jwtEncoded: vcJwt })
|
||||
const endorserApiServer =
|
||||
settings.apiServer || AppString.TEST_ENDORSER_API_SERVER;
|
||||
const url = endorserApiServer + "/api/claim";
|
||||
settings.apiServer || AppString.TEST_ENDORSER_API_SERVER
|
||||
const url = endorserApiServer + '/api/claim'
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
const resp = await axios.post(url, payload, { headers });
|
||||
logger.log("User registration result:", resp);
|
||||
const resp = await axios.post(url, payload, { headers })
|
||||
logger.log('User registration result:', resp)
|
||||
}
|
||||
|
||||
@@ -25,79 +25,79 @@
|
||||
* // 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
|
||||
export const VALID_DEEP_LINK_ROUTES = [
|
||||
"user-profile",
|
||||
"project-details",
|
||||
"onboard-meeting-setup",
|
||||
"invite-one-accept",
|
||||
"contact-import",
|
||||
"confirm-gift",
|
||||
"claim",
|
||||
"claim-cert",
|
||||
"claim-add-raw",
|
||||
"contact-edit",
|
||||
"contacts",
|
||||
"did",
|
||||
] as const;
|
||||
'user-profile',
|
||||
'project-details',
|
||||
'onboard-meeting-setup',
|
||||
'invite-one-accept',
|
||||
'contact-import',
|
||||
'confirm-gift',
|
||||
'claim',
|
||||
'claim-cert',
|
||||
'claim-add-raw',
|
||||
'contact-edit',
|
||||
'contacts',
|
||||
'did'
|
||||
] as const
|
||||
|
||||
// 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
|
||||
export const baseUrlSchema = z.object({
|
||||
scheme: z.literal("timesafari"),
|
||||
scheme: z.literal('timesafari'),
|
||||
path: z.string(),
|
||||
queryParams: z.record(z.string()).optional(),
|
||||
});
|
||||
queryParams: z.record(z.string()).optional()
|
||||
})
|
||||
|
||||
// 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
|
||||
export const deepLinkSchemas = {
|
||||
"user-profile": z.object({
|
||||
id: z.string(),
|
||||
'user-profile': z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
"project-details": z.object({
|
||||
id: z.string(),
|
||||
'project-details': z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
"onboard-meeting-setup": z.object({
|
||||
id: z.string(),
|
||||
'onboard-meeting-setup': z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
"invite-one-accept": z.object({
|
||||
id: z.string(),
|
||||
'invite-one-accept': z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
"contact-import": z.object({
|
||||
jwt: z.string(),
|
||||
'contact-import': z.object({
|
||||
jwt: z.string()
|
||||
}),
|
||||
"confirm-gift": z.object({
|
||||
id: z.string(),
|
||||
'confirm-gift': z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
claim: z.object({
|
||||
id: z.string(),
|
||||
id: z.string()
|
||||
}),
|
||||
"claim-cert": z.object({
|
||||
id: z.string(),
|
||||
'claim-cert': z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
"claim-add-raw": z.object({
|
||||
'claim-add-raw': z.object({
|
||||
id: z.string(),
|
||||
claim: z.string().optional(),
|
||||
claimJwtId: z.string().optional(),
|
||||
claimJwtId: z.string().optional()
|
||||
}),
|
||||
"contact-edit": z.object({
|
||||
did: z.string(),
|
||||
'contact-edit': z.object({
|
||||
did: z.string()
|
||||
}),
|
||||
contacts: z.object({
|
||||
contacts: z.string(), // JSON string of contacts array
|
||||
contacts: z.string() // JSON string of contacts array
|
||||
}),
|
||||
did: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
};
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
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