Compare commits
6 Commits
fix-contac
...
db-backup-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a6e5289ff | ||
|
|
6620311b7d | ||
|
|
6e2bdc69e9 | ||
|
|
b8c3517072 | ||
|
|
a0cf9ea721 | ||
|
|
42d706b1fb |
1
.env.electron
Normal file
1
.env.electron
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PLATFORM=electron
|
||||||
5
.env.mobile
Normal file
5
.env.mobile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
PLATFORM=capacitor
|
||||||
|
VITE_ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim
|
||||||
|
VITE_PARTNER_API_URL=https://test-api.partner.ch/api/v2
|
||||||
|
VITE_IMAGE_API_URL=https://test-api.images.ch/api/v2
|
||||||
|
VITE_PUSH_SERVER_URL=https://test-api.push.ch/api/v2
|
||||||
@@ -26,6 +26,10 @@ module.exports = {
|
|||||||
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
|
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"@typescript-eslint/no-unnecessary-type-constraint": "off"
|
"@typescript-eslint/no-unnecessary-type-constraint": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", {
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_"
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
@@ -1,2 +1,2 @@
|
|||||||
#Fri Mar 21 07:27:50 UTC 2025
|
#Thu Apr 03 10:21:42 UTC 2025
|
||||||
gradle.version=8.2.1
|
gradle.version=8.11.1
|
||||||
|
|||||||
Binary file not shown.
@@ -10,6 +10,8 @@ android {
|
|||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
|
implementation project(':capacitor-filesystem')
|
||||||
|
implementation project(':capacitor-share')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,13 @@
|
|||||||
{
|
{
|
||||||
"pkg": "@capacitor/app",
|
"pkg": "@capacitor/app",
|
||||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pkg": "@capacitor/filesystem",
|
||||||
|
"classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pkg": "@capacitor/share",
|
||||||
|
"classpath": "com.capacitorjs.plugins.share.SharePlugin"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<link rel="icon" href="/favicon.ico">
|
<link rel="icon" href="/favicon.ico">
|
||||||
<title>TimeSafari</title>
|
<title>TimeSafari</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CZMUlUNO.js"></script>
|
<script type="module" crossorigin src="/assets/index-KPivi3wg.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ buildscript {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
classpath 'com.android.tools.build:gradle:8.9.1'
|
||||||
classpath 'com.google.gms:google-services:4.4.0'
|
classpath 'com.google.gms:google-services:4.4.0'
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|||||||
@@ -4,3 +4,9 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
|||||||
|
|
||||||
include ':capacitor-app'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
|
include ':capacitor-filesystem'
|
||||||
|
project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android')
|
||||||
|
|
||||||
|
include ':capacitor-share'
|
||||||
|
project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
1455
package-lock.json
generated
1455
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -45,10 +45,13 @@
|
|||||||
"@capacitor/android": "^6.2.0",
|
"@capacitor/android": "^6.2.0",
|
||||||
"@capacitor/app": "^6.0.0",
|
"@capacitor/app": "^6.0.0",
|
||||||
"@capacitor/cli": "^6.2.0",
|
"@capacitor/cli": "^6.2.0",
|
||||||
"@capacitor/core": "^6.2.0",
|
"@capacitor/core": "^6.2.1",
|
||||||
|
"@capacitor/filesystem": "^6.0.3",
|
||||||
"@capacitor/ios": "^6.2.0",
|
"@capacitor/ios": "^6.2.0",
|
||||||
|
"@capacitor/share": "^6.0.3",
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.1",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.1",
|
||||||
|
"@electron/remote": "^2.1.2",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
@@ -98,13 +101,13 @@
|
|||||||
"pinia-plugin-persistedstate": "^3.2.1",
|
"pinia-plugin-persistedstate": "^3.2.1",
|
||||||
"qr-code-generator-vue3": "^1.4.21",
|
"qr-code-generator-vue3": "^1.4.21",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
|
"qrcode.vue": "^3.6.0",
|
||||||
"ramda": "^0.29.1",
|
"ramda": "^0.29.1",
|
||||||
"readable-stream": "^4.5.2",
|
"readable-stream": "^4.5.2",
|
||||||
"reflect-metadata": "^0.1.14",
|
"reflect-metadata": "^0.1.14",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"simple-vue-camera": "^1.1.3",
|
"simple-vue-camera": "^1.1.3",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"stream-browserify": "^3.0.0",
|
|
||||||
"three": "^0.156.1",
|
"three": "^0.156.1",
|
||||||
"ua-parser-js": "^1.0.37",
|
"ua-parser-js": "^1.0.37",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
@@ -123,7 +126,7 @@
|
|||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.17.30",
|
||||||
"@types/node-fetch": "^2.6.12",
|
"@types/node-fetch": "^2.6.12",
|
||||||
"@types/ramda": "^0.29.11",
|
"@types/ramda": "^0.29.11",
|
||||||
"@types/sqlite3": "^3.1.11",
|
"@types/sqlite3": "^3.1.11",
|
||||||
@@ -133,8 +136,12 @@
|
|||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
|
"assert": "^2.1.0",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
|
"browserify-fs": "^1.0.0",
|
||||||
|
"browserify-zlib": "^0.2.0",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
|
"crypto-browserify": "^3.12.1",
|
||||||
"electron": "^33.2.1",
|
"electron": "^33.2.1",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
@@ -142,14 +149,21 @@
|
|||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
"fs-extra": "^11.3.0",
|
"fs-extra": "^11.3.0",
|
||||||
|
"https-browserify": "^1.0.0",
|
||||||
"markdownlint": "^0.37.4",
|
"markdownlint": "^0.37.4",
|
||||||
"markdownlint-cli": "^0.44.0",
|
"markdownlint-cli": "^0.44.0",
|
||||||
"npm-check-updates": "^17.1.13",
|
"npm-check-updates": "^17.1.13",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
|
"stream-http": "^3.2.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
"tty-browserify": "^0.0.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
|
"url": "^0.11.4",
|
||||||
|
"util": "^0.12.5",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-plugin-pwa": "^0.19.8"
|
"vite-plugin-pwa": "^0.19.8"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -69,11 +69,11 @@ export default defineConfig({
|
|||||||
permissions: ["clipboard-read"],
|
permissions: ["clipboard-read"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: 'firefox',
|
// name: 'firefox',
|
||||||
testMatch: /^(?!.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts).+\.spec\.ts$/,
|
// testMatch: /^(?!.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts).+\.spec\.ts$/,
|
||||||
use: { ...devices['Desktop Firefox'] },
|
// use: { ...devices['Desktop Firefox'] },
|
||||||
},
|
// },
|
||||||
|
|
||||||
/* Test against mobile viewports. */
|
/* Test against mobile viewports. */
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ export default defineConfig({
|
|||||||
permissions: ["clipboard-read"],
|
permissions: ["clipboard-read"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: 'firefox',
|
// name: 'firefox',
|
||||||
use: { ...devices['Desktop Firefox'] },
|
// use: { ...devices['Desktop Firefox'] },
|
||||||
},
|
// },
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// name: 'webkit',
|
// name: 'webkit',
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ import ProjectIcon from "./ProjectIcon.vue";
|
|||||||
EntityIcon,
|
EntityIcon,
|
||||||
ProjectIcon,
|
ProjectIcon,
|
||||||
},
|
},
|
||||||
|
emits: ["loadClaim", "viewImage", "cacheImage", "confirmClaim"],
|
||||||
})
|
})
|
||||||
export default class ActivityListItem extends Vue {
|
export default class ActivityListItem extends Vue {
|
||||||
@Prop() record!: GiveRecordWithContactInfo;
|
@Prop() record!: GiveRecordWithContactInfo;
|
||||||
|
|||||||
@@ -1,42 +1,101 @@
|
|||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<div class="w-fit">
|
||||||
<div class="w-fit" v-html="generateIcon()"></div>
|
<img
|
||||||
|
v-if="hasImage"
|
||||||
|
:src="imageUrl"
|
||||||
|
class="rounded cursor-pointer"
|
||||||
|
:width="iconSize"
|
||||||
|
:height="iconSize"
|
||||||
|
@click="handleClick"
|
||||||
|
/>
|
||||||
|
<div v-else class="cursor-pointer" @click="handleClick">
|
||||||
|
<img
|
||||||
|
v-if="!identifier"
|
||||||
|
:src="blankSquareUrl"
|
||||||
|
class="rounded"
|
||||||
|
:width="iconSize"
|
||||||
|
:height="iconSize"
|
||||||
|
/>
|
||||||
|
<svg
|
||||||
|
v-else
|
||||||
|
:width="iconSize"
|
||||||
|
:height="iconSize"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g v-for="(path, index) in avatarPaths" :key="index">
|
||||||
|
<path :d="path" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createAvatar, StyleOptions } from "@dicebear/core";
|
import { createAvatar, StyleOptions } from "@dicebear/core";
|
||||||
import { avataaars } from "@dicebear/collection";
|
import { avataaars } from "@dicebear/collection";
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class EntityIcon extends Vue {
|
export default class EntityIcon extends Vue {
|
||||||
@Prop contact: Contact;
|
@Prop({ required: false }) contact?: Contact;
|
||||||
@Prop entityId = ""; // overridden by contact.did or profileImageUrl
|
@Prop entityId = ""; // overridden by contact.did or profileImageUrl
|
||||||
@Prop iconSize = 0;
|
@Prop iconSize = 0;
|
||||||
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
|
@Prop profileImageUrl = ""; // overridden by contact.profileImageUrl
|
||||||
|
|
||||||
generateIcon() {
|
private avatarPaths: string[] = [];
|
||||||
const imageUrl = this.contact?.profileImageUrl || this.profileImageUrl;
|
private blankSquareUrl =
|
||||||
if (imageUrl) {
|
import.meta.env.VITE_BASE_URL + "assets/blank-square.svg";
|
||||||
return `<img src="${imageUrl}" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
|
|
||||||
} else {
|
get imageUrl(): string {
|
||||||
const identifier = this.contact?.did || this.entityId;
|
return this.contact?.profileImageUrl || this.profileImageUrl;
|
||||||
if (!identifier) {
|
}
|
||||||
return `<img src="../src/assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`;
|
|
||||||
}
|
get hasImage(): boolean {
|
||||||
// https://api.dicebear.com/8.x/avataaars/svg?seed=
|
return !!this.imageUrl;
|
||||||
// ... 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.
|
get identifier(): string | undefined {
|
||||||
const options: StyleOptions<object> = {
|
return this.contact?.did || this.entityId;
|
||||||
seed: (identifier as string) || "",
|
}
|
||||||
size: this.iconSize,
|
|
||||||
};
|
handleClick() {
|
||||||
const avatar = createAvatar(avataaars, options);
|
try {
|
||||||
const svgString = avatar.toString();
|
// Emit a simple event without passing the event object
|
||||||
return svgString;
|
this.$emit("click");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error handling click event:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateAvatarPaths(): string[] {
|
||||||
|
if (!this.identifier) return [];
|
||||||
|
|
||||||
|
const options: StyleOptions<object> = {
|
||||||
|
seed: this.identifier,
|
||||||
|
size: this.iconSize,
|
||||||
|
};
|
||||||
|
const avatar = createAvatar(avataaars, options);
|
||||||
|
const svgString = avatar.toString();
|
||||||
|
|
||||||
|
// Extract paths from SVG string
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(svgString, "image/svg+xml");
|
||||||
|
const paths = Array.from(doc.querySelectorAll("path")).map(
|
||||||
|
(path) => path.getAttribute("d") || "",
|
||||||
|
);
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.avatarPaths = this.generateAvatarPaths();
|
||||||
|
logger.log("EntityIcon mounted, profileImageUrl:", this.profileImageUrl);
|
||||||
|
logger.log("EntityIcon mounted, entityId:", this.entityId);
|
||||||
|
logger.log("EntityIcon mounted, iconSize:", this.iconSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped></style>
|
<style scoped></style>
|
||||||
|
|||||||
257
src/components/ProfileSection.vue
Normal file
257
src/components/ProfileSection.vue
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
/** * @file ProfileSection.vue * @description Component for managing user
|
||||||
|
profile information * @author Matthew Raymer * @version 1.0.0 */
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8">
|
||||||
|
<div v-if="loading" class="text-center mb-2">
|
||||||
|
<font-awesome
|
||||||
|
icon="spinner"
|
||||||
|
class="fa-spin text-slate-400"
|
||||||
|
></font-awesome>
|
||||||
|
Loading profile...
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center mb-2">
|
||||||
|
<span class="font-bold">Public Profile</span>
|
||||||
|
<font-awesome
|
||||||
|
icon="circle-info"
|
||||||
|
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
||||||
|
@click="showProfileInfo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-model="profileDesc"
|
||||||
|
class="w-full h-32 p-2 border border-slate-300 rounded-md"
|
||||||
|
placeholder="Write something about yourself for the public..."
|
||||||
|
:readonly="loading || saving"
|
||||||
|
:class="{ 'bg-slate-100': loading || saving }"
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-4" @click="toggleLocation">
|
||||||
|
<input v-model="includeLocation" type="checkbox" class="mr-2" />
|
||||||
|
<label for="includeLocation">Include Location</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="includeLocation" class="mb-4 aspect-video">
|
||||||
|
<p class="text-sm mb-2 text-slate-500">
|
||||||
|
For your security, choose a location nearby but not exactly at your
|
||||||
|
place.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<l-map
|
||||||
|
ref="profileMap"
|
||||||
|
class="!z-40 rounded-md"
|
||||||
|
@click="handleMapClick"
|
||||||
|
@ready="onMapReady"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
layer-type="base"
|
||||||
|
name="OpenStreetMap"
|
||||||
|
/>
|
||||||
|
<l-marker
|
||||||
|
v-if="latitude && longitude"
|
||||||
|
:lat-lng="[latitude, longitude]"
|
||||||
|
@click="confirmEraseLocation"
|
||||||
|
/>
|
||||||
|
</l-map>
|
||||||
|
</div>
|
||||||
|
<div v-if="!loading && !saving">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<button
|
||||||
|
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||||
|
:disabled="loading || saving"
|
||||||
|
:class="{
|
||||||
|
'opacity-50 cursor-not-allowed': loading || saving,
|
||||||
|
}"
|
||||||
|
@click="saveProfile"
|
||||||
|
>
|
||||||
|
Save Profile
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||||
|
:disabled="loading || saving"
|
||||||
|
:class="{
|
||||||
|
'opacity-50 cursor-not-allowed':
|
||||||
|
loading || saving || (!profileDesc && !includeLocation),
|
||||||
|
}"
|
||||||
|
@click="confirmDeleteProfile"
|
||||||
|
>
|
||||||
|
Delete Profile
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="loading">Loading...</div>
|
||||||
|
<div v-else>Saving...</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||||
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
|
import { ProfileService } from "../services/ProfileService";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
LMap,
|
||||||
|
LMarker,
|
||||||
|
LTileLayer,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class ProfileSection extends Vue {
|
||||||
|
@Prop({ required: true }) activeDid!: string;
|
||||||
|
@Prop({ required: true }) partnerApiServer!: string;
|
||||||
|
|
||||||
|
@Emit("profile-updated") profileUpdated() {}
|
||||||
|
|
||||||
|
loading = true;
|
||||||
|
saving = false;
|
||||||
|
profileDesc = "";
|
||||||
|
latitude = 0;
|
||||||
|
longitude = 0;
|
||||||
|
includeLocation = false;
|
||||||
|
zoom = 2;
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.loadProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadProfile() {
|
||||||
|
try {
|
||||||
|
const profile = await ProfileService.loadProfile(
|
||||||
|
this.activeDid,
|
||||||
|
this.partnerApiServer,
|
||||||
|
);
|
||||||
|
if (profile) {
|
||||||
|
this.profileDesc = profile.description || "";
|
||||||
|
this.latitude = profile.location?.lat || 0;
|
||||||
|
this.longitude = profile.location?.lng || 0;
|
||||||
|
this.includeLocation = !!(this.latitude && this.longitude);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error loading profile:", error);
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Loading Profile",
|
||||||
|
text: "Your server profile is not available.",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveProfile() {
|
||||||
|
this.saving = true;
|
||||||
|
try {
|
||||||
|
await ProfileService.saveProfile(this.activeDid, this.partnerApiServer, {
|
||||||
|
description: this.profileDesc,
|
||||||
|
location: this.includeLocation
|
||||||
|
? {
|
||||||
|
lat: this.latitude,
|
||||||
|
lng: this.longitude,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Profile Saved",
|
||||||
|
text: "Your profile has been updated successfully.",
|
||||||
|
});
|
||||||
|
this.profileUpdated();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error saving profile:", error);
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Saving Profile",
|
||||||
|
text: "There was an error saving your profile.",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLocation() {
|
||||||
|
this.includeLocation = !this.includeLocation;
|
||||||
|
if (!this.includeLocation) {
|
||||||
|
this.latitude = 0;
|
||||||
|
this.longitude = 0;
|
||||||
|
this.zoom = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMapClick(event: { latlng: { lat: number; lng: number } }) {
|
||||||
|
this.latitude = event.latlng.lat;
|
||||||
|
this.longitude = event.latlng.lng;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMapReady(map: L.Map) {
|
||||||
|
const zoom = this.latitude && this.longitude ? 12 : 2;
|
||||||
|
map.setView([this.latitude, this.longitude], zoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmEraseLocation() {
|
||||||
|
this.$notify({
|
||||||
|
group: "modal",
|
||||||
|
type: "confirm",
|
||||||
|
title: "Erase Marker",
|
||||||
|
text: "Are you sure you don't want to mark a location? This will erase the current location.",
|
||||||
|
onYes: () => {
|
||||||
|
this.latitude = 0;
|
||||||
|
this.longitude = 0;
|
||||||
|
this.zoom = 2;
|
||||||
|
this.includeLocation = false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async confirmDeleteProfile() {
|
||||||
|
this.$notify({
|
||||||
|
group: "modal",
|
||||||
|
type: "confirm",
|
||||||
|
title: "Delete Profile",
|
||||||
|
text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.",
|
||||||
|
onYes: this.deleteProfile,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteProfile() {
|
||||||
|
this.saving = true;
|
||||||
|
try {
|
||||||
|
await ProfileService.deleteProfile(this.activeDid, this.partnerApiServer);
|
||||||
|
this.profileDesc = "";
|
||||||
|
this.latitude = 0;
|
||||||
|
this.longitude = 0;
|
||||||
|
this.includeLocation = false;
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Profile Deleted",
|
||||||
|
text: "Your profile has been deleted successfully.",
|
||||||
|
});
|
||||||
|
this.profileUpdated();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error deleting profile:", error);
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Deleting Profile",
|
||||||
|
text: "There was an error deleting your profile.",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showProfileInfo() {
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "info",
|
||||||
|
title: "Public Profile Information",
|
||||||
|
text: "This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import BaseDexie, { Table } from "dexie";
|
import BaseDexie, { Table } from "dexie";
|
||||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||||
|
import { exportDB, ExportOptions } from "dexie-export-import";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
|
import Dexie from "dexie";
|
||||||
|
|
||||||
import { Account, AccountsSchema } from "./tables/accounts";
|
import { Account, AccountsSchema } from "./tables/accounts";
|
||||||
import { Contact, ContactSchema } from "./tables/contacts";
|
import { Contact, ContactSchema } from "./tables/contacts";
|
||||||
@@ -26,19 +28,26 @@ type NonsensitiveTables = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
||||||
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
export type SecretDexie<T extends Record<string, Dexie.Table> = SecretTable> =
|
||||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
BaseDexie & T & { export: (options?: ExportOptions) => Promise<Blob> };
|
||||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
export type SensitiveDexie<
|
||||||
BaseDexie & T;
|
T extends Record<string, Dexie.Table> = SensitiveTables,
|
||||||
|
> = BaseDexie & T & { export: (options?: ExportOptions) => Promise<Blob> };
|
||||||
|
export type NonsensitiveDexie<
|
||||||
|
T extends Record<string, Dexie.Table> = NonsensitiveTables,
|
||||||
|
> = BaseDexie & T & { export: (options?: ExportOptions) => Promise<Blob> };
|
||||||
|
|
||||||
//// Initialize the DBs, starting with the sensitive ones.
|
//// Initialize the DBs, starting with the sensitive ones.
|
||||||
|
|
||||||
// Initialize Dexie database for secret, which is then used to encrypt accountsDB
|
// Initialize Dexie database for secret, which is then used to encrypt accountsDB
|
||||||
export const secretDB = new BaseDexie("TimeSafariSecret") as SecretDexie;
|
export const secretDB = new BaseDexie("TimeSafariSecret") as SecretDexie;
|
||||||
secretDB.version(1).stores(SecretSchema);
|
secretDB.version(1).stores(SecretSchema);
|
||||||
|
secretDB.export = (options) => exportDB(secretDB, options);
|
||||||
|
|
||||||
// Initialize Dexie database for accounts
|
// Initialize Dexie database for accounts
|
||||||
const accountsDexie = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
const accountsDexie = new BaseDexie("TimeSafariAccounts") as SensitiveDexie;
|
||||||
|
accountsDexie.version(1).stores(AccountsSchema);
|
||||||
|
accountsDexie.export = (options) => exportDB(accountsDexie, options);
|
||||||
|
|
||||||
// Instead of accountsDBPromise, use libs/util retrieveAccountMetadata or retrieveFullyDecryptedAccount
|
// Instead of accountsDBPromise, use libs/util retrieveAccountMetadata or retrieveFullyDecryptedAccount
|
||||||
// so that it's clear whether the usage needs the private key inside.
|
// so that it's clear whether the usage needs the private key inside.
|
||||||
@@ -54,8 +63,15 @@ export const accountsDBPromise = useSecretAndInitializeAccountsDB(
|
|||||||
|
|
||||||
//// Now initialize the other DB.
|
//// Now initialize the other DB.
|
||||||
|
|
||||||
// Initialize Dexie databases for non-sensitive data
|
// Initialize Dexie database for non-sensitive data
|
||||||
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie;
|
||||||
|
db.version(1).stores({
|
||||||
|
contacts: ContactSchema.contacts,
|
||||||
|
logs: LogSchema.logs,
|
||||||
|
settings: SettingsSchema.settings,
|
||||||
|
temp: TempSchema.temp,
|
||||||
|
});
|
||||||
|
db.export = (options) => exportDB(db, options);
|
||||||
|
|
||||||
// Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning
|
// Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
export interface GenericVerifiableCredential {
|
export interface GenericVerifiableCredential {
|
||||||
"@context"?: string;
|
"@context"?: string;
|
||||||
"@type": string;
|
"@type": string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
agent?: { identifier: string } | string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
34
src/interfaces/identifier.ts
Normal file
34
src/interfaces/identifier.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Interface for a Decentralized Identifier (DID)
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { KeyMeta } from "../libs/crypto/vc";
|
||||||
|
|
||||||
|
export interface IKey {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
controller: string;
|
||||||
|
ethereumAddress: string;
|
||||||
|
publicKeyHex: string;
|
||||||
|
privateKeyHex: string;
|
||||||
|
meta?: KeyMeta;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IService {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
serviceEndpoint: string;
|
||||||
|
description?: string;
|
||||||
|
metadata?: {
|
||||||
|
version?: string;
|
||||||
|
capabilities?: string[];
|
||||||
|
config?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IIdentifier {
|
||||||
|
did: string;
|
||||||
|
keys: IKey[];
|
||||||
|
services: IService[];
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@ export * from "./limits";
|
|||||||
export * from "./records";
|
export * from "./records";
|
||||||
export * from "./user";
|
export * from "./user";
|
||||||
export * from "./deepLinks";
|
export * from "./deepLinks";
|
||||||
|
export * from "./identifier";
|
||||||
|
|||||||
288
src/interfaces/service.ts
Normal file
288
src/interfaces/service.ts
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
/**
|
||||||
|
* @file service.ts
|
||||||
|
* @description Service interfaces for Decentralized Identifiers (DIDs)
|
||||||
|
*
|
||||||
|
* This module defines the service interfaces used in the TimeSafari application.
|
||||||
|
* Services are associated with DIDs to provide additional functionality and endpoints.
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* 1. Base IService interface defines common service properties
|
||||||
|
* 2. Specialized interfaces extend IService for specific service types
|
||||||
|
* 3. Services are stored in IIdentifier.services array
|
||||||
|
* 4. Services are loaded and managed by PlatformServiceFactory
|
||||||
|
*
|
||||||
|
* Service Types:
|
||||||
|
* - EndorserService: Handles claims and endorsements
|
||||||
|
* - PushNotificationService: Manages web push notifications
|
||||||
|
* - ProfileService: Handles user profiles and settings
|
||||||
|
* - BackupService: Manages data backup and restore
|
||||||
|
*
|
||||||
|
* @see IIdentifier
|
||||||
|
* @see PlatformServiceFactory
|
||||||
|
* @see DatabaseBackupService
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base interface for all DID services
|
||||||
|
*
|
||||||
|
* This interface defines the core properties that all services must implement.
|
||||||
|
* It follows the W3C DID specification for service endpoints.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const service: IService = {
|
||||||
|
* id: 'endorser-service',
|
||||||
|
* type: 'EndorserService',
|
||||||
|
* serviceEndpoint: 'https://api.endorser.ch',
|
||||||
|
* description: 'Endorser service for claims and endorsements',
|
||||||
|
* metadata: {
|
||||||
|
* version: '1.0.0',
|
||||||
|
* capabilities: ['claims', 'endorsements'],
|
||||||
|
* config: { apiServer: 'https://api.endorser.ch' }
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
export interface IService {
|
||||||
|
/**
|
||||||
|
* Unique identifier for the service
|
||||||
|
* @example 'endorser-service'
|
||||||
|
* @example 'push-notification-service'
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of service
|
||||||
|
* @example 'EndorserService'
|
||||||
|
* @example 'PushNotificationService'
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint URL for the service
|
||||||
|
* @example 'https://api.endorser.ch'
|
||||||
|
* @example 'https://push.timesafari.app'
|
||||||
|
*/
|
||||||
|
serviceEndpoint: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional human-readable description of the service
|
||||||
|
* @example 'Service for handling claims and endorsements'
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional metadata for service configuration
|
||||||
|
*/
|
||||||
|
metadata?: {
|
||||||
|
/**
|
||||||
|
* Service version in semantic versioning format
|
||||||
|
* @example '1.0.0'
|
||||||
|
*/
|
||||||
|
version?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of service capabilities
|
||||||
|
* @example ['claims', 'endorsements']
|
||||||
|
* @example ['notifications', 'alerts']
|
||||||
|
*/
|
||||||
|
capabilities?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service-specific configuration
|
||||||
|
* @example { apiServer: 'https://api.endorser.ch' }
|
||||||
|
*/
|
||||||
|
config?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for handling claims and endorsements
|
||||||
|
*
|
||||||
|
* This service provides endpoints for:
|
||||||
|
* - Submitting claims
|
||||||
|
* - Managing endorsements
|
||||||
|
* - Checking rate limits
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const endorserService: IEndorserService = {
|
||||||
|
* id: 'endorser-service',
|
||||||
|
* type: 'EndorserService',
|
||||||
|
* serviceEndpoint: 'https://api.endorser.ch',
|
||||||
|
* metadata: {
|
||||||
|
* version: '1.0.0',
|
||||||
|
* capabilities: ['claims', 'endorsements'],
|
||||||
|
* config: {
|
||||||
|
* apiServer: 'https://api.endorser.ch',
|
||||||
|
* rateLimits: {
|
||||||
|
* claimsPerDay: 100,
|
||||||
|
* endorsementsPerDay: 1000
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
export interface IEndorserService extends IService {
|
||||||
|
/** @override */
|
||||||
|
type: "EndorserService";
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
metadata: {
|
||||||
|
version: string;
|
||||||
|
capabilities: ["claims", "endorsements"];
|
||||||
|
config: {
|
||||||
|
/**
|
||||||
|
* API server URL
|
||||||
|
* @example 'https://api.endorser.ch'
|
||||||
|
*/
|
||||||
|
apiServer: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional rate limits
|
||||||
|
*/
|
||||||
|
rateLimits?: {
|
||||||
|
/**
|
||||||
|
* Maximum claims per day
|
||||||
|
* @default 100
|
||||||
|
*/
|
||||||
|
claimsPerDay: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum endorsements per day
|
||||||
|
* @default 1000
|
||||||
|
*/
|
||||||
|
endorsementsPerDay: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing web push notifications
|
||||||
|
*
|
||||||
|
* This service provides endpoints for:
|
||||||
|
* - Registering push subscriptions
|
||||||
|
* - Sending push notifications
|
||||||
|
* - Managing notification preferences
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const pushService: IPushNotificationService = {
|
||||||
|
* id: 'push-service',
|
||||||
|
* type: 'PushNotificationService',
|
||||||
|
* serviceEndpoint: 'https://push.timesafari.app',
|
||||||
|
* metadata: {
|
||||||
|
* version: '1.0.0',
|
||||||
|
* capabilities: ['notifications'],
|
||||||
|
* config: {
|
||||||
|
* pushServer: 'https://push.timesafari.app',
|
||||||
|
* vapidPublicKey: '...'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
export interface IPushNotificationService extends IService {
|
||||||
|
/** @override */
|
||||||
|
type: "PushNotificationService";
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
metadata: {
|
||||||
|
version: string;
|
||||||
|
capabilities: ["notifications"];
|
||||||
|
config: {
|
||||||
|
/**
|
||||||
|
* Push server URL
|
||||||
|
* @example 'https://push.timesafari.app'
|
||||||
|
*/
|
||||||
|
pushServer: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional VAPID public key for push notifications
|
||||||
|
*/
|
||||||
|
vapidPublicKey?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing user profiles and settings
|
||||||
|
*
|
||||||
|
* This service provides endpoints for:
|
||||||
|
* - Managing user profiles
|
||||||
|
* - Updating user settings
|
||||||
|
* - Retrieving user preferences
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const profileService: IProfileService = {
|
||||||
|
* id: 'profile-service',
|
||||||
|
* type: 'ProfileService',
|
||||||
|
* serviceEndpoint: 'https://partner-api.endorser.ch',
|
||||||
|
* metadata: {
|
||||||
|
* version: '1.0.0',
|
||||||
|
* capabilities: ['profile', 'settings'],
|
||||||
|
* config: {
|
||||||
|
* partnerApiServer: 'https://partner-api.endorser.ch'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
export interface IProfileService extends IService {
|
||||||
|
/** @override */
|
||||||
|
type: "ProfileService";
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
metadata: {
|
||||||
|
version: string;
|
||||||
|
capabilities: ["profile", "settings"];
|
||||||
|
config: {
|
||||||
|
/**
|
||||||
|
* Partner API server URL
|
||||||
|
* @example 'https://partner-api.endorser.ch'
|
||||||
|
*/
|
||||||
|
partnerApiServer: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing data backup and restore operations
|
||||||
|
*
|
||||||
|
* This service provides endpoints for:
|
||||||
|
* - Creating backups
|
||||||
|
* - Restoring from backups
|
||||||
|
* - Managing backup storage
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const backupService: IBackupService = {
|
||||||
|
* id: 'backup-service',
|
||||||
|
* type: 'BackupService',
|
||||||
|
* serviceEndpoint: 'https://backup.timesafari.app',
|
||||||
|
* metadata: {
|
||||||
|
* version: '1.0.0',
|
||||||
|
* capabilities: ['backup', 'restore'],
|
||||||
|
* config: {
|
||||||
|
* storageType: 'cloud',
|
||||||
|
* encryptionKey: '...'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
*/
|
||||||
|
export interface IBackupService extends IService {
|
||||||
|
/** @override */
|
||||||
|
type: "BackupService";
|
||||||
|
|
||||||
|
/** @override */
|
||||||
|
metadata: {
|
||||||
|
version: string;
|
||||||
|
capabilities: ["backup", "restore"];
|
||||||
|
config: {
|
||||||
|
/**
|
||||||
|
* Storage type for backups
|
||||||
|
* @default 'local'
|
||||||
|
*/
|
||||||
|
storageType: "local" | "cloud";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional encryption key for backups
|
||||||
|
*/
|
||||||
|
encryptionKey?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -38,6 +38,10 @@ export interface KeyMeta {
|
|||||||
* The Webauthn credential ID in hex, if this is from a passkey
|
* The Webauthn credential ID in hex, if this is from a passkey
|
||||||
*/
|
*/
|
||||||
passkeyCredIdHex?: string;
|
passkeyCredIdHex?: string;
|
||||||
|
/**
|
||||||
|
* The derivation path for the key
|
||||||
|
*/
|
||||||
|
derivationPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });
|
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ import {
|
|||||||
} from "../interfaces";
|
} from "../interfaces";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
export type { GenericVerifiableCredential, GenericCredWrapper };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard context for schema.org data
|
* Standard context for schema.org data
|
||||||
* @constant {string}
|
* @constant {string}
|
||||||
|
|||||||
67
src/main.ts
67
src/main.ts
@@ -200,14 +200,63 @@ function setupGlobalErrorHandler(app: VueApp) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App);
|
||||||
.component("fa", FontAwesomeIcon)
|
|
||||||
.component("camera", Camera)
|
|
||||||
.use(createPinia())
|
|
||||||
.use(VueAxios, axios)
|
|
||||||
.use(router)
|
|
||||||
.use(Notifications);
|
|
||||||
|
|
||||||
setupGlobalErrorHandler(app);
|
// Add global error handler for component registration
|
||||||
|
app.config.errorHandler = (err, vm, info) => {
|
||||||
|
logger.error("Vue global error:", {
|
||||||
|
error:
|
||||||
|
err instanceof Error
|
||||||
|
? {
|
||||||
|
name: err.name,
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack,
|
||||||
|
}
|
||||||
|
: err,
|
||||||
|
componentName: vm?.$options?.name || "unknown",
|
||||||
|
info,
|
||||||
|
componentData: vm
|
||||||
|
? {
|
||||||
|
hasRouter: !!vm.$router,
|
||||||
|
hasNotify: !!vm.$notify,
|
||||||
|
hasAxios: !!vm.axios,
|
||||||
|
}
|
||||||
|
: "no vm data",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
app.mount("#app");
|
// Register components and plugins with error handling
|
||||||
|
try {
|
||||||
|
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera);
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
app.use(pinia);
|
||||||
|
logger.log("Pinia store initialized");
|
||||||
|
|
||||||
|
app.use(VueAxios, axios);
|
||||||
|
logger.log("Axios initialized");
|
||||||
|
|
||||||
|
app.use(router);
|
||||||
|
logger.log("Router initialized");
|
||||||
|
|
||||||
|
app.use(Notifications);
|
||||||
|
logger.log("Notifications initialized");
|
||||||
|
|
||||||
|
setupGlobalErrorHandler(app);
|
||||||
|
logger.log("Global error handler setup");
|
||||||
|
|
||||||
|
// Mount the app
|
||||||
|
app.mount("#app");
|
||||||
|
logger.log("App mounted successfully");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Critical error during app initialization:", {
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
}
|
||||||
|
: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
82
src/platforms/capacitor/DatabaseBackupService.ts
Normal file
82
src/platforms/capacitor/DatabaseBackupService.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* @file DatabaseBackupService.ts
|
||||||
|
* @description Capacitor-specific implementation of database backup service
|
||||||
|
*
|
||||||
|
* This service handles database backup operations on Capacitor platforms (Android/iOS)
|
||||||
|
* using the Filesystem and Share plugins. It creates a temporary backup file,
|
||||||
|
* writes the backup data to it, and shares the file using the platform's share sheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Filesystem, Directory } from "@capacitor/filesystem";
|
||||||
|
import { Share } from "@capacitor/share";
|
||||||
|
import { DatabaseBackupService as BaseDatabaseBackupService } from "../../services/DatabaseBackupService";
|
||||||
|
import { log, error } from "../../utils/logger";
|
||||||
|
|
||||||
|
export class DatabaseBackupService extends BaseDatabaseBackupService {
|
||||||
|
/**
|
||||||
|
* Handles the backup process for Capacitor platforms
|
||||||
|
*
|
||||||
|
* @param base64Data - Backup data in base64 format
|
||||||
|
* @param arrayBuffer - Backup data as ArrayBuffer
|
||||||
|
* @param blob - Backup data as Blob
|
||||||
|
*/
|
||||||
|
protected async handleBackup(
|
||||||
|
base64Data: string,
|
||||||
|
_arrayBuffer: ArrayBuffer,
|
||||||
|
_blob: Blob,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
log("Starting backup process for Capacitor platform");
|
||||||
|
|
||||||
|
// Create a timestamped backup file name
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
const backupFileName = `timesafari-backup-${timestamp}.json`;
|
||||||
|
const backupFilePath = `backups/${backupFileName}`;
|
||||||
|
|
||||||
|
log("Creating backup file:", {
|
||||||
|
fileName: backupFileName,
|
||||||
|
path: backupFilePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write the backup file
|
||||||
|
const writeResult = (await Filesystem.writeFile({
|
||||||
|
path: backupFilePath,
|
||||||
|
data: base64Data,
|
||||||
|
directory: Directory.Cache,
|
||||||
|
recursive: true,
|
||||||
|
})) as unknown as { uri: string };
|
||||||
|
|
||||||
|
if (!writeResult.uri) {
|
||||||
|
throw new Error("Failed to write backup file: No URI returned");
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Backup file written successfully:", { uri: writeResult.uri });
|
||||||
|
|
||||||
|
// Share the backup file
|
||||||
|
log("Sharing backup file");
|
||||||
|
await Share.share({
|
||||||
|
title: "TimeSafari Backup",
|
||||||
|
text: "Your TimeSafari backup file",
|
||||||
|
url: writeResult.uri,
|
||||||
|
dialogTitle: "Share TimeSafari Backup",
|
||||||
|
});
|
||||||
|
|
||||||
|
log("Backup shared successfully");
|
||||||
|
|
||||||
|
// Clean up the temporary file
|
||||||
|
try {
|
||||||
|
await Filesystem.deleteFile({
|
||||||
|
path: backupFilePath,
|
||||||
|
directory: Directory.Cache,
|
||||||
|
});
|
||||||
|
log("Temporary backup file cleaned up");
|
||||||
|
} catch (cleanupError) {
|
||||||
|
error("Failed to clean up temporary backup file:", cleanupError);
|
||||||
|
// Don't throw here as the backup was successful
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error("Error during backup process:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,13 @@ import {
|
|||||||
RouteLocationNormalized,
|
RouteLocationNormalized,
|
||||||
RouteRecordRaw,
|
RouteRecordRaw,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
import { accountsDBPromise } from "../db/index";
|
import {
|
||||||
|
accountsDBPromise,
|
||||||
|
retrieveSettingsForActiveAccount,
|
||||||
|
} from "../db/index";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { Component as VueComponent } from "vue-facing-decorator";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -35,7 +40,79 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
{
|
{
|
||||||
path: "/account",
|
path: "/account",
|
||||||
name: "account",
|
name: "account",
|
||||||
component: () => import("../views/AccountViewView.vue"),
|
component: () => {
|
||||||
|
logger.log("Starting lazy load of AccountViewView");
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
import("../views/AccountViewView.vue")
|
||||||
|
.then((module) => {
|
||||||
|
if (!module?.default) {
|
||||||
|
logger.error(
|
||||||
|
"AccountViewView module loaded but default export is missing",
|
||||||
|
{
|
||||||
|
module: {
|
||||||
|
hasDefault: !!module?.default,
|
||||||
|
keys: Object.keys(module || {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
resolve(createErrorComponent());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the component has the required dependencies
|
||||||
|
const component = module.default;
|
||||||
|
logger.log("AccountViewView loaded, checking dependencies...", {
|
||||||
|
componentName: component.name,
|
||||||
|
hasVueComponent: component instanceof VueComponent,
|
||||||
|
hasClass: typeof component === "function",
|
||||||
|
type: typeof component,
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(component);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error("Failed to load AccountViewView:", {
|
||||||
|
error:
|
||||||
|
err instanceof Error
|
||||||
|
? {
|
||||||
|
name: err.name,
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack,
|
||||||
|
}
|
||||||
|
: err,
|
||||||
|
type: typeof err,
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(createErrorComponent());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
beforeEnter: async (to, from, next) => {
|
||||||
|
try {
|
||||||
|
logger.log("Account route beforeEnter guard starting");
|
||||||
|
|
||||||
|
// Check if required dependencies are available
|
||||||
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
|
logger.log("Account route: settings loaded", {
|
||||||
|
hasActiveDid: !!settings.activeDid,
|
||||||
|
isRegistered: !!settings.isRegistered,
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error in account route beforeEnter:", {
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
}
|
||||||
|
: error,
|
||||||
|
});
|
||||||
|
next({ name: "home" });
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/claim/:id?",
|
path: "/claim/:id?",
|
||||||
@@ -315,25 +392,271 @@ const router = createRouter({
|
|||||||
// Replace initial URL to start at `/` if necessary
|
// Replace initial URL to start at `/` if necessary
|
||||||
router.replace(initialPath || "/");
|
router.replace(initialPath || "/");
|
||||||
|
|
||||||
const errorHandler = (
|
// Add global error handler
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
router.onError((error, to, from) => {
|
||||||
error: any,
|
logger.error("Router error:", {
|
||||||
to: RouteLocationNormalized,
|
error:
|
||||||
from: RouteLocationNormalized,
|
error instanceof Error
|
||||||
) => {
|
? {
|
||||||
// Handle the error here
|
name: error.name,
|
||||||
logger.error("Caught in top level error handler:", error, to, from);
|
message: error.message,
|
||||||
alert("Something is very wrong. Try reloading or restarting the app.");
|
stack: error.stack,
|
||||||
|
}
|
||||||
|
: error,
|
||||||
|
to: {
|
||||||
|
name: to.name,
|
||||||
|
path: to.path,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
name: from.name,
|
||||||
|
path: from.path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
// If it's a reference error during account view import, try to handle it gracefully
|
||||||
};
|
if (error instanceof ReferenceError && to.name === "account") {
|
||||||
|
logger.error("Account view import error:", {
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
}
|
||||||
|
: error,
|
||||||
|
});
|
||||||
|
// Instead of redirecting, let the component's error handling take over
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.onError(errorHandler); // Assign the error handler to the router instance
|
// Add navigation guard for debugging
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
logger.log("Navigation debug:", {
|
||||||
|
to: {
|
||||||
|
fullPath: to.fullPath,
|
||||||
|
path: to.path,
|
||||||
|
name: to.name,
|
||||||
|
params: to.params,
|
||||||
|
query: to.query,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
fullPath: from.fullPath,
|
||||||
|
path: from.path,
|
||||||
|
name: from.name,
|
||||||
|
params: from.params,
|
||||||
|
query: from.query,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// router.beforeEach((to, from, next) => {
|
// For account route, try to preload the component
|
||||||
// console.log("Navigating to view:", to.name);
|
if (to.name === "account") {
|
||||||
// console.log("From view:", from.name);
|
logger.log("Preloading account component...");
|
||||||
// next();
|
|
||||||
// });
|
// Wrap in try-catch and use Promise
|
||||||
|
new Promise((resolve) => {
|
||||||
|
logger.log("Starting dynamic import of AccountViewView");
|
||||||
|
|
||||||
|
// Add immediate try-catch to get more context
|
||||||
|
try {
|
||||||
|
const importPromise = import("../views/AccountViewView.vue");
|
||||||
|
logger.log("Import initiated successfully");
|
||||||
|
|
||||||
|
importPromise
|
||||||
|
.then((module) => {
|
||||||
|
try {
|
||||||
|
logger.log("Import completed, analyzing module:", {
|
||||||
|
moduleExists: !!module,
|
||||||
|
moduleType: typeof module,
|
||||||
|
moduleKeys: Object.keys(module || {}),
|
||||||
|
hasDefault: !!module?.default,
|
||||||
|
defaultType: module?.default
|
||||||
|
? typeof module.default
|
||||||
|
: "undefined",
|
||||||
|
defaultConstructor: module?.default?.constructor?.name,
|
||||||
|
moduleContent: {
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(module).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
typeof value === "function"
|
||||||
|
? "function"
|
||||||
|
: typeof value === "object"
|
||||||
|
? Object.keys(value || {})
|
||||||
|
: typeof value,
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!module?.default) {
|
||||||
|
logger.error(
|
||||||
|
"AccountViewView preload: module loaded but default export is missing",
|
||||||
|
{
|
||||||
|
module: {
|
||||||
|
hasDefault: !!module?.default,
|
||||||
|
keys: Object.keys(module || {}),
|
||||||
|
moduleType: typeof module,
|
||||||
|
exports: Object.keys(module || {}).map((key) => ({
|
||||||
|
key,
|
||||||
|
type: typeof (module as any)[key],
|
||||||
|
value:
|
||||||
|
typeof (module as any)[key] === "function"
|
||||||
|
? "function"
|
||||||
|
: typeof (module as any)[key] === "object"
|
||||||
|
? Object.keys((module as any)[key] || {})
|
||||||
|
: (module as any)[key],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = module.default;
|
||||||
|
|
||||||
|
// Try to safely inspect the component
|
||||||
|
const componentDetails = {
|
||||||
|
componentName: component.name,
|
||||||
|
hasVueComponent: component instanceof VueComponent,
|
||||||
|
hasClass: typeof component === "function",
|
||||||
|
type: typeof component,
|
||||||
|
properties: Object.keys(component),
|
||||||
|
decorators: Object.getOwnPropertyDescriptor(
|
||||||
|
component,
|
||||||
|
"__decorators",
|
||||||
|
),
|
||||||
|
vueOptions:
|
||||||
|
(component as any).__vccOpts ||
|
||||||
|
(component as any).options ||
|
||||||
|
null,
|
||||||
|
setup: typeof (component as any).setup === "function",
|
||||||
|
render: typeof (component as any).render === "function",
|
||||||
|
components: (component as any).components
|
||||||
|
? Object.keys((component as any).components)
|
||||||
|
: null,
|
||||||
|
imports: Object.keys(module).filter((key) => key !== "default"),
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log("Successfully analyzed component:", componentDetails);
|
||||||
|
resolve(component);
|
||||||
|
} catch (analysisError) {
|
||||||
|
logger.error("Error during component analysis:", {
|
||||||
|
error:
|
||||||
|
analysisError instanceof Error
|
||||||
|
? {
|
||||||
|
name: analysisError.name,
|
||||||
|
message: analysisError.message,
|
||||||
|
stack: analysisError.stack,
|
||||||
|
keys: Object.keys(analysisError),
|
||||||
|
properties: Object.getOwnPropertyNames(analysisError),
|
||||||
|
}
|
||||||
|
: analysisError,
|
||||||
|
type: typeof analysisError,
|
||||||
|
phase: "analysis",
|
||||||
|
});
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error("Failed to preload account component:", {
|
||||||
|
error:
|
||||||
|
err instanceof Error
|
||||||
|
? {
|
||||||
|
name: err.name,
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack,
|
||||||
|
keys: Object.keys(err),
|
||||||
|
properties: Object.getOwnPropertyNames(err),
|
||||||
|
}
|
||||||
|
: err,
|
||||||
|
type: typeof err,
|
||||||
|
context: {
|
||||||
|
routeName: to.name,
|
||||||
|
routePath: to.path,
|
||||||
|
fromRoute: from.name,
|
||||||
|
},
|
||||||
|
phase: "module-load",
|
||||||
|
});
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
} catch (immediateError) {
|
||||||
|
logger.error("Immediate error during import initiation:", {
|
||||||
|
error:
|
||||||
|
immediateError instanceof Error
|
||||||
|
? {
|
||||||
|
name: immediateError.name,
|
||||||
|
message: immediateError.message,
|
||||||
|
stack: immediateError.stack,
|
||||||
|
keys: Object.keys(immediateError),
|
||||||
|
properties: Object.getOwnPropertyNames(immediateError),
|
||||||
|
}
|
||||||
|
: immediateError,
|
||||||
|
type: typeof immediateError,
|
||||||
|
context: {
|
||||||
|
routeName: to.name,
|
||||||
|
routePath: to.path,
|
||||||
|
fromRoute: from.name,
|
||||||
|
importPath: "../views/AccountViewView.vue",
|
||||||
|
},
|
||||||
|
phase: "import",
|
||||||
|
});
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
logger.error("Critical error in account component preload:", {
|
||||||
|
error:
|
||||||
|
err instanceof Error
|
||||||
|
? {
|
||||||
|
name: err.name,
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack,
|
||||||
|
}
|
||||||
|
: err,
|
||||||
|
context: {
|
||||||
|
routeName: to.name,
|
||||||
|
routePath: to.path,
|
||||||
|
fromRoute: from.name,
|
||||||
|
},
|
||||||
|
phase: "wrapper",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always call next() to continue navigation
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createErrorComponent() {
|
||||||
|
return defineComponent({
|
||||||
|
name: "AccountViewError",
|
||||||
|
components: {
|
||||||
|
// Add any required components here
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const goHome = () => {
|
||||||
|
router.push({ name: "home" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
goHome,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<section class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
|
<h1 class="text-4xl text-center font-light mb-8">Error Loading Account View</h1>
|
||||||
|
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
|
||||||
|
<strong class="font-bold">Failed to load account view.</strong>
|
||||||
|
<span class="block sm:inline"> Please try refreshing the page.</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 text-center">
|
||||||
|
<button @click="goHome" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||||
|
Return to Home
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
95
src/services/DatabaseBackupService.ts
Normal file
95
src/services/DatabaseBackupService.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* @file DatabaseBackupService.ts
|
||||||
|
* @description Base service class for handling database backup operations
|
||||||
|
*
|
||||||
|
* This service implements the Template Method pattern to provide a common interface
|
||||||
|
* for database backup operations across different platforms. It defines the structure
|
||||||
|
* of backup operations while delegating platform-specific implementations to subclasses.
|
||||||
|
*
|
||||||
|
* Build Process Integration:
|
||||||
|
* 1. Platform-Specific Implementation:
|
||||||
|
* - Each platform (web, electron, capacitor) has its own implementation
|
||||||
|
* - Implementations are loaded dynamically via PlatformServiceFactory
|
||||||
|
* - Located in ./platforms/{platform}/DatabaseBackupService.ts
|
||||||
|
*
|
||||||
|
* 2. Build Configuration:
|
||||||
|
* - Vite config files (vite.config.*.mts) set VITE_PLATFORM
|
||||||
|
* - PlatformServiceFactory uses this to load correct implementation
|
||||||
|
* - Build process creates separate chunks for each platform
|
||||||
|
*
|
||||||
|
* 3. Data Handling:
|
||||||
|
* - Supports multiple data formats (base64, ArrayBuffer, Blob)
|
||||||
|
* - Platform implementations handle format conversion
|
||||||
|
* - Ensures consistent backup format across platforms
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Create backup: DatabaseBackupService.createAndShareBackup(data)
|
||||||
|
* - Platform-specific: new WebDatabaseBackupService().handleBackup()
|
||||||
|
*
|
||||||
|
* @see PlatformServiceFactory.ts
|
||||||
|
* @see vite.config.web.mts
|
||||||
|
* @see vite.config.electron.mts
|
||||||
|
* @see vite.config.capacitor.mts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PlatformServiceFactory } from "./PlatformServiceFactory";
|
||||||
|
import { log, error } from "../utils/logger";
|
||||||
|
|
||||||
|
export class DatabaseBackupService {
|
||||||
|
/**
|
||||||
|
* Template method that must be implemented by platform-specific services
|
||||||
|
* @param base64Data - Backup data in base64 format
|
||||||
|
* @param arrayBuffer - Backup data as ArrayBuffer
|
||||||
|
* @param blob - Backup data as Blob
|
||||||
|
* @throws Error if not implemented by subclass
|
||||||
|
*/
|
||||||
|
protected async handleBackup(
|
||||||
|
_base64Data: string,
|
||||||
|
_arrayBuffer: ArrayBuffer,
|
||||||
|
_blob: Blob,
|
||||||
|
): Promise<void> {
|
||||||
|
throw new Error(
|
||||||
|
"handleBackup must be implemented by platform-specific service",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create and share a backup
|
||||||
|
* Uses PlatformServiceFactory to get platform-specific implementation
|
||||||
|
*
|
||||||
|
* @param base64Data - Backup data in base64 format
|
||||||
|
* @param arrayBuffer - Backup data as ArrayBuffer
|
||||||
|
* @param blob - Backup data as Blob
|
||||||
|
* @returns Promise that resolves when backup is complete
|
||||||
|
*/
|
||||||
|
public static async createAndShareBackup(
|
||||||
|
base64Data: string,
|
||||||
|
arrayBuffer: ArrayBuffer,
|
||||||
|
blob: Blob,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
log("Creating platform-specific backup service");
|
||||||
|
const backupService = await this.getPlatformSpecificBackupService();
|
||||||
|
log("Backup service created successfully");
|
||||||
|
|
||||||
|
log("Executing platform-specific backup");
|
||||||
|
await backupService.handleBackup(base64Data, arrayBuffer, blob);
|
||||||
|
log("Backup completed successfully");
|
||||||
|
} catch (err) {
|
||||||
|
error("Error during backup creation:", err);
|
||||||
|
if (err instanceof Error) {
|
||||||
|
error("Error details:", {
|
||||||
|
name: err.name,
|
||||||
|
message: err.message,
|
||||||
|
stack: err.stack,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getPlatformSpecificBackupService(): Promise<DatabaseBackupService> {
|
||||||
|
const factory = PlatformServiceFactory.getInstance();
|
||||||
|
return await factory.createDatabaseBackupService();
|
||||||
|
}
|
||||||
|
}
|
||||||
195
src/services/PlatformServiceFactory.ts
Normal file
195
src/services/PlatformServiceFactory.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* @file PlatformServiceFactory.ts
|
||||||
|
* @description Factory for creating platform-specific service implementations
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*
|
||||||
|
* This factory implements the Abstract Factory pattern to create platform-specific
|
||||||
|
* implementations of services. It uses Vite's dynamic import feature to load the
|
||||||
|
* appropriate implementation based on the current platform (web, electron, etc.).
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* 1. Singleton Pattern:
|
||||||
|
* - Ensures only one factory instance exists
|
||||||
|
* - Manages platform-specific service instances
|
||||||
|
* - Maintains consistent state across the application
|
||||||
|
*
|
||||||
|
* 2. Dynamic Loading:
|
||||||
|
* - Uses Vite's dynamic import for platform-specific code
|
||||||
|
* - Loads services on-demand based on platform
|
||||||
|
* - Handles platform detection and service instantiation
|
||||||
|
*
|
||||||
|
* 3. Platform Detection:
|
||||||
|
* - Uses VITE_PLATFORM environment variable
|
||||||
|
* - Supports web, electron, and capacitor platforms
|
||||||
|
* - Falls back to 'web' if platform is not specified
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```typescript
|
||||||
|
* // Get factory instance
|
||||||
|
* const factory = PlatformServiceFactory.getInstance();
|
||||||
|
*
|
||||||
|
* // Create platform-specific service
|
||||||
|
* const backupService = await factory.createDatabaseBackupService();
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @see vite.config.web.mts
|
||||||
|
* @see vite.config.electron.mts
|
||||||
|
* @see vite.config.capacitor.mts
|
||||||
|
* @see DatabaseBackupService
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DatabaseBackupService } from "./DatabaseBackupService";
|
||||||
|
import { DatabaseBackupService as StubDatabaseBackupService } from "./platforms/empty";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory class for creating platform-specific service implementations
|
||||||
|
*
|
||||||
|
* This class manages the creation and instantiation of platform-specific
|
||||||
|
* service implementations. It uses the Abstract Factory pattern to provide
|
||||||
|
* a consistent interface for creating services across different platforms.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // Get factory instance
|
||||||
|
* const factory = PlatformServiceFactory.getInstance();
|
||||||
|
*
|
||||||
|
* // Create platform-specific service
|
||||||
|
* const backupService = await factory.createDatabaseBackupService();
|
||||||
|
*
|
||||||
|
* // Use the service
|
||||||
|
* await backupService.handleBackup(data);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class PlatformServiceFactory {
|
||||||
|
/**
|
||||||
|
* Singleton instance of the factory
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private static instance: PlatformServiceFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current platform identifier
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private platform: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private constructor to enforce singleton pattern
|
||||||
|
*
|
||||||
|
* Initializes the factory with the current platform from environment variables.
|
||||||
|
* Falls back to 'web' if no platform is specified.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private constructor() {
|
||||||
|
this.platform = import.meta.env.VITE_PLATFORM || "web";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the singleton instance of the factory
|
||||||
|
*
|
||||||
|
* Creates a new instance if one doesn't exist, otherwise returns
|
||||||
|
* the existing instance.
|
||||||
|
*
|
||||||
|
* @returns {PlatformServiceFactory} The singleton factory instance
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const factory = PlatformServiceFactory.getInstance();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public static getInstance(): PlatformServiceFactory {
|
||||||
|
if (!PlatformServiceFactory.instance) {
|
||||||
|
PlatformServiceFactory.instance = new PlatformServiceFactory();
|
||||||
|
}
|
||||||
|
return PlatformServiceFactory.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a platform-specific database backup service
|
||||||
|
*
|
||||||
|
* Dynamically loads and instantiates the appropriate implementation
|
||||||
|
* based on the current platform. The implementation is loaded from
|
||||||
|
* the platforms/{platform}/DatabaseBackupService.ts file.
|
||||||
|
*
|
||||||
|
* @returns {Promise<DatabaseBackupService>} A promise that resolves to a platform-specific backup service
|
||||||
|
* @throws {Error} If the service fails to load or instantiate
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const factory = PlatformServiceFactory.getInstance();
|
||||||
|
* try {
|
||||||
|
* const backupService = await factory.createDatabaseBackupService();
|
||||||
|
* await backupService.handleBackup(data);
|
||||||
|
* } catch (error) {
|
||||||
|
* logger.error('Failed to create backup service:', error);
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public async createDatabaseBackupService(): Promise<DatabaseBackupService> {
|
||||||
|
// List of supported platforms for web builds
|
||||||
|
const webSupportedPlatforms = ["web", "capacitor", "electron"];
|
||||||
|
|
||||||
|
// Return stub implementation for unsupported platforms
|
||||||
|
if (!webSupportedPlatforms.includes(this.platform)) {
|
||||||
|
logger.log(
|
||||||
|
`Using stub implementation for unsupported platform: ${this.platform}`,
|
||||||
|
);
|
||||||
|
return new StubDatabaseBackupService();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.log(`Loading platform-specific service for ${this.platform}`);
|
||||||
|
// Use dynamic import with platform-specific path
|
||||||
|
const module = await import(
|
||||||
|
/* @vite-ignore */
|
||||||
|
`./platforms/${this.platform}/DatabaseBackupService.ts`
|
||||||
|
);
|
||||||
|
logger.log("Platform service loaded successfully");
|
||||||
|
return new module.DatabaseBackupService();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[TimeSafari] Failed to load platform-specific service for ${this.platform}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
// Fallback to stub implementation on error
|
||||||
|
logger.log("Falling back to stub implementation");
|
||||||
|
return new StubDatabaseBackupService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current platform identifier
|
||||||
|
*
|
||||||
|
* @returns {string} The current platform identifier
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const factory = PlatformServiceFactory.getInstance();
|
||||||
|
* logger.log(factory.getPlatform()); // 'web', 'electron', or 'capacitor'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public getPlatform(): string {
|
||||||
|
return this.platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current platform identifier
|
||||||
|
*
|
||||||
|
* This method is primarily used for testing purposes to override
|
||||||
|
* the platform detection. Use with caution in production code.
|
||||||
|
*
|
||||||
|
* @param {string} platform - The platform identifier to set
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const factory = PlatformServiceFactory.getInstance();
|
||||||
|
* factory.setPlatform('electron'); // For testing purposes only
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public setPlatform(platform: string): void {
|
||||||
|
this.platform = platform;
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/services/ProfileService.ts
Normal file
105
src/services/ProfileService.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* @file ProfileService.ts
|
||||||
|
* @description Service class for handling user profile operations
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
import { getHeaders } from "../libs/endorserServer";
|
||||||
|
import type { UserProfile } from "@/types/interfaces";
|
||||||
|
|
||||||
|
export class ProfileService {
|
||||||
|
/**
|
||||||
|
* Saves a user profile to the server
|
||||||
|
* @param activeDid - The user's active DID
|
||||||
|
* @param partnerApiServer - The partner API server URL
|
||||||
|
* @param profile - The profile data to save
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
static async saveProfile(
|
||||||
|
activeDid: string,
|
||||||
|
partnerApiServer: string,
|
||||||
|
profile: Partial<UserProfile>,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const headers = await getHeaders(activeDid);
|
||||||
|
const response = await fetch(
|
||||||
|
`${partnerApiServer}/api/partner/userProfile`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(profile),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to save profile: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error saving profile:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a user profile from the server
|
||||||
|
* @param activeDid - The user's active DID
|
||||||
|
* @param partnerApiServer - The partner API server URL
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
static async deleteProfile(
|
||||||
|
activeDid: string,
|
||||||
|
partnerApiServer: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const headers = await getHeaders(activeDid);
|
||||||
|
const response = await fetch(
|
||||||
|
`${partnerApiServer}/api/partner/userProfile`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to delete profile: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error deleting profile:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a user profile from the server
|
||||||
|
* @param activeDid - The user's active DID
|
||||||
|
* @param partnerApiServer - The partner API server URL
|
||||||
|
* @returns Promise<UserProfile | null>
|
||||||
|
*/
|
||||||
|
static async loadProfile(
|
||||||
|
activeDid: string,
|
||||||
|
partnerApiServer: string,
|
||||||
|
): Promise<UserProfile | null> {
|
||||||
|
try {
|
||||||
|
const headers = await getHeaders(activeDid);
|
||||||
|
const response = await fetch(
|
||||||
|
`${partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`,
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to load profile: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error loading profile:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/services/RateLimitsService.ts
Normal file
110
src/services/RateLimitsService.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* @file RateLimitsService.ts
|
||||||
|
* @description Service class for handling rate limit operations
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
import { getHeaders } from "../libs/endorserServer";
|
||||||
|
import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export class RateLimitsService {
|
||||||
|
/**
|
||||||
|
* Fetches rate limits for a given DID
|
||||||
|
* @param apiServer - The API server URL
|
||||||
|
* @param did - The user's DID
|
||||||
|
* @returns Promise<EndorserRateLimits>
|
||||||
|
*/
|
||||||
|
static async fetchRateLimits(
|
||||||
|
apiServer: string,
|
||||||
|
did: string,
|
||||||
|
): Promise<EndorserRateLimits> {
|
||||||
|
logger.log("Fetching rate limits for DID:", did);
|
||||||
|
logger.log("Using API server:", apiServer);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = await getHeaders(did);
|
||||||
|
const response = await axios.get(
|
||||||
|
`${apiServer}/api/v2/rate-limits/${did}`,
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
logger.log("Rate limits response:", response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
axios.isAxiosError(error) &&
|
||||||
|
(error.response?.status === 400 || error.response?.status === 404)
|
||||||
|
) {
|
||||||
|
const errorData = error.response.data as {
|
||||||
|
error?: { message?: string; code?: string };
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
errorData.error?.code === "UNREGISTERED_USER" ||
|
||||||
|
error.response?.status === 404
|
||||||
|
) {
|
||||||
|
logger.log("User is not registered, returning default limits");
|
||||||
|
return {
|
||||||
|
doneClaimsThisWeek: "0",
|
||||||
|
maxClaimsPerWeek: "0",
|
||||||
|
nextWeekBeginDateTime: new Date().toISOString(),
|
||||||
|
doneRegistrationsThisMonth: "0",
|
||||||
|
maxRegistrationsPerMonth: "0",
|
||||||
|
nextMonthBeginDateTime: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.error("Error fetching rate limits:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches image rate limits for a given DID
|
||||||
|
* @param apiServer - The API server URL
|
||||||
|
* @param activeDid - The user's active DID
|
||||||
|
* @returns Promise<ImageRateLimits>
|
||||||
|
*/
|
||||||
|
static async fetchImageRateLimits(
|
||||||
|
apiServer: string,
|
||||||
|
activeDid: string,
|
||||||
|
): Promise<ImageRateLimits> {
|
||||||
|
try {
|
||||||
|
const headers = await getHeaders(activeDid);
|
||||||
|
const response = await fetch(
|
||||||
|
`${apiServer}/api/endorser/imageRateLimits/${activeDid}`,
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch image rate limits: ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error fetching image rate limits:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats rate limit error messages
|
||||||
|
* @param error - The error object
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
static formatRateLimitError(error: unknown): string {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
if (typeof error === "object" && error !== null) {
|
||||||
|
const err = error as {
|
||||||
|
response?: { data?: { error?: { message?: string } } };
|
||||||
|
};
|
||||||
|
return err.response?.data?.error?.message || "An unknown error occurred";
|
||||||
|
}
|
||||||
|
return "An unknown error occurred";
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/services/platforms/capacitor/DatabaseBackupService.ts
Normal file
35
src/services/platforms/capacitor/DatabaseBackupService.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* @file DatabaseBackupService.ts
|
||||||
|
* @description Capacitor-specific implementation of the DatabaseBackupService
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DatabaseBackupService } from "../../DatabaseBackupService";
|
||||||
|
import { Filesystem } from "@capacitor/filesystem";
|
||||||
|
import { Share } from "@capacitor/share";
|
||||||
|
|
||||||
|
export default class CapacitorDatabaseBackupService extends DatabaseBackupService {
|
||||||
|
protected async handleBackup(
|
||||||
|
base64Data: string,
|
||||||
|
_arrayBuffer: ArrayBuffer,
|
||||||
|
_blob: Blob,
|
||||||
|
): Promise<void> {
|
||||||
|
// Capacitor platform handling
|
||||||
|
const fileName = `database-backup-${new Date().toISOString()}.json`;
|
||||||
|
const path = `backups/${fileName}`;
|
||||||
|
|
||||||
|
await Filesystem.writeFile({
|
||||||
|
path,
|
||||||
|
data: base64Data,
|
||||||
|
directory: "CACHE",
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Share.share({
|
||||||
|
title: "Database Backup",
|
||||||
|
text: "Here's your database backup",
|
||||||
|
url: path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/services/platforms/electron/DatabaseBackupService.ts
Normal file
18
src/services/platforms/electron/DatabaseBackupService.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { DatabaseBackupService } from "../../DatabaseBackupService";
|
||||||
|
import { dialog } from "electron";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
export default class ElectronDatabaseBackupService extends DatabaseBackupService {
|
||||||
|
protected async handleBackup(base64Data: string): Promise<void> {
|
||||||
|
const { filePath } = await dialog.showSaveDialog({
|
||||||
|
title: "Save Database Backup",
|
||||||
|
defaultPath: path.join(process.env.HOME || "", "database-backup.json"),
|
||||||
|
filters: [{ name: "JSON", extensions: ["json"] }],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
fs.writeFileSync(filePath, base64Data, "base64");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/services/platforms/empty.ts
Normal file
20
src/services/platforms/empty.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* @file empty.ts
|
||||||
|
* @description Stub implementation for excluding platform-specific code
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DatabaseBackupService } from "../DatabaseBackupService";
|
||||||
|
|
||||||
|
export default class StubDatabaseBackupService extends DatabaseBackupService {
|
||||||
|
protected async handleBackup(
|
||||||
|
_base64Data: string,
|
||||||
|
_arrayBuffer: ArrayBuffer,
|
||||||
|
_blob: Blob,
|
||||||
|
): Promise<void> {
|
||||||
|
throw new Error("This platform does not support database backups");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { StubDatabaseBackupService as DatabaseBackupService };
|
||||||
33
src/services/platforms/web/DatabaseBackupService.ts
Normal file
33
src/services/platforms/web/DatabaseBackupService.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @file DatabaseBackupService.ts
|
||||||
|
* @description Web-specific implementation of the DatabaseBackupService
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { DatabaseBackupService } from "../../DatabaseBackupService";
|
||||||
|
import { log, error } from "../../../utils/logger";
|
||||||
|
|
||||||
|
export default class WebDatabaseBackupService extends DatabaseBackupService {
|
||||||
|
protected async handleBackup(
|
||||||
|
_base64Data: string,
|
||||||
|
_arrayBuffer: ArrayBuffer,
|
||||||
|
blob: Blob,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
log("Starting web platform backup");
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `database-backup-${new Date().toISOString()}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
log("Web platform backup completed");
|
||||||
|
} catch (err) {
|
||||||
|
error("Error during web platform backup:", err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/types/capacitor.d.ts
vendored
Normal file
64
src/types/capacitor.d.ts
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Type declarations for Capacitor modules used in the application.
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module "@capacitor/filesystem" {
|
||||||
|
export interface FileWriteOptions {
|
||||||
|
path: string;
|
||||||
|
data: string;
|
||||||
|
directory?: string;
|
||||||
|
encoding?: string;
|
||||||
|
recursive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileReadResult {
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileDeleteOptions {
|
||||||
|
path: string;
|
||||||
|
directory?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilesystemDirectory {
|
||||||
|
Cache: "CACHE";
|
||||||
|
Documents: "DOCUMENTS";
|
||||||
|
Data: "DATA";
|
||||||
|
External: "EXTERNAL";
|
||||||
|
ExternalStorage: "EXTERNAL_STORAGE";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Filesystem {
|
||||||
|
writeFile(options: FileWriteOptions): Promise<void>;
|
||||||
|
readFile(options: {
|
||||||
|
path: string;
|
||||||
|
directory?: string;
|
||||||
|
}): Promise<FileReadResult>;
|
||||||
|
deleteFile(options: FileDeleteOptions): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Filesystem: Filesystem;
|
||||||
|
export const Directory: FilesystemDirectory;
|
||||||
|
export const Encoding: {
|
||||||
|
UTF8: "utf8";
|
||||||
|
ASCII: "ascii";
|
||||||
|
UTF16: "utf16";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@capacitor/share" {
|
||||||
|
export interface ShareOptions {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
url?: string;
|
||||||
|
dialogTitle?: string;
|
||||||
|
files?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Share {
|
||||||
|
share(options: ShareOptions): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Share: Share;
|
||||||
|
}
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Index file for all type declarations.
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./interfaces";
|
||||||
|
|
||||||
import { GiveSummaryRecord, GiveVerifiableCredential } from "interfaces";
|
import { GiveSummaryRecord, GiveVerifiableCredential } from "interfaces";
|
||||||
|
|
||||||
export interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
export interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
||||||
|
|||||||
430
src/types/interfaces.ts
Normal file
430
src/types/interfaces.ts
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
/**
|
||||||
|
* @file interfaces.ts
|
||||||
|
* @description Core type declarations for the TimeSafari application
|
||||||
|
*
|
||||||
|
* This module defines the core interfaces and types used throughout the application.
|
||||||
|
* It serves as the central location for type definitions that are shared across
|
||||||
|
* multiple components and services.
|
||||||
|
*
|
||||||
|
* Architecture:
|
||||||
|
* 1. DID (Decentralized Identifier) Types:
|
||||||
|
* - IIdentifier: Core DID structure
|
||||||
|
* - IKey: Cryptographic key information
|
||||||
|
* - IService: Service endpoints and capabilities
|
||||||
|
*
|
||||||
|
* 2. Verifiable Credential Types:
|
||||||
|
* - GenericCredWrapper: Base wrapper for all credentials
|
||||||
|
* - GiveVerifiableCredential: Gift-related credentials
|
||||||
|
* - OfferVerifiableCredential: Offer-related credentials
|
||||||
|
* - RegisterVerifiableCredential: Registration credentials
|
||||||
|
*
|
||||||
|
* 3. Service Types:
|
||||||
|
* - EndorserService: Claims and endorsements
|
||||||
|
* - PushNotificationService: Web push notifications
|
||||||
|
* - ProfileService: User profiles
|
||||||
|
* - BackupService: Data backup
|
||||||
|
*
|
||||||
|
* @see src/interfaces/identifier.ts
|
||||||
|
* @see src/interfaces/claims.ts
|
||||||
|
* @see src/interfaces/limits.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { GiveVerifiableCredential } from "../interfaces";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a Decentralized Identifier (DID)
|
||||||
|
*
|
||||||
|
* This interface defines the structure of a DID, which is a unique identifier
|
||||||
|
* that can be used to look up a DID document containing information associated
|
||||||
|
* with the DID, such as public keys and service endpoints.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const identifier: IIdentifier = {
|
||||||
|
* did: 'did:ethr:0x123...',
|
||||||
|
* provider: 'ethr',
|
||||||
|
* keys: [{
|
||||||
|
* kid: 'keys-1',
|
||||||
|
* kms: 'local',
|
||||||
|
* type: 'Secp256k1',
|
||||||
|
* publicKeyHex: '0x...',
|
||||||
|
* meta: { derivationPath: "m/44'/60'/0'/0/0" }
|
||||||
|
* }],
|
||||||
|
* services: [{
|
||||||
|
* id: 'endorser-service',
|
||||||
|
* type: 'EndorserService',
|
||||||
|
* serviceEndpoint: 'https://api.endorser.ch'
|
||||||
|
* }]
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface IIdentifier {
|
||||||
|
/**
|
||||||
|
* The DID string in the format 'did:method:identifier'
|
||||||
|
* @example 'did:ethr:0x123...'
|
||||||
|
*/
|
||||||
|
did: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The DID method provider
|
||||||
|
* @example 'ethr'
|
||||||
|
*/
|
||||||
|
provider: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of cryptographic keys associated with the DID
|
||||||
|
*/
|
||||||
|
keys: Array<{
|
||||||
|
/**
|
||||||
|
* Key identifier
|
||||||
|
* @example 'keys-1'
|
||||||
|
*/
|
||||||
|
kid: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key management system
|
||||||
|
* @example 'local'
|
||||||
|
*/
|
||||||
|
kms: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key type
|
||||||
|
* @example 'Secp256k1'
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key in hexadecimal format
|
||||||
|
* @example '0x...'
|
||||||
|
*/
|
||||||
|
publicKeyHex: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional metadata about the key
|
||||||
|
*/
|
||||||
|
meta?: {
|
||||||
|
/**
|
||||||
|
* HD wallet derivation path
|
||||||
|
* @example "m/44'/60'/0'/0/0"
|
||||||
|
*/
|
||||||
|
derivationPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key usage or purpose
|
||||||
|
* @example "signing", "encryption"
|
||||||
|
*/
|
||||||
|
usage?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key creation timestamp
|
||||||
|
*/
|
||||||
|
createdAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional key metadata
|
||||||
|
*/
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of service endpoints associated with the DID
|
||||||
|
*/
|
||||||
|
services: Array<{
|
||||||
|
/**
|
||||||
|
* Service identifier
|
||||||
|
* @example 'endorser-service'
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service type
|
||||||
|
* @example 'EndorserService'
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service endpoint URL
|
||||||
|
* @example 'https://api.endorser.ch'
|
||||||
|
*/
|
||||||
|
serviceEndpoint: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional service description
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional metadata about the identifier
|
||||||
|
*/
|
||||||
|
meta?: {
|
||||||
|
/**
|
||||||
|
* DID method-specific metadata
|
||||||
|
* @example { network: "mainnet", chainId: 1 } for ethr
|
||||||
|
*/
|
||||||
|
method?: Record<string, unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identifier creation timestamp
|
||||||
|
*/
|
||||||
|
createdAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last update timestamp
|
||||||
|
*/
|
||||||
|
updatedAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional identifier metadata
|
||||||
|
*/
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a cryptographic key
|
||||||
|
*
|
||||||
|
* This interface defines the structure of a cryptographic key used in the
|
||||||
|
* DID system. It includes both public and private key information, along
|
||||||
|
* with metadata about the key's purpose and derivation.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const key: IKey = {
|
||||||
|
* id: 'did:ethr:0x123...#keys-1',
|
||||||
|
* type: 'Secp256k1VerificationKey2018',
|
||||||
|
* controller: 'did:ethr:0x123...',
|
||||||
|
* ethereumAddress: '0x123...',
|
||||||
|
* publicKeyHex: '0x...',
|
||||||
|
* privateKeyHex: '0x...',
|
||||||
|
* meta: {
|
||||||
|
* derivationPath: "m/44'/60'/0'/0/0"
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface IKey {
|
||||||
|
/**
|
||||||
|
* Unique identifier for the key
|
||||||
|
* @example 'did:ethr:0x123...#keys-1'
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key type specification
|
||||||
|
* @example 'Secp256k1VerificationKey2018'
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DID that controls this key
|
||||||
|
* @example 'did:ethr:0x123...'
|
||||||
|
*/
|
||||||
|
controller: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associated Ethereum address
|
||||||
|
* @example '0x123...'
|
||||||
|
*/
|
||||||
|
ethereumAddress: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public key in hexadecimal format
|
||||||
|
* @example '0x...'
|
||||||
|
*/
|
||||||
|
publicKeyHex: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private key in hexadecimal format
|
||||||
|
* @example '0x...'
|
||||||
|
*/
|
||||||
|
privateKeyHex: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional metadata about the key
|
||||||
|
*/
|
||||||
|
meta?: {
|
||||||
|
/**
|
||||||
|
* HD wallet derivation path
|
||||||
|
* @example "m/44'/60'/0'/0/0"
|
||||||
|
*/
|
||||||
|
derivationPath?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key usage or purpose
|
||||||
|
* @example "signing", "encryption"
|
||||||
|
*/
|
||||||
|
usage?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key creation timestamp
|
||||||
|
*/
|
||||||
|
createdAt?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional key metadata
|
||||||
|
*/
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a service endpoint
|
||||||
|
*
|
||||||
|
* This interface defines the structure of a service endpoint that can be
|
||||||
|
* associated with a DID. Services provide additional functionality and
|
||||||
|
* endpoints for DID operations.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const service: IService = {
|
||||||
|
* id: 'endorser-service',
|
||||||
|
* type: 'EndorserService',
|
||||||
|
* serviceEndpoint: 'https://api.endorser.ch',
|
||||||
|
* description: 'Service for handling claims and endorsements',
|
||||||
|
* metadata: {
|
||||||
|
* version: '1.0.0',
|
||||||
|
* capabilities: ['claims', 'endorsements'],
|
||||||
|
* config: {
|
||||||
|
* apiServer: 'https://api.endorser.ch'
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface IService {
|
||||||
|
/**
|
||||||
|
* Unique identifier for the service
|
||||||
|
* @example 'endorser-service'
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of service
|
||||||
|
* @example 'EndorserService'
|
||||||
|
*/
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service endpoint URL
|
||||||
|
* @example 'https://api.endorser.ch'
|
||||||
|
*/
|
||||||
|
serviceEndpoint: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional human-readable description
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional service metadata
|
||||||
|
*/
|
||||||
|
metadata?: {
|
||||||
|
/**
|
||||||
|
* Service version
|
||||||
|
* @example '1.0.0'
|
||||||
|
*/
|
||||||
|
version?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of service capabilities
|
||||||
|
* @example ['claims', 'endorsements']
|
||||||
|
*/
|
||||||
|
capabilities?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service-specific configuration
|
||||||
|
*/
|
||||||
|
config?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExportProgress {
|
||||||
|
status: "preparing" | "exporting" | "complete" | "error";
|
||||||
|
message?: string;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
/** User's profile description */
|
||||||
|
description: string;
|
||||||
|
/** User's location information */
|
||||||
|
location?: {
|
||||||
|
/** Latitude coordinate */
|
||||||
|
lat: number;
|
||||||
|
/** Longitude coordinate */
|
||||||
|
lng: number;
|
||||||
|
};
|
||||||
|
/** User's given name */
|
||||||
|
givenName?: string;
|
||||||
|
/** User's family name */
|
||||||
|
familyName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorResponse {
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LeafletMouseEvent {
|
||||||
|
latlng: {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GiveRecordWithContactInfo {
|
||||||
|
type?: string;
|
||||||
|
agentDid: string;
|
||||||
|
amount: number;
|
||||||
|
amountConfirmed: number;
|
||||||
|
description: string;
|
||||||
|
fullClaim: GiveVerifiableCredential;
|
||||||
|
fulfillsHandleId: string;
|
||||||
|
fulfillsPlanHandleId?: string;
|
||||||
|
fulfillsType?: string;
|
||||||
|
handleId: string;
|
||||||
|
issuedAt: string;
|
||||||
|
issuerDid: string;
|
||||||
|
jwtId: string;
|
||||||
|
providerPlanHandleId?: string;
|
||||||
|
recipientDid: string;
|
||||||
|
unit: string;
|
||||||
|
giver: {
|
||||||
|
known: boolean;
|
||||||
|
displayName: string;
|
||||||
|
profileImageUrl?: string;
|
||||||
|
};
|
||||||
|
issuer: {
|
||||||
|
known: boolean;
|
||||||
|
displayName: string;
|
||||||
|
profileImageUrl?: string;
|
||||||
|
};
|
||||||
|
receiver: {
|
||||||
|
known: boolean;
|
||||||
|
displayName: string;
|
||||||
|
profileImageUrl?: string;
|
||||||
|
};
|
||||||
|
providerPlanName?: string;
|
||||||
|
recipientProjectName?: string;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSafariError extends Error {
|
||||||
|
/**
|
||||||
|
* User-friendly error message
|
||||||
|
*/
|
||||||
|
userMessage?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error code for programmatic handling
|
||||||
|
*/
|
||||||
|
code?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional error context
|
||||||
|
*/
|
||||||
|
context?: Record<string, unknown>;
|
||||||
|
}
|
||||||
@@ -4,6 +4,32 @@ function safeStringify(obj: unknown) {
|
|||||||
const seen = new WeakSet();
|
const seen = new WeakSet();
|
||||||
|
|
||||||
return JSON.stringify(obj, (key, value) => {
|
return JSON.stringify(obj, (key, value) => {
|
||||||
|
// Skip Vue component instance properties
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
typeof value === "object" &&
|
||||||
|
("$el" in value || "$options" in value || "$parent" in value)
|
||||||
|
) {
|
||||||
|
return "[Vue Component]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Vue router objects
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
typeof value === "object" &&
|
||||||
|
("fullPath" in value || "path" in value || "name" in value)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
fullPath: value.fullPath,
|
||||||
|
path: value.path,
|
||||||
|
name: value.name,
|
||||||
|
params: value.params,
|
||||||
|
query: value.query,
|
||||||
|
hash: value.hash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle circular references
|
||||||
if (typeof value === "object" && value !== null) {
|
if (typeof value === "object" && value !== null) {
|
||||||
if (seen.has(value)) {
|
if (seen.has(value)) {
|
||||||
return "[Circular]";
|
return "[Circular]";
|
||||||
@@ -11,6 +37,7 @@ function safeStringify(obj: unknown) {
|
|||||||
seen.add(value);
|
seen.add(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle functions
|
||||||
if (typeof value === "function") {
|
if (typeof value === "function") {
|
||||||
return `[Function: ${value.name || "anonymous"}]`;
|
return `[Function: ${value.name || "anonymous"}]`;
|
||||||
}
|
}
|
||||||
@@ -19,28 +46,63 @@ function safeStringify(obj: unknown) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatMessage(message: string, ...args: unknown[]): string {
|
||||||
|
const prefix = "[TimeSafari]";
|
||||||
|
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||||
|
return `${prefix} ${message}${argsString}`;
|
||||||
|
}
|
||||||
|
|
||||||
export const logger = {
|
export const logger = {
|
||||||
log: (message: string, ...args: unknown[]) => {
|
log: (message: string, ...args: unknown[]) => {
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
const formattedMessage = formatMessage(message, ...args);
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(message, ...args);
|
console.log(formattedMessage);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
|
||||||
logToDb(message + argsString);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
warn: (message: string, ...args: unknown[]) => {
|
warn: (message: string, ...args: unknown[]) => {
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
const formattedMessage = formatMessage(message, ...args);
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(message, ...args);
|
console.warn(formattedMessage);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
|
||||||
logToDb(message + argsString);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (message: string, ...args: unknown[]) => {
|
error: (message: string, ...args: unknown[]) => {
|
||||||
// Errors will always be logged
|
const formattedMessage = formatMessage(message, ...args);
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(message, ...args);
|
console.error(formattedMessage);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : ""));
|
||||||
logToDb(message + argsString);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function log(message: string, ...args: unknown[]): void {
|
||||||
|
const formattedMessage = formatMessage(message, ...args);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(formattedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function error(message: string, ...args: unknown[]): void {
|
||||||
|
const formattedMessage = formatMessage(message, ...args);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(formattedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function warn(message: string, ...args: unknown[]): void {
|
||||||
|
const formattedMessage = formatMessage(message, ...args);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(formattedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function info(message: string, ...args: unknown[]): void {
|
||||||
|
const formattedMessage = formatMessage(message, ...args);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.info(formattedMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debug(message: string, ...args: unknown[]): void {
|
||||||
|
const formattedMessage = formatMessage(message, ...args);
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.debug(formattedMessage);
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,17 @@
|
|||||||
<router-link :to="'/claim/' + claimId">
|
<router-link :to="'/claim/' + claimId">
|
||||||
<canvas ref="claimCanvas" class="w-full block mx-auto"></canvas>
|
<canvas ref="claimCanvas" class="w-full block mx-auto"></canvas>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<div class="qr-code-container">
|
||||||
|
<QRCodeVue
|
||||||
|
ref="qrCodeRef"
|
||||||
|
:value="qrCodeData"
|
||||||
|
:size="200"
|
||||||
|
level="H"
|
||||||
|
render-as="svg"
|
||||||
|
:margin="0"
|
||||||
|
:color="{ dark: '#000000', light: '#ffffff' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -13,13 +24,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { nextTick } from "vue";
|
import { nextTick } from "vue";
|
||||||
import QRCode from "qrcode";
|
import QRCodeVue from "qrcode.vue";
|
||||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
|
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
@Component
|
@Component({
|
||||||
|
components: {
|
||||||
|
QRCodeVue,
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class ClaimCertificateView extends Vue {
|
export default class ClaimCertificateView extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
@@ -31,6 +46,8 @@ export default class ClaimCertificateView extends Vue {
|
|||||||
|
|
||||||
serverUtil = serverUtil;
|
serverUtil = serverUtil;
|
||||||
|
|
||||||
|
private qrCodeRef: InstanceType<typeof QRCodeVue> | null = null;
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
@@ -252,19 +269,23 @@ export default class ClaimCertificateView extends Vue {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Generate and draw QR code
|
// Generate and draw QR code
|
||||||
const qrCodeCanvas = document.createElement("canvas");
|
await this.generateQRCode();
|
||||||
await QRCode.toCanvas(
|
|
||||||
qrCodeCanvas,
|
|
||||||
APP_SERVER + "/claim/" + this.claimId,
|
|
||||||
{
|
|
||||||
width: 150,
|
|
||||||
color: { light: "#0000" /* Transparent background */ },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ctx.drawImage(qrCodeCanvas, CANVAS_WIDTH * 0.6, CANVAS_HEIGHT * 0.55);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async generateQRCode() {
|
||||||
|
if (!this.qrCodeRef) return;
|
||||||
|
|
||||||
|
const canvas = await this.qrCodeRef.toCanvas();
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// Draw the QR code on the claim canvas
|
||||||
|
const CANVAS_WIDTH = 1100;
|
||||||
|
const CANVAS_HEIGHT = 850;
|
||||||
|
ctx.drawImage(canvas, CANVAS_WIDTH * 0.6, CANVAS_HEIGHT * 0.55);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
<section id="Content">
|
<section id="Content">
|
||||||
<div v-if="claimData">
|
<div v-if="claimData">
|
||||||
<canvas ref="claimCanvas"></canvas>
|
<canvas ref="claimCanvas"></canvas>
|
||||||
|
<div class="qr-code-container">
|
||||||
|
<QRCodeVue
|
||||||
|
ref="qrCodeRef"
|
||||||
|
:value="qrCodeData"
|
||||||
|
:size="200"
|
||||||
|
level="H"
|
||||||
|
render-as="svg"
|
||||||
|
:margin="0"
|
||||||
|
:color="{ dark: '#000000', light: '#ffffff' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -9,13 +20,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { nextTick } from "vue";
|
import { nextTick } from "vue";
|
||||||
import QRCode from "qrcode";
|
import QRCodeVue from "qrcode.vue";
|
||||||
|
|
||||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import * as endorserServer from "../libs/endorserServer";
|
import * as endorserServer from "../libs/endorserServer";
|
||||||
|
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
@Component
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
QRCodeVue,
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class ClaimReportCertificateView extends Vue {
|
export default class ClaimReportCertificateView extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
@@ -23,10 +40,14 @@ export default class ClaimReportCertificateView extends Vue {
|
|||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
claimId = "";
|
claimId = "";
|
||||||
claimData = null;
|
claimData: GenericCredWrapper<GenericVerifiableCredential> | null = null;
|
||||||
|
|
||||||
endorserServer = endorserServer;
|
endorserServer = endorserServer;
|
||||||
|
|
||||||
|
private qrCodeRef: InstanceType<typeof QRCodeVue> | null = null;
|
||||||
|
private readonly CANVAS_WIDTH = 1100;
|
||||||
|
private readonly CANVAS_HEIGHT = 850;
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
const settings = await retrieveSettingsForActiveAccount();
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
@@ -63,20 +84,12 @@ export default class ClaimReportCertificateView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async drawCanvas(
|
async drawCanvas(claimData: GenericCredWrapper<GenericVerifiableCredential>) {
|
||||||
claimData: endorserServer.GenericCredWrapper<endorserServer.GenericVerifiableCredential>,
|
|
||||||
) {
|
|
||||||
await db.open();
|
await db.open();
|
||||||
const allContacts = await db.contacts.toArray();
|
const allContacts = await db.contacts.toArray();
|
||||||
|
|
||||||
const canvas = this.$refs.claimCanvas as HTMLCanvasElement;
|
const canvas = this.$refs.claimCanvas as HTMLCanvasElement;
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
const CANVAS_WIDTH = 1100;
|
|
||||||
const CANVAS_HEIGHT = 850;
|
|
||||||
|
|
||||||
// size to approximate portrait of 8.5"x11"
|
|
||||||
canvas.width = CANVAS_WIDTH;
|
|
||||||
canvas.height = CANVAS_HEIGHT;
|
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
// Load the background image
|
// Load the background image
|
||||||
@@ -84,7 +97,13 @@ export default class ClaimReportCertificateView extends Vue {
|
|||||||
backgroundImage.src = "/img/background/cert-frame-2.jpg";
|
backgroundImage.src = "/img/background/cert-frame-2.jpg";
|
||||||
backgroundImage.onload = async () => {
|
backgroundImage.onload = async () => {
|
||||||
// Draw the background image
|
// Draw the background image
|
||||||
ctx.drawImage(backgroundImage, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
ctx.drawImage(
|
||||||
|
backgroundImage,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
this.CANVAS_WIDTH,
|
||||||
|
this.CANVAS_HEIGHT,
|
||||||
|
);
|
||||||
|
|
||||||
// Set font and styles
|
// Set font and styles
|
||||||
ctx.fillStyle = "black";
|
ctx.fillStyle = "black";
|
||||||
@@ -98,8 +117,8 @@ export default class ClaimReportCertificateView extends Vue {
|
|||||||
const claimTypeWidth = ctx.measureText(claimTypeText).width;
|
const claimTypeWidth = ctx.measureText(claimTypeText).width;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
claimTypeText,
|
claimTypeText,
|
||||||
(CANVAS_WIDTH - claimTypeWidth) / 2, // Center horizontally
|
(this.CANVAS_WIDTH - claimTypeWidth) / 2, // Center horizontally
|
||||||
CANVAS_HEIGHT * 0.33,
|
this.CANVAS_HEIGHT * 0.33,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (claimData.claim.agent) {
|
if (claimData.claim.agent) {
|
||||||
@@ -108,8 +127,8 @@ export default class ClaimReportCertificateView extends Vue {
|
|||||||
const presentedWidth = ctx.measureText(presentedText).width;
|
const presentedWidth = ctx.measureText(presentedText).width;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
presentedText,
|
presentedText,
|
||||||
(CANVAS_WIDTH - presentedWidth) / 2, // Center horizontally
|
(this.CANVAS_WIDTH - presentedWidth) / 2, // Center horizontally
|
||||||
CANVAS_HEIGHT * 0.37,
|
this.CANVAS_HEIGHT * 0.37,
|
||||||
);
|
);
|
||||||
const agentText = endorserServer.didInfoForCertificate(
|
const agentText = endorserServer.didInfoForCertificate(
|
||||||
claimData.claim.agent,
|
claimData.claim.agent,
|
||||||
@@ -119,8 +138,8 @@ export default class ClaimReportCertificateView extends Vue {
|
|||||||
const agentWidth = ctx.measureText(agentText).width;
|
const agentWidth = ctx.measureText(agentText).width;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
agentText,
|
agentText,
|
||||||
(CANVAS_WIDTH - agentWidth) / 2, // Center horizontally
|
(this.CANVAS_WIDTH - agentWidth) / 2, // Center horizontally
|
||||||
CANVAS_HEIGHT * 0.4,
|
this.CANVAS_HEIGHT * 0.4,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,8 +154,8 @@ export default class ClaimReportCertificateView extends Vue {
|
|||||||
const descriptionWidth = ctx.measureText(descriptionLine).width;
|
const descriptionWidth = ctx.measureText(descriptionLine).width;
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
descriptionLine,
|
descriptionLine,
|
||||||
(CANVAS_WIDTH - descriptionWidth) / 2,
|
(this.CANVAS_WIDTH - descriptionWidth) / 2,
|
||||||
CANVAS_HEIGHT * 0.45,
|
this.CANVAS_HEIGHT * 0.45,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,33 +168,43 @@ export default class ClaimReportCertificateView extends Vue {
|
|||||||
claimData.issuer,
|
claimData.issuer,
|
||||||
allContacts,
|
allContacts,
|
||||||
);
|
);
|
||||||
ctx.fillText(issuerText, CANVAS_WIDTH * 0.3, CANVAS_HEIGHT * 0.6);
|
ctx.fillText(
|
||||||
|
issuerText,
|
||||||
|
this.CANVAS_WIDTH * 0.3,
|
||||||
|
this.CANVAS_HEIGHT * 0.6,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw claim ID
|
// Draw claim ID
|
||||||
ctx.font = "14px Arial";
|
ctx.font = "14px Arial";
|
||||||
ctx.fillText(this.claimId, CANVAS_WIDTH * 0.3, CANVAS_HEIGHT * 0.7);
|
ctx.fillText(
|
||||||
|
this.claimId,
|
||||||
|
this.CANVAS_WIDTH * 0.3,
|
||||||
|
this.CANVAS_HEIGHT * 0.7,
|
||||||
|
);
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
"via EndorserSearch.com",
|
"via EndorserSearch.com",
|
||||||
CANVAS_WIDTH * 0.3,
|
this.CANVAS_WIDTH * 0.3,
|
||||||
CANVAS_HEIGHT * 0.73,
|
this.CANVAS_HEIGHT * 0.73,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate and draw QR code
|
// Generate and draw QR code
|
||||||
const qrCodeCanvas = document.createElement("canvas");
|
await this.generateQRCode();
|
||||||
await QRCode.toCanvas(
|
|
||||||
qrCodeCanvas,
|
|
||||||
APP_SERVER + "/claim/" + this.claimId,
|
|
||||||
{
|
|
||||||
width: 150,
|
|
||||||
color: { light: "#0000" /* Transparent background */ },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ctx.drawImage(qrCodeCanvas, CANVAS_WIDTH * 0.6, CANVAS_HEIGHT * 0.55);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async generateQRCode() {
|
||||||
|
if (!this.qrCodeRef) return;
|
||||||
|
|
||||||
|
const canvas = await this.qrCodeRef.toCanvas();
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
// Draw the QR code on the report canvas
|
||||||
|
ctx.drawImage(canvas, this.CANVAS_WIDTH * 0.6, this.CANVAS_HEIGHT * 0.55);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -186,5 +215,18 @@ canvas {
|
|||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<h2 class="text-base flex gap-4 items-center">
|
<h2 class="text-base flex gap-4 items-center">
|
||||||
<span class="grow">
|
<span class="grow">
|
||||||
<img
|
<img
|
||||||
src="../assets/blank-square.svg"
|
src="@/assets/blank-square.svg"
|
||||||
width="32"
|
width="32"
|
||||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -484,13 +484,13 @@
|
|||||||
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
|
<a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer">
|
||||||
<span class="text-blue-500 mr-1">CC0 1.0</span>
|
<span class="text-blue-500 mr-1">CC0 1.0</span>
|
||||||
<img
|
<img
|
||||||
src="../assets/help/creative-commons-circle.svg"
|
src="@/assets/help/creative-commons-circle.svg"
|
||||||
alt="CC circle"
|
alt="CC circle"
|
||||||
width="20"
|
width="20"
|
||||||
class="display: inline"
|
class="display: inline"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src="../assets/help/creative-commons-zero.svg"
|
src="@/assets/help/creative-commons-zero.svg"
|
||||||
alt="CC zero"
|
alt="CC zero"
|
||||||
width="20"
|
width="20"
|
||||||
style="display: inline"
|
style="display: inline"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -220,7 +220,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li @click="openGiftDialogToProject()">
|
<li @click="openGiftDialogToProject()">
|
||||||
<img
|
<img
|
||||||
src="../assets/blank-square.svg"
|
src="@/assets/blank-square.svg"
|
||||||
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
|
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<h3
|
<h3
|
||||||
|
|||||||
@@ -279,11 +279,7 @@ import ProjectIcon from "../components/ProjectIcon.vue";
|
|||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import {
|
import { didInfo, getHeaders, getPlanFromCache } from "../libs/endorserServer";
|
||||||
didInfo,
|
|
||||||
getHeaders,
|
|
||||||
getPlanFromCache,
|
|
||||||
} from "../libs/endorserServer";
|
|
||||||
import { OfferSummaryRecord, PlanData } from "../interfaces/records";
|
import { OfferSummaryRecord, PlanData } from "../interfaces/records";
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import { OnboardPage } from "../libs/util";
|
import { OnboardPage } from "../libs/util";
|
||||||
|
|||||||
@@ -74,13 +74,30 @@ import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUt
|
|||||||
test('Check activity feed - check that server is running', async ({ page }) => {
|
test('Check activity feed - check that server is running', async ({ page }) => {
|
||||||
// Load app homepage
|
// Load app homepage
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
|
||||||
|
// Wait for and dismiss onboarding dialog, with retry logic
|
||||||
|
const closeOnboarding = async () => {
|
||||||
|
const closeButton = page.getByTestId('closeOnboardingAndFinish');
|
||||||
|
if (await closeButton.isVisible()) {
|
||||||
|
await closeButton.click();
|
||||||
|
await expect(closeButton).toBeHidden();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Check that initial 10 activities have been loaded
|
// Initial dismissal
|
||||||
|
await closeOnboarding();
|
||||||
|
|
||||||
|
// Wait for network to be idle
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
|
// Check and dismiss onboarding again if it reappeared
|
||||||
|
await closeOnboarding();
|
||||||
|
|
||||||
|
// Wait for initial feed items to load
|
||||||
await expect(page.locator('ul#listLatestActivity li:nth-child(10)')).toBeVisible();
|
await expect(page.locator('ul#listLatestActivity li:nth-child(10)')).toBeVisible();
|
||||||
|
|
||||||
// Scroll down a bit to trigger loading additional activities
|
// Scroll down a bit to trigger loading additional activities
|
||||||
await page.locator('ul#listLatestActivity li:nth-child(50)').scrollIntoViewIfNeeded();
|
await page.locator('ul#listLatestActivity li:nth-child(20)').scrollIntoViewIfNeeded();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Check discover results', async ({ page }) => {
|
test('Check discover results', async ({ page }) => {
|
||||||
@@ -104,8 +121,11 @@ test('Check no-ID messaging in account', async ({ page }) => {
|
|||||||
// Check 'a friend needs to register you' notice
|
// Check 'a friend needs to register you' notice
|
||||||
await expect(page.locator('#noticeBeforeAnnounce')).toBeVisible();
|
await expect(page.locator('#noticeBeforeAnnounce')).toBeVisible();
|
||||||
|
|
||||||
// Check that there is no ID
|
// Check that there is no ID by finding the wrapper first
|
||||||
await expect(page.locator('#sectionIdentityDetails code.truncate')).toBeEmpty();
|
const didWrapper = page.locator('[data-testId="didWrapper"]');
|
||||||
|
await expect(didWrapper).toBeVisible();
|
||||||
|
const codeElement = didWrapper.locator('code[role="code"]');
|
||||||
|
await expect(codeElement).toBeEmpty();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Check ability to share contact', async ({ page }) => {
|
test('Check ability to share contact', async ({ page }) => {
|
||||||
@@ -169,7 +189,14 @@ test('Check setting name & sharing info', async ({ page }) => {
|
|||||||
test('Confirm test API setting (may fail if you are running your own Time Safari)', async ({ page }, testInfo) => {
|
test('Confirm test API setting (may fail if you are running your own Time Safari)', async ({ page }, testInfo) => {
|
||||||
// Load account view
|
// Load account view
|
||||||
await page.goto('./account');
|
await page.goto('./account');
|
||||||
await page.getByRole('heading', { name: 'Advanced' }).click();
|
|
||||||
|
// Wait for and click the Advanced heading
|
||||||
|
const advancedHeading = page.getByRole('heading', { name: 'Advanced' });
|
||||||
|
await advancedHeading.waitFor({ state: 'visible' });
|
||||||
|
await advancedHeading.click();
|
||||||
|
|
||||||
|
// Wait for the Advanced section to be fully loaded
|
||||||
|
await page.waitForLoadState('networkidle');
|
||||||
|
|
||||||
// look into the config file: if it starts Time Safari, it might say which server it should set by default
|
// look into the config file: if it starts Time Safari, it might say which server it should set by default
|
||||||
const webServer = testInfo.config.webServer;
|
const webServer = testInfo.config.webServer;
|
||||||
@@ -178,8 +205,12 @@ test('Confirm test API setting (may fail if you are running your own Time Safari
|
|||||||
const endorserTerm = endorserWords?.find(word => word.startsWith(ENDORSER_ENV_NAME + '='));
|
const endorserTerm = endorserWords?.find(word => word.startsWith(ENDORSER_ENV_NAME + '='));
|
||||||
const endorserTermInConfig = endorserTerm?.substring(ENDORSER_ENV_NAME.length + 1);
|
const endorserTermInConfig = endorserTerm?.substring(ENDORSER_ENV_NAME.length + 1);
|
||||||
|
|
||||||
const endorserServer = endorserTermInConfig || 'https://test-api.endorser.ch';
|
// Find the Claim Server input field using the label's for attribute
|
||||||
await expect(page.getByRole('textbox').nth(1)).toHaveValue(endorserServer);
|
const serverInput = page.locator('input[type="text"]').first();
|
||||||
|
await serverInput.waitFor({ state: 'visible' });
|
||||||
|
|
||||||
|
const endorserServer = endorserTermInConfig || 'https://api.endorser.ch';
|
||||||
|
await expect(serverInput).toHaveValue(endorserServer);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Check User 0 can register a random person', async ({ page }) => {
|
test('Check User 0 can register a random person', async ({ page }) => {
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
"@/db/*": ["db/*"],
|
"@/db/*": ["db/*"],
|
||||||
"@/libs/*": ["libs/*"],
|
"@/libs/*": ["libs/*"],
|
||||||
"@/constants/*": ["constants/*"],
|
"@/constants/*": ["constants/*"],
|
||||||
"@/store/*": ["store/*"]
|
"@/store/*": ["store/*"],
|
||||||
|
"@/types/*": ["types/*"]
|
||||||
},
|
},
|
||||||
"lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs
|
"lib": ["ES2020", "dom", "dom.iterable"], // Include typings for ES2020 and DOM APIs
|
||||||
},
|
},
|
||||||
|
|||||||
46
vite.config.base.ts
Normal file
46
vite.config.base.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'),
|
||||||
|
'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
|
||||||
|
'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'),
|
||||||
|
stream: 'stream-browserify',
|
||||||
|
util: 'util',
|
||||||
|
crypto: 'crypto-browserify'
|
||||||
|
},
|
||||||
|
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'],
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'],
|
||||||
|
esbuildOptions: {
|
||||||
|
define: {
|
||||||
|
global: 'globalThis'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
sourcemap: true,
|
||||||
|
target: 'esnext',
|
||||||
|
chunkSizeWarningLimit: 1000,
|
||||||
|
commonjsOptions: {
|
||||||
|
include: [/node_modules/],
|
||||||
|
transformMixedEsModules: true
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['stream', 'util', 'crypto'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
stream: 'stream',
|
||||||
|
util: 'util',
|
||||||
|
crypto: 'crypto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -36,7 +36,15 @@ export async function createBuildConfig(mode: string) {
|
|||||||
assetsDir: 'assets',
|
assetsDir: 'assets',
|
||||||
chunkSizeWarningLimit: 1000,
|
chunkSizeWarningLimit: 1000,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: isCapacitor ? ['@capacitor/app'] : []
|
external: isCapacitor ? ['@capacitor/app'] : [],
|
||||||
|
output: {
|
||||||
|
assetFileNames: (assetInfo) => {
|
||||||
|
if (assetInfo.name?.endsWith('.svg')) {
|
||||||
|
return 'assets/[name][extname]';
|
||||||
|
}
|
||||||
|
return 'assets/[name]-[hash][extname]';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
@@ -44,6 +52,8 @@ export async function createBuildConfig(mode: string) {
|
|||||||
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
||||||
'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)),
|
'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)),
|
||||||
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
|
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
|
||||||
|
'process.env.VITE_ASSET_URL': JSON.stringify(isCapacitor ? './assets/' : '/assets/'),
|
||||||
|
'process.env.VITE_BASE_URL': JSON.stringify(isCapacitor ? './' : '/')
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
156
vite.config.dev.mts.timestamp-1743747195916-245e61245d5ec.mjs
Normal file
156
vite.config.dev.mts.timestamp-1743747195916-245e61245d5ec.mjs
Normal file
File diff suppressed because one or more lines are too long
28
vite.config.mobile.ts
Normal file
28
vite.config.mobile.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig, loadEnv } from "vite";
|
||||||
|
import baseConfig from "./vite.config.base";
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseConfig,
|
||||||
|
define: {
|
||||||
|
'import.meta.env.VITE_PLATFORM': JSON.stringify('mobile'),
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
...baseConfig.build,
|
||||||
|
outDir: 'dist/mobile',
|
||||||
|
rollupOptions: {
|
||||||
|
...baseConfig.build.rollupOptions,
|
||||||
|
output: {
|
||||||
|
...baseConfig.build.rollupOptions.output,
|
||||||
|
manualChunks: {
|
||||||
|
// Mobile-specific chunk splitting
|
||||||
|
vendor: ['vue', 'vue-router', 'pinia'],
|
||||||
|
capacitor: ['@capacitor/core', '@capacitor/filesystem', '@capacitor/share'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -1,46 +1,18 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import baseConfig from "./vite.config.base";
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
export default defineConfig({
|
// https://vitejs.dev/config/
|
||||||
plugins: [vue()],
|
export default defineConfig(({ mode }) => {
|
||||||
resolve: {
|
// Load env file based on `mode` in the current working directory.
|
||||||
alias: {
|
// Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
|
||||||
'@': path.resolve(__dirname, './src'),
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'),
|
const platform = env.PLATFORM || 'web';
|
||||||
'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
|
|
||||||
'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'),
|
// Load platform-specific config
|
||||||
stream: 'stream-browserify',
|
const platformConfig = require(`./vite.config.${platform}`).default;
|
||||||
util: 'util',
|
|
||||||
crypto: 'crypto-browserify'
|
return {
|
||||||
},
|
...baseConfig,
|
||||||
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'],
|
...platformConfig,
|
||||||
},
|
};
|
||||||
optimizeDeps: {
|
|
||||||
include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'],
|
|
||||||
esbuildOptions: {
|
|
||||||
define: {
|
|
||||||
global: 'globalThis'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
sourcemap: true,
|
|
||||||
target: 'esnext',
|
|
||||||
chunkSizeWarningLimit: 1000,
|
|
||||||
commonjsOptions: {
|
|
||||||
include: [/node_modules/],
|
|
||||||
transformMixedEsModules: true
|
|
||||||
},
|
|
||||||
rollupOptions: {
|
|
||||||
external: ['stream', 'util', 'crypto'],
|
|
||||||
output: {
|
|
||||||
globals: {
|
|
||||||
stream: 'stream',
|
|
||||||
util: 'util',
|
|
||||||
crypto: 'crypto'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
@@ -1,27 +1,92 @@
|
|||||||
import { defineConfig, mergeConfig } from "vite";
|
/**
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
* @file vite.config.web.mts
|
||||||
import { createBuildConfig } from "./vite.config.common.mts";
|
* @description Vite configuration for web platform builds
|
||||||
import { loadAppConfig } from "./vite.config.utils.mts";
|
*
|
||||||
|
* This configuration file defines how the application is built for web platforms.
|
||||||
|
* It extends the base configuration with web-specific settings and optimizations.
|
||||||
|
*
|
||||||
|
* Build Process Integration:
|
||||||
|
* 1. Configuration Loading:
|
||||||
|
* - Loads environment variables based on build mode
|
||||||
|
* - Merges base configuration from vite.config.common.mts
|
||||||
|
* - Loads application-specific configuration
|
||||||
|
*
|
||||||
|
* 2. Platform Definition:
|
||||||
|
* - Sets VITE_PLATFORM environment variable to 'web'
|
||||||
|
* - Used by PlatformServiceFactory to load web-specific implementations
|
||||||
|
*
|
||||||
|
* 3. Build Output:
|
||||||
|
* - Outputs to 'dist/web' directory
|
||||||
|
* - Creates vendor chunk for Vue-related dependencies
|
||||||
|
* - Enables PWA features with auto-update capability
|
||||||
|
*
|
||||||
|
* 4. Development vs Production:
|
||||||
|
* - Development: Enables source maps and development features
|
||||||
|
* - Production: Optimizes chunks and enables PWA features
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* - Development: npm run dev
|
||||||
|
* - Production: npm run build:web
|
||||||
|
*
|
||||||
|
* @see vite.config.common.mts
|
||||||
|
* @see vite.config.utils.mts
|
||||||
|
* @see PlatformServiceFactory.ts
|
||||||
|
*/
|
||||||
|
|
||||||
export default defineConfig(async () => {
|
import { defineConfig } from "vite";
|
||||||
const baseConfig = await createBuildConfig('web');
|
import vue from "@vitejs/plugin-vue";
|
||||||
const appConfig = await loadAppConfig();
|
import baseConfig from "./vite.config.base";
|
||||||
|
|
||||||
return mergeConfig(baseConfig, {
|
// Define Node.js built-in modules that need browser compatibility
|
||||||
plugins: [
|
const nodeBuiltins = {
|
||||||
VitePWA({
|
stream: 'stream-browserify',
|
||||||
registerType: 'autoUpdate',
|
util: 'util',
|
||||||
manifest: appConfig.pwaConfig?.manifest,
|
crypto: 'crypto-browserify',
|
||||||
devOptions: {
|
http: 'stream-http',
|
||||||
enabled: false
|
https: 'https-browserify',
|
||||||
},
|
zlib: 'browserify-zlib',
|
||||||
workbox: {
|
url: 'url',
|
||||||
cleanupOutdatedCaches: true,
|
assert: 'assert',
|
||||||
skipWaiting: true,
|
path: 'path-browserify',
|
||||||
clientsClaim: true,
|
fs: 'browserify-fs',
|
||||||
sourcemap: true
|
tty: 'tty-browserify'
|
||||||
}
|
};
|
||||||
})
|
|
||||||
]
|
export default defineConfig({
|
||||||
});
|
...baseConfig,
|
||||||
|
plugins: [vue()],
|
||||||
|
optimizeDeps: {
|
||||||
|
...baseConfig.optimizeDeps,
|
||||||
|
include: [...(baseConfig.optimizeDeps?.include || []), 'qrcode.vue'],
|
||||||
|
exclude: Object.keys(nodeBuiltins),
|
||||||
|
esbuildOptions: {
|
||||||
|
define: {
|
||||||
|
global: 'globalThis'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
...baseConfig.resolve,
|
||||||
|
alias: {
|
||||||
|
...baseConfig.resolve?.alias,
|
||||||
|
...nodeBuiltins
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
...baseConfig.build,
|
||||||
|
commonjsOptions: {
|
||||||
|
...baseConfig.build?.commonjsOptions,
|
||||||
|
include: [/node_modules/],
|
||||||
|
exclude: [/src\/services\/platforms\/electron/],
|
||||||
|
transformMixedEsModules: true
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
...baseConfig.build?.rollupOptions,
|
||||||
|
external: Object.keys(nodeBuiltins),
|
||||||
|
output: {
|
||||||
|
...baseConfig.build?.rollupOptions?.output,
|
||||||
|
globals: nodeBuiltins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user