forked from jsnbuchanan/crowd-funder-for-time-pwa
Merge branch 'deep_linking'
This commit is contained in:
118
src/App.vue
118
src/App.vue
@@ -40,7 +40,10 @@
|
||||
<div
|
||||
class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
|
||||
>
|
||||
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="fa-fw fa-xl"
|
||||
></font-awesome>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
|
||||
@@ -48,10 +51,10 @@
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
|
||||
<button
|
||||
@click="close(notification.id)"
|
||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
|
||||
@click="close(notification.id)"
|
||||
>
|
||||
<fa icon="xmark" class="fa-fw"></fa>
|
||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +66,10 @@
|
||||
<div
|
||||
class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
|
||||
>
|
||||
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="fa-fw fa-xl"
|
||||
></font-awesome>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
|
||||
@@ -71,10 +77,10 @@
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
|
||||
<button
|
||||
@click="close(notification.id)"
|
||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
|
||||
@click="close(notification.id)"
|
||||
>
|
||||
<fa icon="xmark" class="fa-fw"></fa>
|
||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,7 +92,10 @@
|
||||
<div
|
||||
class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
|
||||
>
|
||||
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
|
||||
<font-awesome
|
||||
icon="triangle-exclamation"
|
||||
class="fa-fw fa-xl"
|
||||
></font-awesome>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
|
||||
@@ -94,10 +103,10 @@
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
|
||||
<button
|
||||
@click="close(notification.id)"
|
||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
|
||||
@click="close(notification.id)"
|
||||
>
|
||||
<fa icon="xmark" class="fa-fw"></fa>
|
||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,7 +118,10 @@
|
||||
<div
|
||||
class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
|
||||
>
|
||||
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
|
||||
<font-awesome
|
||||
icon="triangle-exclamation"
|
||||
class="fa-fw fa-xl"
|
||||
></font-awesome>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
|
||||
@@ -117,10 +129,10 @@
|
||||
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
|
||||
|
||||
<button
|
||||
@click="close(notification.id)"
|
||||
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
|
||||
@click="close(notification.id)"
|
||||
>
|
||||
<fa icon="xmark" class="fa-fw"></fa>
|
||||
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +143,8 @@
|
||||
|
||||
<!--
|
||||
This "group" of "modal" is the prompt for an answer.
|
||||
Set "type" as follows: "confirm" for yes/no, and "notification" ones: "-permission", "-mute", "-off"
|
||||
Set "type" as follows: "confirm" for yes/no, and "notification" ones:
|
||||
"-permission", "-mute", "-off"
|
||||
-->
|
||||
<NotificationGroup group="modal">
|
||||
<div class="fixed z-[100] top-0 inset-x-0 w-full">
|
||||
@@ -174,11 +187,11 @@
|
||||
|
||||
<button
|
||||
v-if="notification.onYes"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
@click="
|
||||
notification.onYes();
|
||||
close(notification.id);
|
||||
"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
>
|
||||
Yes{{
|
||||
notification.yesText ? ", " + notification.yesText : ""
|
||||
@@ -187,12 +200,12 @@
|
||||
|
||||
<button
|
||||
v-if="notification.onNo"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
@click="
|
||||
notification.onNo(stopAsking);
|
||||
close(notification.id);
|
||||
stopAsking = false; // reset value
|
||||
"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
>
|
||||
No{{ notification.noText ? ", " + notification.noText : "" }}
|
||||
</button>
|
||||
@@ -209,8 +222,8 @@
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="stopAsking"
|
||||
type="checkbox"
|
||||
name="stopAsking"
|
||||
class="sr-only"
|
||||
/>
|
||||
@@ -224,6 +237,7 @@
|
||||
</label>
|
||||
|
||||
<button
|
||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||
@click="
|
||||
notification.onCancel
|
||||
? notification.onCancel(stopAsking)
|
||||
@@ -231,7 +245,6 @@
|
||||
close(notification.id);
|
||||
stopAsking = false; // reset value for next time they open this modal
|
||||
"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||
>
|
||||
{{ notification.onYes ? "Cancel" : "Close" }}
|
||||
</button>
|
||||
@@ -270,8 +283,8 @@
|
||||
Until I turn it back on
|
||||
</button>
|
||||
<button
|
||||
@click="close(notification.id)"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||
@click="close(notification.id)"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -292,17 +305,17 @@
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
@click="
|
||||
close(notification.id);
|
||||
turnOffNotifications(notification);
|
||||
"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
>
|
||||
Turn Off Notification
|
||||
</button>
|
||||
<button
|
||||
@click="close(notification.id)"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||
@click="close(notification.id)"
|
||||
>
|
||||
Leave it On
|
||||
</button>
|
||||
@@ -315,12 +328,11 @@
|
||||
</NotificationGroup>
|
||||
</template>
|
||||
|
||||
<style></style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
|
||||
import { NotificationIface } from "./constants/app";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
interface Settings {
|
||||
notifyingNewActivityTime?: string;
|
||||
@@ -334,38 +346,38 @@ export default class App extends Vue {
|
||||
stopAsking = false;
|
||||
|
||||
// created() {
|
||||
// console.log(
|
||||
// logger.log(
|
||||
// "Component created: Reactivity set up.",
|
||||
// window.location.pathname,
|
||||
// );
|
||||
// }
|
||||
|
||||
// beforeCreate() {
|
||||
// console.log("Component beforeCreate: Instance initialized.");
|
||||
// logger.log("Component beforeCreate: Instance initialized.");
|
||||
// }
|
||||
|
||||
// beforeMount() {
|
||||
// console.log("Component beforeMount: Template is about to be rendered.");
|
||||
// logger.log("Component beforeMount: Template is about to be rendered.");
|
||||
// }
|
||||
|
||||
// mounted() {
|
||||
// console.log("Component mounted: Template is now rendered.");
|
||||
// logger.log("Component mounted: Template is now rendered.");
|
||||
// }
|
||||
|
||||
// beforeUpdate() {
|
||||
// console.log("Component beforeUpdate: DOM is about to be updated.");
|
||||
// logger.log("Component beforeUpdate: DOM is about to be updated.");
|
||||
// }
|
||||
|
||||
// updated() {
|
||||
// console.log("Component updated: DOM has been updated.");
|
||||
// logger.log("Component updated: DOM has been updated.");
|
||||
// }
|
||||
|
||||
// beforeUnmount() {
|
||||
// console.log("Component beforeUnmount: Cleaning up before removal.");
|
||||
// logger.log("Component beforeUnmount: Cleaning up before removal.");
|
||||
// }
|
||||
|
||||
// unmounted() {
|
||||
// console.log("Component unmounted: Component removed from the DOM.");
|
||||
// logger.log("Component unmounted: Component removed from the DOM.");
|
||||
// }
|
||||
|
||||
truncateLongWords(sentence: string) {
|
||||
@@ -378,42 +390,42 @@ export default class App extends Vue {
|
||||
async turnOffNotifications(
|
||||
notification: NotificationIface,
|
||||
): Promise<boolean> {
|
||||
console.log("Starting turnOffNotifications...");
|
||||
logger.log("Starting turnOffNotifications...");
|
||||
let subscription: PushSubscriptionJSON | null = null;
|
||||
let allGoingOff = false;
|
||||
|
||||
try {
|
||||
console.log("Retrieving settings for the active account...");
|
||||
logger.log("Retrieving settings for the active account...");
|
||||
const settings: Settings = await retrieveSettingsForActiveAccount();
|
||||
console.log("Retrieved settings:", settings);
|
||||
logger.log("Retrieved settings:", settings);
|
||||
|
||||
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
||||
const notifyingReminder = !!settings?.notifyingReminderTime;
|
||||
|
||||
if (!notifyingNewActivity || !notifyingReminder) {
|
||||
allGoingOff = true;
|
||||
console.log("Both notifications are being turned off.");
|
||||
logger.log("Both notifications are being turned off.");
|
||||
}
|
||||
|
||||
console.log("Checking service worker readiness...");
|
||||
logger.log("Checking service worker readiness...");
|
||||
await navigator.serviceWorker?.ready
|
||||
.then((registration) => {
|
||||
console.log("Service worker is ready. Fetching subscription...");
|
||||
logger.log("Service worker is ready. Fetching subscription...");
|
||||
return registration.pushManager.getSubscription();
|
||||
})
|
||||
.then(async (subscript: PushSubscription | null) => {
|
||||
if (subscript) {
|
||||
subscription = subscript.toJSON();
|
||||
console.log("PushSubscription retrieved:", subscription);
|
||||
logger.log("PushSubscription retrieved:", subscription);
|
||||
|
||||
if (allGoingOff) {
|
||||
console.log("Unsubscribing from push notifications...");
|
||||
logger.log("Unsubscribing from push notifications...");
|
||||
await subscript.unsubscribe();
|
||||
console.log("Successfully unsubscribed.");
|
||||
logger.log("Successfully unsubscribed.");
|
||||
}
|
||||
} else {
|
||||
logConsoleAndDb("Subscription object is not available.");
|
||||
console.log("No subscription found.");
|
||||
logger.log("No subscription found.");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -422,11 +434,11 @@ export default class App extends Vue {
|
||||
JSON.stringify(error),
|
||||
true,
|
||||
);
|
||||
console.error("Error during subscription fetch:", error);
|
||||
logger.error("Error during subscription fetch:", error);
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
console.log("No subscription available. Notifying user...");
|
||||
logger.log("No subscription available. Notifying user...");
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -436,7 +448,7 @@ export default class App extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
console.log("Exiting as there is no subscription to process.");
|
||||
logger.log("Exiting as there is no subscription to process.");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -445,12 +457,12 @@ export default class App extends Vue {
|
||||
};
|
||||
if (!allGoingOff) {
|
||||
serverSubscription["notifyType"] = notification.title;
|
||||
console.log(
|
||||
logger.log(
|
||||
`Server subscription updated with notifyType: ${notification.title}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Sending unsubscribe request to the server...");
|
||||
logger.log("Sending unsubscribe request to the server...");
|
||||
const pushServerSuccess = await fetch("/web-push/unsubscribe", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -465,9 +477,9 @@ export default class App extends Vue {
|
||||
`Push server failed: ${response.status} ${errorBody}`,
|
||||
true,
|
||||
);
|
||||
console.error("Push server error response:", errorBody);
|
||||
logger.error("Push server error response:", errorBody);
|
||||
}
|
||||
console.log(`Server response status: ${response.status}`);
|
||||
logger.log(`Server response status: ${response.status}`);
|
||||
return response.ok;
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -475,14 +487,14 @@ export default class App extends Vue {
|
||||
"Push server communication failed: " + JSON.stringify(error),
|
||||
true,
|
||||
);
|
||||
console.error("Error during server communication:", error);
|
||||
logger.error("Error during server communication:", error);
|
||||
return false;
|
||||
});
|
||||
|
||||
const message = pushServerSuccess
|
||||
? "Notification is off."
|
||||
: "Notification is still on. Try to turn it off again.";
|
||||
console.log("Server response processed. Message:", message);
|
||||
logger.log("Server response processed. Message:", message);
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
@@ -495,11 +507,11 @@ export default class App extends Vue {
|
||||
);
|
||||
|
||||
if (notification.callback) {
|
||||
console.log("Executing notification callback...");
|
||||
logger.log("Executing notification callback...");
|
||||
notification.callback(pushServerSuccess);
|
||||
}
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
"Completed turnOffNotifications with success:",
|
||||
pushServerSuccess,
|
||||
);
|
||||
@@ -509,7 +521,7 @@ export default class App extends Vue {
|
||||
"Error turning off notifications: " + JSON.stringify(error),
|
||||
true,
|
||||
);
|
||||
console.error("Critical error in turnOffNotifications:", error);
|
||||
logger.error("Critical error in turnOffNotifications:", error);
|
||||
|
||||
this.$notify(
|
||||
{
|
||||
@@ -526,3 +538,5 @@ export default class App extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
||||
@@ -29,29 +29,29 @@
|
||||
<p class="text-sm mb-2">{{ text }}</p>
|
||||
|
||||
<button
|
||||
@click="handleOption1(close)"
|
||||
class="block w-full text-center text-md font-bold capitalize bg-blue-800 text-white px-2 py-2 rounded-md mb-2"
|
||||
@click="handleOption1(close)"
|
||||
>
|
||||
{{ option1Text }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="handleOption2(close)"
|
||||
class="block w-full text-center text-md font-bold capitalize bg-blue-700 text-white px-2 py-2 rounded-md mb-2"
|
||||
@click="handleOption2(close)"
|
||||
>
|
||||
{{ option2Text }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="handleOption3(close)"
|
||||
class="block w-full text-center text-md font-bold capitalize bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
|
||||
@click="handleOption3(close)"
|
||||
>
|
||||
{{ option3Text }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="handleCancel(close)"
|
||||
class="block w-full text-center text-md font-bold capitalize bg-slate-600 text-white px-2 py-2 rounded-md"
|
||||
@click="handleCancel(close)"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
{{ message }}
|
||||
Note that their name is only stored on this device.
|
||||
<input
|
||||
v-model="newText"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
v-model="newText"
|
||||
/>
|
||||
|
||||
<div class="mt-8">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div v-html="generateIcon()" class="w-fit"></div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div class="w-fit" v-html="generateIcon()"></div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { createAvatar, StyleOptions } from "@dicebear/core";
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="hasVisibleDid"
|
||||
type="checkbox"
|
||||
name="toggleFilterFromMyContacts"
|
||||
class="sr-only"
|
||||
/>
|
||||
@@ -46,8 +46,8 @@
|
||||
<div v-if="hasSearchBox" class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="isNearby"
|
||||
type="checkbox"
|
||||
name="toggleFilterNearby"
|
||||
class="sr-only"
|
||||
/>
|
||||
@@ -98,7 +98,7 @@ import {
|
||||
LRectangle,
|
||||
LTileLayer,
|
||||
} from "@vue-leaflet/vue-leaflet";
|
||||
|
||||
import { Router } from "vue-router";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
|
||||
@@ -111,6 +111,7 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
},
|
||||
})
|
||||
export default class FeedFilters extends Vue {
|
||||
$router!: Router;
|
||||
onCloseIfChanged = () => {};
|
||||
hasSearchBox = false;
|
||||
hasVisibleDid = false;
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
{{ customTitle }}
|
||||
</h1>
|
||||
<input
|
||||
v-model="description"
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
:placeholder="prompt || 'What was given?'"
|
||||
v-model="description"
|
||||
/>
|
||||
<div class="flex flex-row justify-center">
|
||||
<span
|
||||
@@ -21,19 +21,19 @@
|
||||
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="amountInput === '0' ? null : decrement()"
|
||||
>
|
||||
<fa icon="chevron-left" />
|
||||
<font-awesome icon="chevron-left" />
|
||||
</div>
|
||||
<input
|
||||
id="inputGivenAmount"
|
||||
v-model="amountInput"
|
||||
type="number"
|
||||
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
|
||||
v-model="amountInput"
|
||||
/>
|
||||
<div
|
||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="increment()"
|
||||
>
|
||||
<fa icon="chevron-right" />
|
||||
<font-awesome icon="chevron-right" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-center">
|
||||
@@ -62,7 +62,7 @@
|
||||
</div>
|
||||
<p class="text-center mb-2 mt-6 italic">
|
||||
Sign & Send to publish to the world
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
@click="explainData()"
|
||||
@@ -162,7 +162,7 @@ export default class GiftedDialog extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error retrieving settings from database:", err);
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -311,7 +311,7 @@ export default class GiftedDialog extends Vue {
|
||||
this.isGiveCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
console.error("Error with give creation result:", result);
|
||||
logger.error("Error with give creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -337,7 +337,7 @@ export default class GiftedDialog extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Error with give recordation caught:", error);
|
||||
logger.error("Error with give recordation caught:", error);
|
||||
const errorMessage =
|
||||
error.userMessage ||
|
||||
serverMessageForUser(error) ||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
|
||||
@click="cancel"
|
||||
>
|
||||
<fa icon="xmark" class="w-[1em]"></fa>
|
||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
||||
</div>
|
||||
</h1>
|
||||
<span class="mt-2 flex justify-between">
|
||||
@@ -16,7 +16,7 @@
|
||||
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"
|
||||
@click="prevIdea()"
|
||||
>
|
||||
<fa icon="chevron-left" class="m-auto" />
|
||||
<font-awesome icon="chevron-left" class="m-auto" />
|
||||
</span>
|
||||
|
||||
<div class="m-2">
|
||||
@@ -45,7 +45,7 @@
|
||||
class="text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4"
|
||||
@click="nextIdeaPastContacts()"
|
||||
>
|
||||
Skip Contacts <fa icon="forward" />
|
||||
Skip Contacts <font-awesome icon="forward" />
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
@@ -57,7 +57,7 @@
|
||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2 flex"
|
||||
@click="nextIdea()"
|
||||
>
|
||||
<fa icon="chevron-right" class="m-auto" />
|
||||
<font-awesome icon="chevron-right" class="m-auto" />
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
@@ -82,6 +82,7 @@ import { GiverReceiverInputInfo } from "../libs/util";
|
||||
@Component
|
||||
export default class GivenPrompts extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
CATEGORY_CONTACTS = 1;
|
||||
CATEGORY_IDEAS = 0;
|
||||
@@ -145,7 +146,7 @@ export default class GivenPrompts extends Vue {
|
||||
// proceed with logic but don't change values (just in case some actions are added later)
|
||||
this.visible = false;
|
||||
if (this.currentCategory === this.CATEGORY_IDEAS) {
|
||||
(this.$router as Router).push({
|
||||
this.$router.push({
|
||||
name: "contact-gift",
|
||||
query: {
|
||||
prompt: this.IDEAS[this.currentIdeaIndex],
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold capitalize">{{ roleName }} Details</h2>
|
||||
<button @click="close" class="text-gray-500 hover:text-gray-700">
|
||||
<fa icon="times" />
|
||||
<button class="text-gray-500 hover:text-gray-700" @click="close">
|
||||
<font-awesome icon="times" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,10 @@
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
@@ -66,7 +69,7 @@
|
||||
<div class="mt-4">
|
||||
<span v-if="canShare">
|
||||
If you'd like an introduction,
|
||||
<a @click="onClickShareClaim()" class="text-blue-500"
|
||||
<a class="text-blue-500" @click="onClickShareClaim()"
|
||||
>click here to share the information with them and ask if they'll
|
||||
tell you more about the {{ roleName }}.</a
|
||||
>
|
||||
@@ -74,8 +77,8 @@
|
||||
<span v-else>
|
||||
If you'd like an introduction,
|
||||
<a
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
class="text-blue-500"
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
>click here to copy this page, paste it into a message, and ask if
|
||||
they'll tell you more about the {{ roleName }}.</a
|
||||
>
|
||||
@@ -86,8 +89,8 @@
|
||||
<!-- Footer -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
@click="close"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
||||
@click="close"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
||||
@click="close()"
|
||||
>
|
||||
<fa icon="xmark" class="w-[1em]"></fa>
|
||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-center mt-8">
|
||||
<div>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="camera"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
|
||||
@click="openPhotoDialog()"
|
||||
@@ -31,17 +31,21 @@
|
||||
<div class="mt-4">
|
||||
<span class="mt-2">
|
||||
... or paste a URL:
|
||||
<input type="text" v-model="imageUrl" class="border-2" />
|
||||
<input v-model="imageUrl" type="text" class="border-2" />
|
||||
</span>
|
||||
<span class="ml-2">
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="imageUrl"
|
||||
icon="check"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md cursor-pointer"
|
||||
@click="acceptUrl"
|
||||
/>
|
||||
<!-- so that there's no shifting when it becomes visible -->
|
||||
<fa v-else icon="check" class="text-white bg-white px-2 py-2" />
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="check"
|
||||
class="text-white bg-white px-2 py-2"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
class="text-white text-2xl p-2 rounded-full hover:bg-white/10"
|
||||
@click="close"
|
||||
>
|
||||
<fa icon="xmark" />
|
||||
<font-awesome icon="xmark" />
|
||||
</button>
|
||||
|
||||
<!-- Mobile share button -->
|
||||
@@ -17,7 +17,7 @@
|
||||
class="text-white text-xl p-2 rounded-full hover:bg-white/10"
|
||||
@click="handleShare"
|
||||
>
|
||||
<fa icon="ellipsis" />
|
||||
<font-awesome icon="ellipsis" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
<img
|
||||
:src="imageUrl"
|
||||
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
|
||||
@click.stop
|
||||
alt="expanded shared content"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@ export default class ImageViewer extends Vue {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Share failed, opening in new tab:", error);
|
||||
logger.warn("Share failed, opening in new tab:", error);
|
||||
window.open(this.imageUrl, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,18 +8,18 @@
|
||||
If you want to store your own way, the invitation ID is:
|
||||
{{ inviteIdentifier }}
|
||||
<input
|
||||
v-model="text"
|
||||
type="text"
|
||||
placeholder="Notes"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
v-model="text"
|
||||
/>
|
||||
|
||||
<!-- Add date selection element -->
|
||||
Expiration
|
||||
<input
|
||||
v-model="expiresAt"
|
||||
type="date"
|
||||
class="block rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
v-model="expiresAt"
|
||||
/>
|
||||
|
||||
<div class="mt-8">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-if="isLoading"
|
||||
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
|
||||
>
|
||||
<fa icon="spinner" class="fa-spin-pulse" />
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
|
||||
<!-- Members List -->
|
||||
@@ -33,13 +33,13 @@
|
||||
<span
|
||||
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
|
||||
>
|
||||
<fa icon="plus" class="text-sm" />
|
||||
<font-awesome icon="plus" class="text-sm" />
|
||||
</span>
|
||||
/
|
||||
<span
|
||||
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
|
||||
>
|
||||
<fa icon="minus" class="text-sm" />
|
||||
<font-awesome icon="minus" class="text-sm" />
|
||||
</span>
|
||||
to add/remove them to/from the meeting.
|
||||
</span>
|
||||
@@ -54,20 +54,23 @@
|
||||
<span
|
||||
class="mx-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600"
|
||||
>
|
||||
<fa icon="circle-user" class="text-xl" />
|
||||
<font-awesome icon="circle-user" class="text-xl" />
|
||||
</span>
|
||||
to add them to your contacts.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<!-- always have at least one refresh button even without members in case the organizer changes the password -->
|
||||
<!--
|
||||
always have at least one refresh button even without members in case the organizer
|
||||
changes the password
|
||||
-->
|
||||
<button
|
||||
@click="fetchMembers"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
|
||||
title="Refresh members list"
|
||||
@click="fetchMembers"
|
||||
>
|
||||
<fa icon="rotate" :class="{ 'fa-spin': isLoading }" />
|
||||
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
@@ -83,24 +86,24 @@
|
||||
class="flex justify-end"
|
||||
>
|
||||
<button
|
||||
@click="addAsContact(member)"
|
||||
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600 hover:bg-green-200 hover:text-green-800 transition-colors"
|
||||
title="Add as contact"
|
||||
@click="addAsContact(member)"
|
||||
>
|
||||
<fa icon="circle-user" class="text-xl" />
|
||||
<font-awesome icon="circle-user" class="text-xl" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-if="member.did !== activeDid"
|
||||
class="ml-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors"
|
||||
title="Contact info"
|
||||
@click="
|
||||
informAboutAddingContact(
|
||||
getContactFor(member.did) !== undefined,
|
||||
)
|
||||
"
|
||||
class="ml-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors"
|
||||
title="Contact info"
|
||||
>
|
||||
<fa icon="circle-info" class="text-base" />
|
||||
<font-awesome icon="circle-info" class="text-base" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex">
|
||||
@@ -111,23 +114,23 @@
|
||||
class="flex items-center"
|
||||
>
|
||||
<button
|
||||
@click="checkWhetherContactBeforeAdmitting(member)"
|
||||
class="mr-2 w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
|
||||
:title="
|
||||
member.member.admitted ? 'Remove member' : 'Admit member'
|
||||
"
|
||||
@click="checkWhetherContactBeforeAdmitting(member)"
|
||||
>
|
||||
<fa
|
||||
<font-awesome
|
||||
:icon="member.member.admitted ? 'minus' : 'plus'"
|
||||
class="text-sm"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
@click="informAboutAdmission()"
|
||||
class="mr-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors"
|
||||
title="Admission info"
|
||||
@click="informAboutAdmission()"
|
||||
>
|
||||
<fa icon="circle-info" class="text-base" />
|
||||
<font-awesome icon="circle-info" class="text-base" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -138,11 +141,11 @@
|
||||
</div>
|
||||
<div v-if="membersToShow().length > 0" class="flex justify-center mt-4">
|
||||
<button
|
||||
@click="fetchMembers"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
|
||||
title="Refresh members list"
|
||||
@click="fetchMembers"
|
||||
>
|
||||
<fa icon="rotate" :class="{ 'fa-spin': isLoading }" />
|
||||
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<div class="dialog">
|
||||
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1>
|
||||
<input
|
||||
v-model="description"
|
||||
type="text"
|
||||
data-testId="inputDescription"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
placeholder="Description of what is offered"
|
||||
v-model="description"
|
||||
/>
|
||||
<div class="flex flex-row mt-2">
|
||||
<span
|
||||
@@ -17,23 +17,23 @@
|
||||
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
|
||||
</span>
|
||||
<div
|
||||
v-if="amountInput !== '0'"
|
||||
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="decrement()"
|
||||
v-if="amountInput !== '0'"
|
||||
>
|
||||
<fa icon="chevron-left" />
|
||||
<font-awesome icon="chevron-left" />
|
||||
</div>
|
||||
<input
|
||||
v-model="amountInput"
|
||||
data-testId="inputOfferAmount"
|
||||
type="number"
|
||||
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
|
||||
v-model="amountInput"
|
||||
/>
|
||||
<div
|
||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="increment()"
|
||||
>
|
||||
<fa icon="chevron-right" />
|
||||
<font-awesome icon="chevron-right" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-center">
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@Component
|
||||
export default class OfferDialog extends Vue {
|
||||
@@ -121,7 +122,7 @@ export default class OfferDialog extends Vue {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error retrieving settings from database:", err);
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -249,7 +250,7 @@ export default class OfferDialog extends Vue {
|
||||
this.isOfferCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
||||
console.error("Error with offer creation result:", result);
|
||||
logger.error("Error with offer creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -272,7 +273,7 @@ export default class OfferDialog extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Error with offer recordation caught:", error);
|
||||
logger.error("Error with offer recordation caught:", error);
|
||||
const message =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class="text-lg text-center leading-none absolute right-0 -top-1"
|
||||
@click="onClickClose(true)"
|
||||
>
|
||||
<fa icon="xmark" class="w-[1em]" />
|
||||
<font-awesome icon="xmark" class="w-[1em]" />
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</span>
|
||||
click on the
|
||||
<span class="bg-green-600 text-white rounded-full">
|
||||
<fa icon="plus" class="fa-fw" />
|
||||
<font-awesome icon="plus" class="fa-fw" />
|
||||
</span>
|
||||
button to express your appreciation for... whatever -- maybe thanks for
|
||||
showing you all these fascinating stories of
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<p class="mt-4 flex items-center">
|
||||
The
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="house-chimney"
|
||||
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
|
||||
/>
|
||||
@@ -84,7 +84,7 @@
|
||||
class="text-lg text-center leading-none absolute right-0 -top-1"
|
||||
@click="onClickClose(true)"
|
||||
>
|
||||
<fa icon="xmark" class="w-[1em]" />
|
||||
<font-awesome icon="xmark" class="w-[1em]" />
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
|
||||
<p class="mt-4 flex items-center">
|
||||
The
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="magnifying-glass"
|
||||
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
|
||||
/>
|
||||
@@ -141,14 +141,14 @@
|
||||
class="text-lg text-center leading-none absolute right-0 -top-1"
|
||||
@click="onClickClose(true)"
|
||||
>
|
||||
<fa icon="xmark" class="w-[1em]" />
|
||||
<font-awesome icon="xmark" class="w-[1em]" />
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
<p class="relative">
|
||||
Now you can take a turn: click on the
|
||||
<span class="bg-green-600 text-white rounded-full">
|
||||
<fa icon="plus" class="fa-fw" />
|
||||
<font-awesome icon="plus" class="fa-fw" />
|
||||
</span>
|
||||
button to throw out projects of your own... anything you'd like to see
|
||||
happen. If your first idea doesn't catch anyone, try, try again... and
|
||||
@@ -157,7 +157,7 @@
|
||||
|
||||
<p class="mt-4 flex items-center">
|
||||
The
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="hand"
|
||||
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
|
||||
/>
|
||||
@@ -168,7 +168,7 @@
|
||||
By the way, one good way to get to know your neighbors and their
|
||||
interests is to offer time directly to them. You can do this on the
|
||||
contacts screen
|
||||
<fa icon="users" class="text-slate-500" />
|
||||
<font-awesome icon="users" class="text-slate-500" />
|
||||
which is a great way to get to know a neighbor's interests.
|
||||
</p>
|
||||
|
||||
@@ -219,6 +219,7 @@ import { OnboardPage } from "../libs/util";
|
||||
})
|
||||
export default class OnboardingDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
firstContactName = null;
|
||||
@@ -254,7 +255,7 @@ export default class OnboardingDialog extends Vue {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
if (goHome) {
|
||||
(this.$router as Router).push({ name: "home" });
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
|
||||
@click="close()"
|
||||
>
|
||||
<fa icon="xmark" class="w-[1em]"></fa>
|
||||
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="uploading" class="flex justify-center">
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="fa-spin fa-3x text-center block px-12 py-12"
|
||||
/>
|
||||
@@ -28,7 +28,7 @@
|
||||
<div v-else-if="blob">
|
||||
<div v-if="crop">
|
||||
<VuePictureCropper
|
||||
:boxStyle="{
|
||||
:box-style="{
|
||||
backgroundColor: '#f8f8f8',
|
||||
margin: 'auto',
|
||||
}"
|
||||
@@ -56,8 +56,8 @@
|
||||
</div>
|
||||
<div class="absolute bottom-[1rem] left-[1rem] px-2 py-1">
|
||||
<button
|
||||
@click="uploadImage"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
|
||||
@click="uploadImage"
|
||||
>
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
@@ -67,8 +67,8 @@
|
||||
class="absolute bottom-[1rem] right-[1rem] px-2 py-1"
|
||||
>
|
||||
<button
|
||||
@click="retryImage"
|
||||
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
|
||||
@click="retryImage"
|
||||
>
|
||||
<span>Retry</span>
|
||||
</button>
|
||||
@@ -81,37 +81,37 @@
|
||||
:resolution="{ width: 375, height: 812 }"
|
||||
-->
|
||||
<camera
|
||||
facingMode="environment"
|
||||
autoplay
|
||||
ref="camera"
|
||||
facing-mode="environment"
|
||||
autoplay
|
||||
@started="cameraStarted()"
|
||||
>
|
||||
<div
|
||||
class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
|
||||
>
|
||||
<button
|
||||
@click="takeImage()"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="takeImage()"
|
||||
>
|
||||
<fa icon="camera" class="w-[1em]"></fa>
|
||||
<font-awesome icon="camera" class="w-[1em]"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
|
||||
>
|
||||
<button
|
||||
@click="swapMirrorClass()"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="swapMirrorClass()"
|
||||
>
|
||||
<fa icon="left-right" class="w-[1em]"></fa>
|
||||
<font-awesome icon="left-right" class="w-[1em]"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="numDevices > 1" class="absolute bottom-2 right-4">
|
||||
<button
|
||||
@click="switchCamera()"
|
||||
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
|
||||
@click="switchCamera()"
|
||||
>
|
||||
<fa icon="rotate" class="w-[1em]"></fa>
|
||||
<font-awesome icon="rotate" class="w-[1em]"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
</camera>
|
||||
@@ -129,6 +129,7 @@ import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@Component({ components: { Camera, VuePictureCropper } })
|
||||
export default class PhotoDialog extends Vue {
|
||||
@@ -155,7 +156,7 @@ export default class PhotoDialog extends Vue {
|
||||
this.activeDid = settings.activeDid || "";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error retrieving settings from database:", err);
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -373,7 +374,7 @@ export default class PhotoDialog extends Vue {
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
) {
|
||||
console.log(
|
||||
logger.log(
|
||||
"Using shared image API server, so only users on that server can play with images.",
|
||||
);
|
||||
}
|
||||
@@ -387,7 +388,7 @@ export default class PhotoDialog extends Vue {
|
||||
this.close();
|
||||
this.setImageCallback(response.data.url as string);
|
||||
} catch (error) {
|
||||
console.error("Error uploading the image", error);
|
||||
logger.error("Error uploading the image", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<a
|
||||
v-if="linkToFull && imageUrl"
|
||||
@@ -5,12 +6,12 @@
|
||||
target="_blank"
|
||||
class="h-full w-full object-contain"
|
||||
>
|
||||
<div v-html="generateIdenticon()" class="h-full w-full object-contain" />
|
||||
<div class="h-full w-full object-contain" v-html="generateIdenticon()" />
|
||||
</a>
|
||||
<div
|
||||
v-else
|
||||
v-html="generateIdenticon()"
|
||||
class="h-full w-full object-contain"
|
||||
v-html="generateIdenticon()"
|
||||
/>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</p>
|
||||
<p v-else class="text-lg mb-4">
|
||||
Waiting for system initialization, which may take up to 5 seconds...
|
||||
<fa icon="spinner" spin />
|
||||
<font-awesome icon="spinner" spin />
|
||||
</p>
|
||||
|
||||
<div v-if="serviceWorkerReady && vapidKey">
|
||||
@@ -54,23 +54,25 @@
|
||||
<span class="flex flex-row justify-center">
|
||||
<span class="mt-2">... at: </span>
|
||||
<input
|
||||
type="number"
|
||||
@change="checkHourInput"
|
||||
class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
|
||||
v-model="hourInput"
|
||||
type="number"
|
||||
class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
|
||||
@change="checkHourInput"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
@change="checkMinuteInput"
|
||||
class="border border-slate-400 mt-2 px-2 py-2 text-center w-20"
|
||||
v-model="minuteInput"
|
||||
type="number"
|
||||
class="border border-slate-400 mt-2 px-2 py-2 text-center w-20"
|
||||
@change="checkMinuteInput"
|
||||
/>
|
||||
<span
|
||||
class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20"
|
||||
@click="hourAm = !hourAm"
|
||||
>
|
||||
<span v-if="hourAm"> AM <fa icon="chevron-down" /> </span>
|
||||
<span v-else> PM <fa icon="chevron-up" /> </span>
|
||||
<span v-if="hourAm">
|
||||
AM <font-awesome icon="chevron-down" />
|
||||
</span>
|
||||
<span v-else> PM <font-awesome icon="chevron-up" /> </span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -86,8 +88,8 @@
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="close()"
|
||||
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md"
|
||||
@click="close()"
|
||||
>
|
||||
No, Not Now
|
||||
</button>
|
||||
@@ -109,6 +111,7 @@ import {
|
||||
import { MASTER_SECRET_KEY } from "../db/tables/secret";
|
||||
import { urlBase64ToUint8Array } from "../libs/crypto/vc/util";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// Example interface for error
|
||||
interface ErrorResponse {
|
||||
@@ -493,13 +496,13 @@ export default class PushNotificationPermission extends Vue {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!("serviceWorker" in navigator && "PushManager" in window)) {
|
||||
const errorMsg = "Push messaging is not supported";
|
||||
console.warn(errorMsg);
|
||||
logger.warn(errorMsg);
|
||||
return reject(new Error(errorMsg));
|
||||
}
|
||||
|
||||
if (window.Notification.permission !== "granted") {
|
||||
const errorMsg = "Notification permission not granted";
|
||||
console.warn(errorMsg);
|
||||
logger.warn(errorMsg);
|
||||
return reject(new Error(errorMsg));
|
||||
}
|
||||
|
||||
@@ -560,7 +563,7 @@ export default class PushNotificationPermission extends Vue {
|
||||
body: JSON.stringify(subscription),
|
||||
}).then((response) => {
|
||||
if (!response.ok) {
|
||||
console.error("Bad response subscribing to web push: ", response);
|
||||
logger.error("Bad response subscribing to web push: ", response);
|
||||
throw new Error("Failed to send push subscription to server");
|
||||
}
|
||||
logConsoleAndDb("Push subscription sent to server successfully.");
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
>
|
||||
<router-link :to="{ name: 'home' }" class="block text-center py-2 px-1">
|
||||
<div class="flex flex-col items-center">
|
||||
<fa icon="house-chimney" class="fa-fw" />
|
||||
<font-awesome icon="house-chimney" class="fa-fw" />
|
||||
<span class="text-xs mt-1">feed</span>
|
||||
</div>
|
||||
</router-link>
|
||||
@@ -32,7 +32,7 @@
|
||||
class="block text-center py-2 px-1"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<fa icon="magnifying-glass" class="fa-fw" />
|
||||
<font-awesome icon="magnifying-glass" class="fa-fw" />
|
||||
<span class="text-xs mt-1">search</span>
|
||||
</div>
|
||||
</router-link>
|
||||
@@ -51,7 +51,7 @@
|
||||
class="block text-center py-2 px-1"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<fa icon="hand" class="fa-fw" />
|
||||
<font-awesome icon="hand" class="fa-fw" />
|
||||
<span class="text-xs mt-1">your work</span>
|
||||
</div>
|
||||
</router-link>
|
||||
@@ -70,7 +70,7 @@
|
||||
class="block text-center py-2 px-1"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<fa icon="users" class="fa-fw" />
|
||||
<font-awesome icon="users" class="fa-fw" />
|
||||
<span class="text-xs mt-1">contacts</span>
|
||||
</div>
|
||||
</router-link>
|
||||
@@ -89,12 +89,13 @@
|
||||
class="block text-center py-2 px-1"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<fa icon="circle-user" class="fa-fw" />
|
||||
<font-awesome icon="circle-user" class="fa-fw" />
|
||||
<!--
|
||||
We used to say "account", so we'll keep that in the code,
|
||||
but it isn't accurate because we don't hold anything for them.
|
||||
We'll say "profile" to the users.
|
||||
(Or: settings, face, registry, cache, repo, vault... or separate preferences from identity.)
|
||||
(Or: settings, face, registry, cache, repo, vault... or separate
|
||||
preferences from identity.)
|
||||
-->
|
||||
<span class="text-xs mt-1">profile</span>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
{{ sharingExplanation }}
|
||||
<input
|
||||
v-model="givenName"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
v-model="givenName"
|
||||
/>
|
||||
|
||||
<div class="mt-8">
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
|
||||
import * as TWEEN from "@tweenjs/tween.js";
|
||||
import { retrieveSettingsForActiveAccount } from "../../../../db";
|
||||
import { getHeaders } from "../../../../libs/endorserServer";
|
||||
import { logger } from "../../../../utils/logger";
|
||||
|
||||
const ANIMATION_DURATION_SECS = 10;
|
||||
const ENDORSER_ENTITY_PREFIX = "https://endorser.ch/entity/";
|
||||
@@ -82,7 +83,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
||||
},
|
||||
undefined,
|
||||
function (error) {
|
||||
console.error(error);
|
||||
logger.error(error);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -117,7 +118,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
||||
world.lights = [...world.lights, light];
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Got bad server response status & data of",
|
||||
resp.status,
|
||||
resp.data,
|
||||
@@ -128,7 +129,7 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got exception contacting server:", error);
|
||||
logger.error("Got exception contacting server:", error);
|
||||
vue.setAlert(
|
||||
"Error With Server",
|
||||
"There was a problem retrieving your claims from the server.",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "./tables/settings";
|
||||
import { Temp, TempSchema } from "./tables/temp";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// Define types for tables that hold sensitive and non-sensitive data
|
||||
type SecretTable = { secret: Table<Secret> };
|
||||
@@ -219,9 +220,9 @@ export async function logConsoleAndDb(
|
||||
isError = false,
|
||||
): Promise<void> {
|
||||
if (isError) {
|
||||
console.error(`${new Date().toISOString()} ${message}`);
|
||||
logger.error(`${new Date().toISOString()} ${message}`);
|
||||
} else {
|
||||
console.log(`${new Date().toISOString()} ${message}`);
|
||||
logger.log(`${new Date().toISOString()} ${message}`);
|
||||
}
|
||||
|
||||
await db.open();
|
||||
|
||||
@@ -20,7 +20,7 @@ export type Settings = {
|
||||
// active Decentralized ID
|
||||
activeDid?: string; // only used in the MASTER_SETTINGS_KEY entry
|
||||
|
||||
apiServer: string; // API server URL
|
||||
apiServer?: string; // API server URL
|
||||
|
||||
filterFeedByNearby?: boolean; // filter by nearby
|
||||
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { app, BrowserWindow } = require("electron");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
// Check if running in dev mode
|
||||
const isDev = process.argv.includes("--inspect");
|
||||
@@ -8,8 +9,8 @@ const isDev = process.argv.includes("--inspect");
|
||||
function createWindow() {
|
||||
// Add before createWindow function
|
||||
const preloadPath = path.join(__dirname, "preload.js");
|
||||
console.log("Checking preload path:", preloadPath);
|
||||
console.log("Preload exists:", fs.existsSync(preloadPath));
|
||||
logger.log("Checking preload path:", preloadPath);
|
||||
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
@@ -65,25 +66,25 @@ function createWindow() {
|
||||
|
||||
if (isDev) {
|
||||
// Debug info
|
||||
console.log("Debug Info:");
|
||||
console.log("Running in dev mode:", isDev);
|
||||
console.log("App is packaged:", app.isPackaged);
|
||||
console.log("Process resource path:", process.resourcesPath);
|
||||
console.log("App path:", app.getAppPath());
|
||||
console.log("__dirname:", __dirname);
|
||||
console.log("process.cwd():", process.cwd());
|
||||
logger.log("Debug Info:");
|
||||
logger.log("Running in dev mode:", isDev);
|
||||
logger.log("App is packaged:", app.isPackaged);
|
||||
logger.log("Process resource path:", process.resourcesPath);
|
||||
logger.log("App path:", app.getAppPath());
|
||||
logger.log("__dirname:", __dirname);
|
||||
logger.log("process.cwd():", process.cwd());
|
||||
}
|
||||
|
||||
const indexPath = path.join(__dirname, "www", "index.html");
|
||||
|
||||
if (isDev) {
|
||||
console.log("Loading index from:", indexPath);
|
||||
console.log("www path:", path.join(__dirname, "www"));
|
||||
console.log("www assets path:", path.join(__dirname, "www", "assets"));
|
||||
logger.log("Loading index from:", indexPath);
|
||||
logger.log("www path:", path.join(__dirname, "www"));
|
||||
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
console.error(`Index file not found at: ${indexPath}`);
|
||||
logger.error(`Index file not found at: ${indexPath}`);
|
||||
throw new Error("Index file not found");
|
||||
}
|
||||
|
||||
@@ -110,38 +111,38 @@ function createWindow() {
|
||||
mainWindow
|
||||
.loadFile(indexPath)
|
||||
.then(() => {
|
||||
console.log("Successfully loaded index.html");
|
||||
logger.log("Successfully loaded index.html");
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
console.log("DevTools opened - running in dev mode");
|
||||
logger.log("DevTools opened - running in dev mode");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to load index.html:", err);
|
||||
console.error("Attempted path:", indexPath);
|
||||
logger.error("Failed to load index.html:", err);
|
||||
logger.error("Attempted path:", indexPath);
|
||||
});
|
||||
|
||||
// Listen for console messages from the renderer
|
||||
mainWindow.webContents.on("console-message", (_event, level, message) => {
|
||||
console.log("Renderer Console:", message);
|
||||
logger.log("Renderer Console:", message);
|
||||
});
|
||||
|
||||
// Add right after creating the BrowserWindow
|
||||
mainWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
(event, errorCode, errorDescription) => {
|
||||
console.error("Page failed to load:", errorCode, errorDescription);
|
||||
logger.error("Page failed to load:", errorCode, errorDescription);
|
||||
},
|
||||
);
|
||||
|
||||
mainWindow.webContents.on("preload-error", (event, preloadPath, error) => {
|
||||
console.error("Preload script error:", preloadPath, error);
|
||||
logger.error("Preload script error:", preloadPath, error);
|
||||
});
|
||||
|
||||
mainWindow.webContents.on(
|
||||
"console-message",
|
||||
(event, level, message, line, sourceId) => {
|
||||
console.log("Renderer Console:", line, sourceId, message);
|
||||
logger.log("Renderer Console:", line, sourceId, message);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -169,5 +170,5 @@ app.on("activate", () => {
|
||||
|
||||
// Handle any errors
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.error("Uncaught Exception:", error);
|
||||
logger.error("Uncaught Exception:", error);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
const logger = {
|
||||
log: (message, ...args) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
/* eslint-disable no-console */
|
||||
console.log(message, ...args);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
},
|
||||
warn: (message, ...args) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
/* eslint-disable no-console */
|
||||
console.warn(message, ...args);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
},
|
||||
error: (message, ...args) => {
|
||||
/* eslint-disable no-console */
|
||||
console.error(message, ...args); // Errors should always be logged
|
||||
/* eslint-enable no-console */
|
||||
},
|
||||
};
|
||||
|
||||
// Use a more direct path resolution approach
|
||||
const getPath = (pathType) => {
|
||||
switch (pathType) {
|
||||
@@ -19,7 +41,7 @@ const getPath = (pathType) => {
|
||||
}
|
||||
};
|
||||
|
||||
console.log("Preload script starting...");
|
||||
logger.log("Preload script starting...");
|
||||
|
||||
try {
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
@@ -50,7 +72,7 @@ try {
|
||||
},
|
||||
});
|
||||
|
||||
console.log("Preload script completed successfully");
|
||||
logger.log("Preload script completed successfully");
|
||||
} catch (error) {
|
||||
console.error("Error in preload script:", error);
|
||||
logger.error("Error in preload script:", error);
|
||||
}
|
||||
|
||||
58
src/interfaces/claims-result.ts
Normal file
58
src/interfaces/claims-result.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { GiverReceiverInputInfo } from "../libs/util";
|
||||
import { ErrorResult, ResultWithType } from "./common";
|
||||
|
||||
export interface GiverOutputInfo {
|
||||
action: string;
|
||||
giver?: GiverReceiverInputInfo;
|
||||
description?: string;
|
||||
amount?: number;
|
||||
unitCode?: string;
|
||||
}
|
||||
|
||||
export interface ClaimResult {
|
||||
success: { claimId: string; handleId: string };
|
||||
error: { code: string; message: string };
|
||||
}
|
||||
|
||||
export interface VerifiableCredential {
|
||||
exp?: number;
|
||||
iat: number;
|
||||
iss: string;
|
||||
vc: {
|
||||
"@context": string[];
|
||||
type: string[];
|
||||
credentialSubject: VerifiableCredentialSubject;
|
||||
};
|
||||
}
|
||||
|
||||
export interface VerifiableCredentialSubject {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface WorldProperties {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
/**
|
||||
* Could be a DID or a handleId that identifies the provider
|
||||
*/
|
||||
identifier: string;
|
||||
/**
|
||||
* Indicates if the provider link has been confirmed
|
||||
*/
|
||||
linkConfirmed: boolean;
|
||||
}
|
||||
|
||||
// Type for createAndSubmitClaim result
|
||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
||||
|
||||
// Update SuccessResult to use ClaimResult
|
||||
export interface SuccessResult extends ResultWithType {
|
||||
type: "success";
|
||||
response: AxiosResponse<ClaimResult>;
|
||||
}
|
||||
68
src/interfaces/claims.ts
Normal file
68
src/interfaces/claims.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { GenericVerifiableCredential } from "./common";
|
||||
|
||||
export interface AgreeVerifiableCredential {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
object: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id4
|
||||
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
"@type": "GiveAction";
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
|
||||
identifier?: string;
|
||||
image?: string;
|
||||
object?: { amountOfThisGood: number; unitCode: string };
|
||||
provider?: GenericVerifiableCredential;
|
||||
recipient?: { identifier: string };
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id8
|
||||
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
"@type": "Offer";
|
||||
description?: string;
|
||||
includesObject?: { amountOfThisGood: number; unitCode: string };
|
||||
itemOffered?: {
|
||||
description?: string;
|
||||
isPartOf?: {
|
||||
identifier?: string;
|
||||
lastClaimId?: string;
|
||||
"@type"?: string;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
offeredBy?: { identifier: string };
|
||||
recipient?: { identifier: string };
|
||||
validThrough?: string;
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id7
|
||||
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context": "https://schema.org";
|
||||
"@type": "PlanAction";
|
||||
name: string;
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
identifier?: string;
|
||||
lastClaimId?: string;
|
||||
location?: {
|
||||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
||||
};
|
||||
}
|
||||
|
||||
// AKA Registration & RegisterAction
|
||||
export interface RegisterVerifiableCredential {
|
||||
"@context": string;
|
||||
"@type": "RegisterAction";
|
||||
agent: { identifier: string };
|
||||
identifier?: string;
|
||||
object: string;
|
||||
participant?: { identifier: string };
|
||||
}
|
||||
36
src/interfaces/common.ts
Normal file
36
src/interfaces/common.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// similar to VerifiableCredentialSubject... maybe rename this
|
||||
export interface GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
"@type": string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
||||
claim: T;
|
||||
claimType?: string;
|
||||
handleId: string;
|
||||
id: string;
|
||||
issuedAt: string;
|
||||
issuer: string;
|
||||
publicUrls?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ResultWithType {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error?: {
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InternalError {
|
||||
error: string;
|
||||
userMessage?: string;
|
||||
}
|
||||
|
||||
export interface ErrorResult extends ResultWithType {
|
||||
type: "error";
|
||||
error: InternalError;
|
||||
}
|
||||
13
src/interfaces/deepLinks.ts
Normal file
13
src/interfaces/deepLinks.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @file Deep Link Interface Definitions
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* Defines the core interfaces for the deep linking system.
|
||||
* These interfaces are used across the deep linking implementation
|
||||
* to ensure type safety and consistent error handling.
|
||||
*/
|
||||
|
||||
export interface DeepLinkError extends Error {
|
||||
code: string;
|
||||
details?: unknown;
|
||||
}
|
||||
7
src/interfaces/index.ts
Normal file
7
src/interfaces/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from "./claims";
|
||||
export * from "./claims-result";
|
||||
export * from "./common";
|
||||
export * from "./limits";
|
||||
export * from "./records";
|
||||
export * from "./user";
|
||||
export * from "./deepLinks";
|
||||
14
src/interfaces/limits.ts
Normal file
14
src/interfaces/limits.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface EndorserRateLimits {
|
||||
doneClaimsThisWeek: string;
|
||||
doneRegistrationsThisMonth: string;
|
||||
maxClaimsPerWeek: string;
|
||||
maxRegistrationsPerMonth: string;
|
||||
nextMonthBeginDateTime: string;
|
||||
nextWeekBeginDateTime: string;
|
||||
}
|
||||
|
||||
export interface ImageRateLimits {
|
||||
doneImagesThisWeek: string;
|
||||
maxImagesPerWeek: string;
|
||||
nextWeekBeginDateTime: string;
|
||||
}
|
||||
93
src/interfaces/records.ts
Normal file
93
src/interfaces/records.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { GiveVerifiableCredential, OfferVerifiableCredential } from "./claims";
|
||||
|
||||
// a summary record; the VC is found the fullClaim field
|
||||
export interface GiveSummaryRecord {
|
||||
[x: string]: PropertyKey | undefined | GiveVerifiableCredential;
|
||||
type?: string;
|
||||
agentDid: string;
|
||||
amount: number;
|
||||
amountConfirmed: number;
|
||||
description: string;
|
||||
fullClaim: GiveVerifiableCredential;
|
||||
fulfillsHandleId: string;
|
||||
fulfillsPlanHandleId?: string;
|
||||
fulfillsType?: string;
|
||||
handleId: string;
|
||||
issuedAt: string;
|
||||
issuerDid: string;
|
||||
jwtId: string;
|
||||
providerPlanHandleId?: string;
|
||||
recipientDid: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
// a summary record; the VC is found the fullClaim field
|
||||
export interface OfferSummaryRecord {
|
||||
amount: number;
|
||||
amountGiven: number;
|
||||
amountGivenConfirmed: number;
|
||||
fullClaim: OfferVerifiableCredential;
|
||||
fulfillsPlanHandleId: string;
|
||||
handleId: string;
|
||||
issuerDid: string;
|
||||
jwtId: string;
|
||||
nonAmountGivenConfirmed: number;
|
||||
objectDescription: string;
|
||||
offeredByDid: string;
|
||||
recipientDid: string;
|
||||
requirementsMet: boolean;
|
||||
unit: string;
|
||||
validThrough: string;
|
||||
}
|
||||
|
||||
export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
|
||||
planName: string;
|
||||
}
|
||||
|
||||
// a summary record; the VC is not currently part of this record
|
||||
export interface PlanSummaryRecord {
|
||||
agentDid?: string;
|
||||
description: string;
|
||||
endTime?: string;
|
||||
fulfillsPlanHandleId: string;
|
||||
handleId: string;
|
||||
image?: string;
|
||||
issuerDid: string;
|
||||
locLat?: number;
|
||||
locLon?: number;
|
||||
name?: string;
|
||||
startTime?: string;
|
||||
url?: string;
|
||||
jwtId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data about a project
|
||||
*
|
||||
* @deprecated
|
||||
* (Maybe we should use PlanSummaryRecord instead, either by adding rowId or by iterating with jwtId.)
|
||||
**/
|
||||
export interface PlanData {
|
||||
/**
|
||||
* Description of the project
|
||||
**/
|
||||
description: string;
|
||||
/**
|
||||
* URL referencing information about the project
|
||||
**/
|
||||
handleId: string;
|
||||
image?: string;
|
||||
/**
|
||||
* The DID of the issuer
|
||||
*/
|
||||
issuerDid: string;
|
||||
/**
|
||||
* Name of the project
|
||||
**/
|
||||
name: string;
|
||||
/**
|
||||
* The identifier of the project record -- different from jwtId
|
||||
* (Maybe we should use the jwtId to iterate through the records instead.)
|
||||
**/
|
||||
rowId?: string;
|
||||
}
|
||||
8
src/interfaces/user.ts
Normal file
8
src/interfaces/user.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface UserInfo {
|
||||
did: string;
|
||||
name: string;
|
||||
publicEncKey: string;
|
||||
registered: boolean;
|
||||
profileImageUrl?: string;
|
||||
nextPublicEncKeyHash?: string;
|
||||
}
|
||||
59
src/lib/capacitor/app.ts
Normal file
59
src/lib/capacitor/app.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// Import from node_modules using relative path
|
||||
|
||||
import {
|
||||
App as CapacitorApp,
|
||||
AppLaunchUrl,
|
||||
BackButtonListener,
|
||||
} from "../../../node_modules/@capacitor/app";
|
||||
import type { PluginListenerHandle } from "@capacitor/core";
|
||||
|
||||
/**
|
||||
* Interface defining the app event listener functionality
|
||||
* Supports 'backButton' and 'appUrlOpen' events from Capacitor
|
||||
*/
|
||||
interface AppInterface {
|
||||
/**
|
||||
* Add listener for back button events
|
||||
* @param eventName - Must be 'backButton'
|
||||
* @param listenerFunc - Callback function for back button events
|
||||
* @returns Promise that resolves with a removable listener handle
|
||||
*/
|
||||
addListener(
|
||||
eventName: "backButton",
|
||||
listenerFunc: BackButtonListener,
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
|
||||
/**
|
||||
* Add listener for app URL open events
|
||||
* @param eventName - Must be 'appUrlOpen'
|
||||
* @param listenerFunc - Callback function for URL open events
|
||||
* @returns Promise that resolves with a removable listener handle
|
||||
*/
|
||||
addListener(
|
||||
eventName: "appUrlOpen",
|
||||
listenerFunc: (data: AppLaunchUrl) => void,
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
}
|
||||
|
||||
/**
|
||||
* App wrapper for Capacitor functionality
|
||||
* Provides type-safe event listeners for back button and URL open events
|
||||
*/
|
||||
export const App: AppInterface = {
|
||||
addListener(
|
||||
eventName: "backButton" | "appUrlOpen",
|
||||
listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void),
|
||||
): Promise<PluginListenerHandle> & PluginListenerHandle {
|
||||
if (eventName === "backButton") {
|
||||
return CapacitorApp.addListener(
|
||||
eventName,
|
||||
listenerFunc as BackButtonListener,
|
||||
) as Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
} else {
|
||||
return CapacitorApp.addListener(
|
||||
eventName,
|
||||
listenerFunc as (data: AppLaunchUrl) => void,
|
||||
) as Promise<PluginListenerHandle> & PluginListenerHandle;
|
||||
}
|
||||
},
|
||||
};
|
||||
168
src/lib/fontawesome.ts
Normal file
168
src/lib/fontawesome.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* @file Font Awesome Icon Library Configuration
|
||||
* @description Centralizes Font Awesome icon imports and library configuration
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowRotateBackward,
|
||||
faArrowUpRightFromSquare,
|
||||
faArrowUp,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faCamera,
|
||||
faCaretDown,
|
||||
faChair,
|
||||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faChevronUp,
|
||||
faCircle,
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCircleQuestion,
|
||||
faCircleUser,
|
||||
faClock,
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faDollar,
|
||||
faEllipsis,
|
||||
faEllipsisVertical,
|
||||
faEnvelopeOpenText,
|
||||
faEraser,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileContract,
|
||||
faFileLines,
|
||||
faFilter,
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faForward,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHammer,
|
||||
faHand,
|
||||
faHandHoldingDollar,
|
||||
faHandHoldingHeart,
|
||||
faHouseChimney,
|
||||
faImagePortrait,
|
||||
faLeftRight,
|
||||
faLightbulb,
|
||||
faLink,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faMinus,
|
||||
faPen,
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faQrcode,
|
||||
faRightFromBracket,
|
||||
faRotate,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
faSquare,
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faSquarePlus,
|
||||
faTrashCan,
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUsers,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
// Initialize Font Awesome library with all required icons
|
||||
library.add(
|
||||
faArrowDown,
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowRotateBackward,
|
||||
faArrowUpRightFromSquare,
|
||||
faArrowUp,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faCamera,
|
||||
faCaretDown,
|
||||
faChair,
|
||||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faChevronUp,
|
||||
faCircle,
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCircleQuestion,
|
||||
faCircleUser,
|
||||
faClock,
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faDollar,
|
||||
faEllipsis,
|
||||
faEllipsisVertical,
|
||||
faEnvelopeOpenText,
|
||||
faEraser,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileContract,
|
||||
faFileLines,
|
||||
faFilter,
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faForward,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHammer,
|
||||
faHand,
|
||||
faHandHoldingDollar,
|
||||
faHandHoldingHeart,
|
||||
faHouseChimney,
|
||||
faImagePortrait,
|
||||
faLeftRight,
|
||||
faLightbulb,
|
||||
faLink,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faMinus,
|
||||
faPen,
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQrcode,
|
||||
faQuestion,
|
||||
faRotate,
|
||||
faRightFromBracket,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
faSquare,
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faSquarePlus,
|
||||
faTrashCan,
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUsers,
|
||||
faXmark,
|
||||
);
|
||||
|
||||
// Export the FontAwesomeIcon component for use in other files
|
||||
export { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
||||
} from "../../libs/endorserServer";
|
||||
import { DEFAULT_DID_PROVIDER_NAME } from "../veramo/setup";
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
export const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
|
||||
|
||||
@@ -269,35 +270,35 @@ export async function testEncryptionDecryption() {
|
||||
const testMessage = "Hello, this is a test message! 🚀";
|
||||
const testPassword = "myTestPassword123";
|
||||
|
||||
console.log("Original message:", testMessage);
|
||||
logger.log("Original message:", testMessage);
|
||||
|
||||
// Test encryption
|
||||
console.log("Encrypting...");
|
||||
logger.log("Encrypting...");
|
||||
const encrypted = await encryptMessage(testMessage, testPassword);
|
||||
console.log("Encrypted result:", encrypted);
|
||||
logger.log("Encrypted result:", encrypted);
|
||||
|
||||
// Test decryption
|
||||
console.log("Decrypting...");
|
||||
logger.log("Decrypting...");
|
||||
const decrypted = await decryptMessage(encrypted, testPassword);
|
||||
console.log("Decrypted result:", decrypted);
|
||||
logger.log("Decrypted result:", decrypted);
|
||||
|
||||
// Verify
|
||||
const success = testMessage === decrypted;
|
||||
console.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
||||
console.log("Messages match:", success);
|
||||
logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
||||
logger.log("Messages match:", success);
|
||||
|
||||
// Test with wrong password
|
||||
console.log("\nTesting with wrong password...");
|
||||
logger.log("\nTesting with wrong password...");
|
||||
try {
|
||||
await decryptMessage(encrypted, "wrongPassword");
|
||||
console.log("Should not reach here");
|
||||
logger.log("Should not reach here");
|
||||
} catch (error) {
|
||||
console.log("Correctly failed with wrong password ✅");
|
||||
logger.log("Correctly failed with wrong password ✅");
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.error("Test failed with error:", error);
|
||||
logger.error("Test failed with error:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
peerDidToPublicKeyBytes,
|
||||
verifyPeerSignature,
|
||||
} from "../../../libs/crypto/vc/didPeer";
|
||||
import { logger } from "../../../utils/logger";
|
||||
|
||||
export interface JWK {
|
||||
kty: string;
|
||||
@@ -69,7 +70,7 @@ export async function registerCredential(passkeyName?: string) {
|
||||
|
||||
const credIdBase64Url = verification.registrationInfo?.credentialID as string;
|
||||
if (attResp.rawId !== credIdBase64Url) {
|
||||
console.log("Warning! The raw ID does not match the credential ID.");
|
||||
logger.warn("Warning! The raw ID does not match the credential ID.");
|
||||
}
|
||||
const credIdHex = Buffer.from(
|
||||
base64URLStringToArrayBuffer(credIdBase64Url),
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
/**
|
||||
* @fileoverview Endorser Server Interface and Utilities
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This module provides the interface and utilities for interacting with the Endorser server.
|
||||
* It handles authentication, data validation, and server communication for claims, contacts,
|
||||
* and other core functionality.
|
||||
*
|
||||
* Key Features:
|
||||
* - Deep link URL path constants
|
||||
* - DID validation and handling
|
||||
* - Contact management utilities
|
||||
* - Server authentication
|
||||
* - Plan caching
|
||||
*
|
||||
* @module endorserServer
|
||||
*/
|
||||
|
||||
import { Axios, AxiosRequestConfig } from "axios";
|
||||
import { Buffer } from "buffer";
|
||||
import { sha256 } from "ethereum-cryptography/sha256";
|
||||
import { LRUCache } from "lru-cache";
|
||||
@@ -17,63 +35,63 @@ import {
|
||||
retrieveAccountMetadata,
|
||||
retrieveFullyDecryptedAccount,
|
||||
getPasskeyExpirationSeconds,
|
||||
GiverReceiverInputInfo,
|
||||
} from "../libs/util";
|
||||
import { createEndorserJwtForKey, KeyMeta } from "../libs/crypto/vc";
|
||||
|
||||
import {
|
||||
GiveVerifiableCredential,
|
||||
OfferVerifiableCredential,
|
||||
RegisterVerifiableCredential,
|
||||
GenericVerifiableCredential,
|
||||
GenericCredWrapper,
|
||||
PlanSummaryRecord,
|
||||
UserInfo,
|
||||
CreateAndSubmitClaimResult,
|
||||
} from "../interfaces";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* Standard context for schema.org data
|
||||
* @constant {string}
|
||||
*/
|
||||
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
|
||||
// the object in RegisterAction claims
|
||||
|
||||
/**
|
||||
* Service identifier for RegisterAction claims
|
||||
* @constant {string}
|
||||
*/
|
||||
export const SERVICE_ID = "endorser.ch";
|
||||
// the header line for contacts exported via Endorser Mobile
|
||||
|
||||
/**
|
||||
* Header line format for contacts exported via Endorser Mobile
|
||||
* @constant {string}
|
||||
*/
|
||||
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
|
||||
// the suffix for the contact URL in this app where they are confirmed before import
|
||||
|
||||
/**
|
||||
* URL path suffix for contact confirmation before import
|
||||
* @constant {string}
|
||||
*/
|
||||
export const CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact-import/";
|
||||
// the suffix for the contact URL in this app where a single one gets imported automatically
|
||||
|
||||
/**
|
||||
* URL path suffix for the contact URL in this app where a single one gets imported automatically
|
||||
* @constant {string}
|
||||
*/
|
||||
export const CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI = "/contacts?contactJwt=";
|
||||
// the suffix for the old contact URL -- deprecated Jan 2025, though "endorser.ch/contact?jwt=" shows data on endorser.ch server
|
||||
|
||||
/**
|
||||
* URL path suffix for the old contact URL -- deprecated Jan 2025, though "endorser.ch/contact?jwt=" shows data on endorser.ch server
|
||||
* @constant {string}
|
||||
*/
|
||||
export const CONTACT_URL_PATH_ENDORSER_CH_OLD = "/contact?jwt=";
|
||||
// unused now that we match on the URL path; just note that it was used for a while to create URLs that showed at endorser.ch
|
||||
//export const CONTACT_URL_PREFIX_ENDORSER_CH_OLD = "https://endorser.ch";
|
||||
// the prefix for handle IDs, the permanent ID for claims on Endorser
|
||||
|
||||
/**
|
||||
* The prefix for handle IDs, the permanent ID for claims on Endorser
|
||||
* @constant {string}
|
||||
*/
|
||||
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
||||
|
||||
export interface AgreeVerifiableCredential {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
// "any" because arbitrary objects can be subject of agreement
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
object: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface GiverOutputInfo {
|
||||
action: string;
|
||||
giver?: GiverReceiverInputInfo;
|
||||
description?: string;
|
||||
amount?: number;
|
||||
unitCode?: string;
|
||||
}
|
||||
|
||||
export interface ClaimResult {
|
||||
success: { claimId: string; handleId: string };
|
||||
error: { code: string; message: string };
|
||||
}
|
||||
|
||||
// similar to VerifiableCredentialSubject... maybe rename this
|
||||
export interface GenericVerifiableCredential {
|
||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||
"@type": string;
|
||||
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
|
||||
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
||||
claim: T;
|
||||
claimType?: string;
|
||||
handleId: string;
|
||||
id: string;
|
||||
issuedAt: string;
|
||||
issuer: string;
|
||||
publicUrls?: Record<string, string>; // only for IDs that want to be public
|
||||
}
|
||||
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
||||
{
|
||||
claim: { "@type": "" },
|
||||
@@ -83,266 +101,98 @@ export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCr
|
||||
issuer: "",
|
||||
};
|
||||
|
||||
// a summary record; the VC is found the fullClaim field
|
||||
export interface GiveSummaryRecord {
|
||||
agentDid: string;
|
||||
amount: number;
|
||||
amountConfirmed: number;
|
||||
description: string;
|
||||
fullClaim: GiveVerifiableCredential;
|
||||
fulfillsHandleId: string;
|
||||
fulfillsPlanHandleId?: string;
|
||||
fulfillsType?: string;
|
||||
handleId: string;
|
||||
issuedAt: string;
|
||||
issuerDid: string;
|
||||
jwtId: string;
|
||||
providerPlanHandleId?: string;
|
||||
recipientDid: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
// a summary record; the VC is found the fullClaim field
|
||||
export interface OfferSummaryRecord {
|
||||
amount: number;
|
||||
amountGiven: number;
|
||||
amountGivenConfirmed: number;
|
||||
fullClaim: OfferVerifiableCredential;
|
||||
fulfillsPlanHandleId: string;
|
||||
handleId: string;
|
||||
issuerDid: string;
|
||||
jwtId: string;
|
||||
nonAmountGivenConfirmed: number;
|
||||
objectDescription: string;
|
||||
offeredByDid: string;
|
||||
recipientDid: string;
|
||||
requirementsMet: boolean;
|
||||
unit: string;
|
||||
validThrough: string;
|
||||
}
|
||||
|
||||
export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
|
||||
planName: string;
|
||||
}
|
||||
|
||||
// a summary record; the VC is not currently part of this record
|
||||
export interface PlanSummaryRecord {
|
||||
agentDid?: string; // optional, if the issuer wants someone else to manage as well
|
||||
description: string;
|
||||
endTime?: string;
|
||||
fulfillsPlanHandleId: string;
|
||||
handleId: string;
|
||||
image?: string;
|
||||
issuerDid: string;
|
||||
locLat?: number;
|
||||
locLon?: number;
|
||||
name?: string;
|
||||
startTime?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id4
|
||||
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context"?: string; // optional when embedded, eg. in an Agree
|
||||
"@type": "GiveAction";
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
|
||||
identifier?: string;
|
||||
image?: string;
|
||||
object?: { amountOfThisGood: number; unitCode: string };
|
||||
provider?: GenericVerifiableCredential; // typically @type & identifier
|
||||
recipient?: { identifier: string };
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id8
|
||||
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context"?: string; // optional when embedded... though it doesn't make sense to agree to an offer
|
||||
"@type": "Offer";
|
||||
description?: string; // conditions for the offer
|
||||
includesObject?: { amountOfThisGood: number; unitCode: string };
|
||||
itemOffered?: {
|
||||
description?: string; // description of the item
|
||||
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
|
||||
};
|
||||
offeredBy?: { identifier: string };
|
||||
recipient?: { identifier: string };
|
||||
validThrough?: string;
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id7
|
||||
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context": "https://schema.org";
|
||||
"@type": "PlanAction";
|
||||
name: string;
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
identifier?: string;
|
||||
lastClaimId?: string;
|
||||
location?: {
|
||||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents data about a project
|
||||
*
|
||||
* @deprecated
|
||||
* (Maybe we should use PlanSummaryRecord instead, either by adding rowId or by iterating with jwtId.)
|
||||
**/
|
||||
export interface PlanData {
|
||||
/**
|
||||
* Description of the project
|
||||
**/
|
||||
description: string;
|
||||
/**
|
||||
* URL referencing information about the project
|
||||
**/
|
||||
handleId: string;
|
||||
image?: string;
|
||||
/**
|
||||
* The DID of the issuer
|
||||
*/
|
||||
issuerDid: string;
|
||||
/**
|
||||
* Name of the project
|
||||
**/
|
||||
name: string;
|
||||
/**
|
||||
* The identifier of the project record -- different from jwtId
|
||||
* (Maybe we should use the jwtId to iterate through the records instead.)
|
||||
**/
|
||||
rowId?: string;
|
||||
}
|
||||
|
||||
export interface EndorserRateLimits {
|
||||
doneClaimsThisWeek: string;
|
||||
doneRegistrationsThisMonth: string;
|
||||
maxClaimsPerWeek: string;
|
||||
maxRegistrationsPerMonth: string;
|
||||
nextMonthBeginDateTime: string;
|
||||
nextWeekBeginDateTime: string;
|
||||
}
|
||||
|
||||
export interface ImageRateLimits {
|
||||
doneImagesThisWeek: string;
|
||||
maxImagesPerWeek: string;
|
||||
nextWeekBeginDateTime: string;
|
||||
}
|
||||
|
||||
export interface VerifiableCredential {
|
||||
exp?: number;
|
||||
iat: number;
|
||||
iss: string;
|
||||
vc: {
|
||||
"@context": string[];
|
||||
type: string[];
|
||||
credentialSubject: VerifiableCredentialSubject;
|
||||
};
|
||||
}
|
||||
|
||||
// similar to GenericVerifiableCredential... maybe replace that one
|
||||
export interface VerifiableCredentialSubject {
|
||||
"@context": string;
|
||||
"@type": string;
|
||||
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
|
||||
export interface WorldProperties {
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
// AKA Registration & RegisterAction
|
||||
export interface RegisterVerifiableCredential {
|
||||
"@context": typeof SCHEMA_ORG_CONTEXT;
|
||||
"@type": "RegisterAction";
|
||||
agent: { identifier: string };
|
||||
identifier?: string; // used for invites (when participant ID isn't known)
|
||||
object: string;
|
||||
participant?: { identifier: string }; // used when person is known (not an invite)
|
||||
}
|
||||
|
||||
// now for some of the error & other wrapper types
|
||||
|
||||
export interface ResultWithType {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SuccessResult extends ResultWithType {
|
||||
type: "success";
|
||||
response: AxiosResponse<ClaimResult>;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error?: {
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InternalError {
|
||||
error: string; // for system logging
|
||||
userMessage?: string; // for user display
|
||||
}
|
||||
|
||||
export interface ErrorResult extends ResultWithType {
|
||||
type: "error";
|
||||
error: InternalError;
|
||||
}
|
||||
|
||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
||||
|
||||
/**
|
||||
* This is similar to Contact but it grew up in different logic paths.
|
||||
* We may want to change this to be a Contact.
|
||||
*/
|
||||
export interface UserInfo {
|
||||
did: string;
|
||||
name: string;
|
||||
publicEncKey: string;
|
||||
registered: boolean;
|
||||
profileImageUrl?: string;
|
||||
nextPublicEncKeyHash?: string;
|
||||
}
|
||||
|
||||
// This is used to check for hidden info.
|
||||
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
|
||||
const HIDDEN_DID = "did:none:HIDDEN";
|
||||
|
||||
export function isDid(did: string) {
|
||||
/**
|
||||
* Validates if a string is a valid DID
|
||||
* @param {string} did - The DID string to validate
|
||||
* @returns {boolean} True if string is a valid DID format
|
||||
*/
|
||||
export function isDid(did: string): boolean {
|
||||
return did.startsWith("did:");
|
||||
}
|
||||
|
||||
export function isHiddenDid(did: string) {
|
||||
/**
|
||||
* Checks if a DID is the special hidden DID value
|
||||
* @param {string} did - The DID to check
|
||||
* @returns {boolean} True if DID is hidden
|
||||
*/
|
||||
export function isHiddenDid(did: string): boolean {
|
||||
return did === HIDDEN_DID;
|
||||
}
|
||||
|
||||
export function isEmptyOrHiddenDid(did?: string) {
|
||||
return !did || did === HIDDEN_DID; // catching empty string as well
|
||||
/**
|
||||
* Checks if a DID is empty or hidden
|
||||
* @param {string} [did] - The DID to check
|
||||
* @returns {boolean} True if DID is empty or hidden
|
||||
*/
|
||||
export function isEmptyOrHiddenDid(did?: string): boolean {
|
||||
return !did || did === HIDDEN_DID;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true for any string within this primitive/object/array where func(input) === true
|
||||
* Recursively tests strings within an object/array against a test function
|
||||
* @param {Function} func - Test function to apply to strings
|
||||
* @param {any} input - Object/array to recursively test
|
||||
* @returns {boolean} True if any string passes the test function
|
||||
*
|
||||
* Similar logic is found in endorser-mobile.
|
||||
* @example
|
||||
* testRecursivelyOnStrings(isDid, { user: { id: "did:example:123" } })
|
||||
* // Returns: true
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) {
|
||||
/**
|
||||
* Recursively tests strings within a nested object/array structure against a test function
|
||||
*
|
||||
* This function traverses through objects and arrays to find all string values and applies
|
||||
* a test function to each string found. It handles:
|
||||
* - Direct string values
|
||||
* - Strings in objects (at any depth)
|
||||
* - Strings in arrays (at any depth)
|
||||
* - Mixed nested structures (objects containing arrays containing objects, etc)
|
||||
*
|
||||
* @param {Function} func - Test function that takes a string and returns boolean
|
||||
* @param {any} input - Value to recursively search (can be string, object, array, or other)
|
||||
* @returns {boolean} True if any string in the structure passes the test function
|
||||
*
|
||||
* @example
|
||||
* // Test if any string is a DID
|
||||
* const obj = {
|
||||
* user: {
|
||||
* id: "did:example:123",
|
||||
* details: ["name", "did:example:456"]
|
||||
* }
|
||||
* };
|
||||
* testRecursivelyOnStrings(isDid, obj); // Returns: true
|
||||
*
|
||||
* @example
|
||||
* // Test for hidden DIDs
|
||||
* const obj = {
|
||||
* visible: "did:example:123",
|
||||
* hidden: ["did:none:HIDDEN"]
|
||||
* };
|
||||
* testRecursivelyOnStrings(isHiddenDid, obj); // Returns: true
|
||||
*/
|
||||
function testRecursivelyOnStrings(
|
||||
func: (arg0: unknown) => boolean,
|
||||
input: unknown,
|
||||
): boolean {
|
||||
// Test direct string values
|
||||
if (Object.prototype.toString.call(input) === "[object String]") {
|
||||
return func(input);
|
||||
} else if (input instanceof Object) {
|
||||
}
|
||||
// Recursively test objects and arrays
|
||||
else if (input instanceof Object) {
|
||||
if (!Array.isArray(input)) {
|
||||
// it's an object
|
||||
// Handle plain objects
|
||||
for (const key in input) {
|
||||
if (testRecursivelyOnStrings(func, input[key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// it's an array
|
||||
// Handle arrays
|
||||
for (const value of input) {
|
||||
if (testRecursivelyOnStrings(func, value)) {
|
||||
return true;
|
||||
@@ -351,6 +201,7 @@ function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) {
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
// Non-string, non-object values can't contain strings
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -617,15 +468,23 @@ export async function getHeaders(
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache for storing plan data
|
||||
* @constant {LRUCache}
|
||||
*/
|
||||
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
|
||||
max: 500,
|
||||
});
|
||||
|
||||
/**
|
||||
* @param handleId nullable, in which case "undefined" will be returned
|
||||
* @param requesterDid optional, in which case no private info will be returned
|
||||
* @param axios
|
||||
* @param apiServer
|
||||
* Retrieves plan data from cache or server
|
||||
* @param {string} handleId - Plan handle ID
|
||||
* @param {Axios} axios - Axios instance
|
||||
* @param {string} apiServer - API server URL
|
||||
* @param {string} [requesterDid] - Optional requester DID for private info
|
||||
* @returns {Promise<PlanSummaryRecord|undefined>} Plan data or undefined if not found
|
||||
*
|
||||
* @throws {Error} If server request fails
|
||||
*/
|
||||
export async function getPlanFromCache(
|
||||
handleId: string | undefined,
|
||||
@@ -649,44 +508,44 @@ export async function getPlanFromCache(
|
||||
cred = resp.data.data[0];
|
||||
planCache.set(handleId, cred);
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to load plan with handle",
|
||||
logger.log(
|
||||
"[EndorserServer] Plan cache is empty for handle",
|
||||
handleId,
|
||||
" Got data:",
|
||||
resp.data,
|
||||
JSON.stringify(resp.data),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to load plan with handle",
|
||||
logger.error(
|
||||
"[EndorserServer] Failed to load plan with handle",
|
||||
handleId,
|
||||
" Got error:",
|
||||
error,
|
||||
JSON.stringify(error),
|
||||
);
|
||||
}
|
||||
}
|
||||
return cred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates plan data in cache
|
||||
* @param {string} handleId - Plan handle ID
|
||||
* @param {PlanSummaryRecord} planSummary - Plan data to cache
|
||||
*/
|
||||
export async function setPlanInCache(
|
||||
handleId: string,
|
||||
planSummary: PlanSummaryRecord,
|
||||
) {
|
||||
): Promise<void> {
|
||||
planCache.set(handleId, planSummary);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param error that is thrown from an Endorser server call by Axios
|
||||
* @returns user-friendly message, or undefined if none found
|
||||
* Extracts user-friendly message from server error
|
||||
* @param {any} error - Error thrown from Endorser server call
|
||||
* @returns {string|undefined} User-friendly message or undefined if none found
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function serverMessageForUser(error: any) {
|
||||
return (
|
||||
// this is how most user messages are returned
|
||||
error?.response?.data?.error?.message
|
||||
// some are returned as "error" with a string, but those are more for devs and are less helpful to the user
|
||||
);
|
||||
export function serverMessageForUser(error: unknown): string | undefined {
|
||||
return error?.response?.data?.error?.message;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -696,7 +555,7 @@ export function serverMessageForUser(error: any) {
|
||||
* @param error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function errorStringForLog(error: any) {
|
||||
export function errorStringForLog(error: unknown) {
|
||||
let stringifiedError = "" + error;
|
||||
try {
|
||||
stringifiedError = JSON.stringify(error);
|
||||
@@ -1126,7 +985,7 @@ export async function createAndSubmitClaim(
|
||||
return { type: "success", response };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Error submitting claim:", error);
|
||||
logger.error("Error submitting claim:", error);
|
||||
const errorMessage: string =
|
||||
serverMessageForUser(error) ||
|
||||
error.message ||
|
||||
@@ -1472,7 +1331,7 @@ export async function register(
|
||||
}
|
||||
return { error: message };
|
||||
} else {
|
||||
console.error(resp);
|
||||
logger.error(resp);
|
||||
return { error: "Got a server error when registering." };
|
||||
}
|
||||
}
|
||||
@@ -1502,7 +1361,7 @@ export async function setVisibilityUtil(
|
||||
}
|
||||
return { success };
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Got some bad server response when setting visibility: ",
|
||||
resp.status,
|
||||
resp,
|
||||
@@ -1512,7 +1371,7 @@ export async function setVisibilityUtil(
|
||||
return { error: message };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Got some error when setting visibility:", err);
|
||||
logger.error("Got some error when setting visibility:", err);
|
||||
return { error: "Check connectivity and try again." };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { KeyMeta } from "../libs/crypto/vc";
|
||||
import { createPeerDid } from "../libs/crypto/vc/didPeer";
|
||||
import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export interface GiverReceiverInputInfo {
|
||||
did?: string;
|
||||
@@ -98,6 +99,11 @@ export function numberOrZero(str: string): number {
|
||||
return isNumeric(str) ? +str : 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* from https://tools.ietf.org/html/rfc3986#section-3
|
||||
* also useful is https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Definition
|
||||
**/
|
||||
export const isGlobalUri = (uri: string) => {
|
||||
return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/));
|
||||
};
|
||||
@@ -222,7 +228,7 @@ export async function retrieveConfirmerIdList(
|
||||
};
|
||||
return result;
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Bad response status of",
|
||||
response.status,
|
||||
"for confirmers:",
|
||||
@@ -610,7 +616,7 @@ export const sendTestThroughPushServer = async (
|
||||
message: `Test, where you will see this message ${ skipFilter ? "un" : "" }filtered.`,
|
||||
title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push",
|
||||
};
|
||||
console.log("Sending a test web push message:", newPayload);
|
||||
logger.log("Sending a test web push message:", newPayload);
|
||||
const payloadStr = JSON.stringify(newPayload);
|
||||
const response = await axios.post(
|
||||
pushUrl + "/web-push/send-test",
|
||||
@@ -622,6 +628,6 @@ export const sendTestThroughPushServer = async (
|
||||
},
|
||||
);
|
||||
|
||||
console.log("Got response from web push server:", response);
|
||||
logger.log("Got response from web push server:", response);
|
||||
return response;
|
||||
};
|
||||
|
||||
90
src/main.capacitor.ts
Normal file
90
src/main.capacitor.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* @file Capacitor Main Entry Point
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This file initializes the deep linking system for the TimeSafari app.
|
||||
* It sets up the connection between Capacitor's URL handling and our deep link processor.
|
||||
*
|
||||
* Deep Linking Flow:
|
||||
* 1. Capacitor receives URL open event
|
||||
* 2. Event is passed to DeepLinkHandler
|
||||
* 3. URL is validated and processed
|
||||
* 4. Router navigates to appropriate view
|
||||
*
|
||||
* Integration Points:
|
||||
* - Capacitor App plugin for URL handling
|
||||
* - Vue Router for navigation
|
||||
* - Error handling system
|
||||
* - Logging system
|
||||
*
|
||||
* Type Safety:
|
||||
* - Uses DeepLinkHandler for type-safe parameter processing
|
||||
* - Ensures type safety between Capacitor events and app routing
|
||||
* - Maintains type checking through the entire deep link flow
|
||||
*
|
||||
* @example
|
||||
* // URL open event from OS
|
||||
* timesafari://claim/123?view=details
|
||||
* // Processed and routed to appropriate view with type-safe parameters
|
||||
*/
|
||||
|
||||
import { initializeApp } from "./main.common";
|
||||
import { App } from "./lib/capacitor/app";
|
||||
import router from "./router";
|
||||
import { handleApiError } from "./services/api";
|
||||
import { AxiosError } from "axios";
|
||||
import { DeepLinkHandler } from "./services/deepLinks";
|
||||
import { logConsoleAndDb } from "./db";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
logger.log("[Capacitor] Starting initialization");
|
||||
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
||||
|
||||
const app = initializeApp();
|
||||
|
||||
// Initialize API error handling for unhandled promise rejections
|
||||
window.addEventListener("unhandledrejection", (event) => {
|
||||
if (event.reason?.response) {
|
||||
handleApiError(event.reason, event.reason.config?.url || "unknown");
|
||||
}
|
||||
});
|
||||
|
||||
const deepLinkHandler = new DeepLinkHandler(router);
|
||||
|
||||
/**
|
||||
* Handles deep link routing for the application
|
||||
* Processes URLs in the format timesafari://<route>/<param>
|
||||
* Maps incoming deep links to corresponding router paths with parameters
|
||||
*
|
||||
* @param {Object} data - Deep link data object
|
||||
* @param {string} data.url - The full deep link URL to process
|
||||
* @returns {Promise<void>}
|
||||
*
|
||||
* @example
|
||||
* // Handles URLs like:
|
||||
* // timesafari://claim/01JMAAFZRNSRTQ0EBSD70A8E1H
|
||||
* // timesafari://project/abc123
|
||||
*
|
||||
* @throws {Error} If URL format is invalid
|
||||
*/
|
||||
const handleDeepLink = async (data: { url: string }) => {
|
||||
try {
|
||||
await router.isReady();
|
||||
await deepLinkHandler.handleDeepLink(data.url);
|
||||
} catch (error) {
|
||||
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
|
||||
handleApiError(
|
||||
{
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
} as AxiosError,
|
||||
"deep-link",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Register deep link handler with Capacitor
|
||||
App.addListener("appUrlOpen", handleDeepLink);
|
||||
|
||||
logger.log("[Capacitor] Mounting app");
|
||||
app.mount("#app");
|
||||
logger.log("[Capacitor] App mounted");
|
||||
61
src/main.common.ts
Normal file
61
src/main.common.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createPinia } from "pinia";
|
||||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import axios from "axios";
|
||||
import VueAxios from "vue-axios";
|
||||
import Notifications from "notiwind";
|
||||
import "./assets/styles/tailwind.css";
|
||||
import { FontAwesomeIcon } from "./lib/fontawesome";
|
||||
import Camera from "simple-vue-camera";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
// Global Error Handler
|
||||
function setupGlobalErrorHandler(app: VueApp) {
|
||||
logger.log("[App Init] Setting up global error handler");
|
||||
app.config.errorHandler = (
|
||||
err: unknown,
|
||||
instance: ComponentPublicInstance | null,
|
||||
info: string,
|
||||
) => {
|
||||
logger.error("[App Error] Global Error Handler:", {
|
||||
error: err,
|
||||
info,
|
||||
component: instance?.$options.name || "unknown",
|
||||
});
|
||||
alert(
|
||||
(err instanceof Error ? err.message : "Something bad happened") +
|
||||
" - Try reloading or restarting the app.",
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Function to initialize the app
|
||||
export function initializeApp() {
|
||||
logger.log("[App Init] Starting app initialization");
|
||||
logger.log("[App Init] Platform:", process.env.VITE_PLATFORM);
|
||||
|
||||
const app = createApp(App);
|
||||
logger.log("[App Init] Vue app created");
|
||||
|
||||
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera);
|
||||
logger.log("[App Init] Components registered");
|
||||
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
logger.log("[App Init] Pinia store initialized");
|
||||
|
||||
app.use(VueAxios, axios);
|
||||
logger.log("[App Init] Axios initialized");
|
||||
|
||||
app.use(router);
|
||||
logger.log("[App Init] Router initialized");
|
||||
|
||||
app.use(Notifications);
|
||||
logger.log("[App Init] Notifications initialized");
|
||||
|
||||
setupGlobalErrorHandler(app);
|
||||
logger.log("[App Init] App initialization complete");
|
||||
|
||||
return app;
|
||||
}
|
||||
4
src/main.electron.ts
Normal file
4
src/main.electron.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { initializeApp } from "./main.common";
|
||||
|
||||
const app = initializeApp();
|
||||
app.mount("#app");
|
||||
4
src/main.pywebview.ts
Normal file
4
src/main.pywebview.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { initializeApp } from "./main.common";
|
||||
|
||||
const app = initializeApp();
|
||||
app.mount("#app");
|
||||
169
src/main.ts
169
src/main.ts
@@ -6,170 +6,10 @@ import router from "./router";
|
||||
import axios from "axios";
|
||||
import VueAxios from "vue-axios";
|
||||
import Notifications from "notiwind";
|
||||
|
||||
import "./assets/styles/tailwind.css";
|
||||
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowRotateBackward,
|
||||
faArrowUpRightFromSquare,
|
||||
faArrowUp,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faCamera,
|
||||
faCaretDown,
|
||||
faChair,
|
||||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faChevronUp,
|
||||
faCircle,
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCircleQuestion,
|
||||
faCircleUser,
|
||||
faClock,
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faDollar,
|
||||
faEllipsis,
|
||||
faEllipsisVertical,
|
||||
faEnvelopeOpenText,
|
||||
faEraser,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileContract,
|
||||
faFileLines,
|
||||
faFilter,
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faForward,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHammer,
|
||||
faHand,
|
||||
faHandHoldingDollar,
|
||||
faHandHoldingHeart,
|
||||
faHouseChimney,
|
||||
faImagePortrait,
|
||||
faLeftRight,
|
||||
faLightbulb,
|
||||
faLink,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faMinus,
|
||||
faPen,
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faQrcode,
|
||||
faRightFromBracket,
|
||||
faRotate,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
faSquare,
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faSquarePlus,
|
||||
faTrashCan,
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUsers,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
faArrowDown,
|
||||
faArrowLeft,
|
||||
faArrowRight,
|
||||
faArrowRotateBackward,
|
||||
faArrowUpRightFromSquare,
|
||||
faArrowUp,
|
||||
faBan,
|
||||
faBitcoinSign,
|
||||
faBurst,
|
||||
faCalendar,
|
||||
faCamera,
|
||||
faCaretDown,
|
||||
faChair,
|
||||
faCheck,
|
||||
faChevronDown,
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faChevronUp,
|
||||
faCircle,
|
||||
faCircleCheck,
|
||||
faCircleInfo,
|
||||
faCircleQuestion,
|
||||
faCircleUser,
|
||||
faClock,
|
||||
faCoins,
|
||||
faComment,
|
||||
faCopy,
|
||||
faDollar,
|
||||
faEllipsis,
|
||||
faEllipsisVertical,
|
||||
faEnvelopeOpenText,
|
||||
faEraser,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileContract,
|
||||
faFileLines,
|
||||
faFilter,
|
||||
faFloppyDisk,
|
||||
faFolderOpen,
|
||||
faForward,
|
||||
faGift,
|
||||
faGlobe,
|
||||
faHammer,
|
||||
faHand,
|
||||
faHandHoldingDollar,
|
||||
faHandHoldingHeart,
|
||||
faHouseChimney,
|
||||
faImagePortrait,
|
||||
faLeftRight,
|
||||
faLightbulb,
|
||||
faLink,
|
||||
faLocationDot,
|
||||
faLongArrowAltLeft,
|
||||
faLongArrowAltRight,
|
||||
faMagnifyingGlass,
|
||||
faMessage,
|
||||
faMinus,
|
||||
faPen,
|
||||
faPersonCircleCheck,
|
||||
faPersonCircleQuestion,
|
||||
faPlus,
|
||||
faQrcode,
|
||||
faQuestion,
|
||||
faRotate,
|
||||
faRightFromBracket,
|
||||
faShareNodes,
|
||||
faSpinner,
|
||||
faSquare,
|
||||
faSquareCaretDown,
|
||||
faSquareCaretUp,
|
||||
faSquarePlus,
|
||||
faTrashCan,
|
||||
faTriangleExclamation,
|
||||
faUser,
|
||||
faUsers,
|
||||
faXmark,
|
||||
);
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { FontAwesomeIcon } from "./lib/fontawesome";
|
||||
import Camera from "simple-vue-camera";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
// Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
|
||||
function setupGlobalErrorHandler(app: VueApp) {
|
||||
@@ -179,7 +19,7 @@ function setupGlobalErrorHandler(app: VueApp) {
|
||||
instance: ComponentPublicInstance | null,
|
||||
info: string,
|
||||
) => {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Ouch! Global Error Handler.",
|
||||
"Error:",
|
||||
err,
|
||||
@@ -197,7 +37,7 @@ function setupGlobalErrorHandler(app: VueApp) {
|
||||
);
|
||||
};
|
||||
}
|
||||
// console.log("Bootstrapping Vue app...");
|
||||
|
||||
const app = createApp(App)
|
||||
.component("fa", FontAwesomeIcon)
|
||||
.component("camera", Camera)
|
||||
@@ -209,4 +49,3 @@ const app = createApp(App)
|
||||
setupGlobalErrorHandler(app);
|
||||
|
||||
app.mount("#app");
|
||||
// console.log("Vue app mounted.");
|
||||
|
||||
5
src/main.web.ts
Normal file
5
src/main.web.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { initializeApp } from "./main.common";
|
||||
import "./registerServiceWorker"; // Web PWA support
|
||||
|
||||
const app = initializeApp();
|
||||
app.mount("#app");
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
import { accountsDBPromise } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -276,8 +277,8 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () => import("../views/TestView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/userProfile/:id?",
|
||||
name: "userProfile",
|
||||
path: "/user-profile/:id?",
|
||||
name: "user-profile",
|
||||
component: () => import("../views/UserProfileView.vue"),
|
||||
},
|
||||
];
|
||||
@@ -307,7 +308,7 @@ const errorHandler = (
|
||||
from: RouteLocationNormalized,
|
||||
) => {
|
||||
// Handle the error here
|
||||
console.error("Caught in top level error handler:", error, to, from);
|
||||
logger.error("Caught in top level error handler:", error, to, from);
|
||||
alert("Something is very wrong. Try reloading or restarting the app.");
|
||||
|
||||
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
||||
|
||||
24
src/services/api.ts
Normal file
24
src/services/api.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { logger } from "../utils/logger";
|
||||
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||
if (process.env.VITE_PLATFORM === "capacitor") {
|
||||
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
data: error.response?.data,
|
||||
config: {
|
||||
url: error.config?.url,
|
||||
method: error.config?.method,
|
||||
headers: error.config?.headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Specific handling for rate limits
|
||||
if (error.response?.status === 400) {
|
||||
logger.warn(`[Rate Limit] ${endpoint}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
148
src/services/deepLinks.ts
Normal file
148
src/services/deepLinks.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* @file Deep Link Handler Service
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This service handles the processing and routing of deep links in the TimeSafari app.
|
||||
* It provides a type-safe interface between the raw deep links and the application router.
|
||||
*
|
||||
* Architecture:
|
||||
* 1. DeepLinkHandler class encapsulates all deep link processing logic
|
||||
* 2. Uses Zod schemas from types/deepLinks for parameter validation
|
||||
* 3. Provides consistent error handling and logging
|
||||
* 4. Maps validated parameters to Vue router calls
|
||||
*
|
||||
* Error Handling Strategy:
|
||||
* - All errors are wrapped in DeepLinkError interface
|
||||
* - Errors include error codes for systematic handling
|
||||
* - Detailed error information is logged for debugging
|
||||
* - Errors are propagated to the global error handler
|
||||
*
|
||||
* Validation Strategy:
|
||||
* - URL structure validation
|
||||
* - Route-specific parameter validation using Zod schemas
|
||||
* - Query parameter validation and sanitization
|
||||
* - Type-safe parameter passing to router
|
||||
*
|
||||
* @example
|
||||
* const handler = new DeepLinkHandler(router);
|
||||
* await handler.handleDeepLink("timesafari://claim/123?view=details");
|
||||
*/
|
||||
|
||||
import { Router } from "vue-router";
|
||||
import { deepLinkSchemas, baseUrlSchema } from "../types/deepLinks";
|
||||
import { logConsoleAndDb } from "../db";
|
||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||
|
||||
export class DeepLinkHandler {
|
||||
private router: Router;
|
||||
|
||||
constructor(router: Router) {
|
||||
this.router = router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses deep link URL into path, params and query components
|
||||
*/
|
||||
private parseDeepLink(url: string) {
|
||||
const parts = url.split("://");
|
||||
if (parts.length !== 2) {
|
||||
throw { code: "INVALID_URL", message: "Invalid URL format" };
|
||||
}
|
||||
|
||||
// Validate base URL structure
|
||||
baseUrlSchema.parse({
|
||||
scheme: parts[0],
|
||||
path: parts[1],
|
||||
queryParams: {}, // Will be populated below
|
||||
});
|
||||
|
||||
const [path, queryString] = parts[1].split("?");
|
||||
const [routePath, param] = path.split("/");
|
||||
|
||||
const query: Record<string, string> = {};
|
||||
if (queryString) {
|
||||
new URLSearchParams(queryString).forEach((value, key) => {
|
||||
query[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
path: routePath,
|
||||
params: param ? { id: param } : {},
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes incoming deep links and routes them appropriately
|
||||
* @param url The deep link URL to process
|
||||
*/
|
||||
async handleDeepLink(url: string): Promise<void> {
|
||||
try {
|
||||
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
||||
const { path, params, query } = this.parseDeepLink(url);
|
||||
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
||||
const sanitizedParams = Object.fromEntries(
|
||||
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
||||
);
|
||||
await this.validateAndRoute(path, sanitizedParams, query);
|
||||
} catch (error) {
|
||||
const deepLinkError = error as DeepLinkError;
|
||||
logConsoleAndDb(
|
||||
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
||||
true,
|
||||
);
|
||||
|
||||
throw {
|
||||
code: deepLinkError.code || "UNKNOWN_ERROR",
|
||||
message: deepLinkError.message,
|
||||
details: deepLinkError.details,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes the deep link to appropriate view with validated parameters
|
||||
*/
|
||||
private async validateAndRoute(
|
||||
path: string,
|
||||
params: Record<string, string>,
|
||||
query: Record<string, string>,
|
||||
): Promise<void> {
|
||||
const routeMap: Record<string, string> = {
|
||||
"user-profile": "user-profile",
|
||||
project: "project",
|
||||
"onboard-meeting-setup": "onboard-meeting-setup",
|
||||
"invite-one-accept": "invite-one-accept",
|
||||
"contact-import": "contact-import",
|
||||
"confirm-gift": "confirm-gift",
|
||||
claim: "claim",
|
||||
"claim-cert": "claim-cert",
|
||||
"claim-add-raw": "claim-add-raw",
|
||||
"contact-edit": "contact-edit",
|
||||
contacts: "contacts",
|
||||
did: "did",
|
||||
};
|
||||
|
||||
const routeName = routeMap[path];
|
||||
if (!routeName) {
|
||||
throw {
|
||||
code: "INVALID_ROUTE",
|
||||
message: `Unsupported route: ${path}`,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate parameters based on route type
|
||||
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
|
||||
const validatedParams = await schema.parseAsync({
|
||||
...params,
|
||||
...query,
|
||||
});
|
||||
|
||||
await this.router.replace({
|
||||
name: routeName,
|
||||
params: validatedParams,
|
||||
query,
|
||||
});
|
||||
}
|
||||
}
|
||||
81
src/services/plan.ts
Normal file
81
src/services/plan.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import axios from "axios";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
interface PlanResponse {
|
||||
data?: unknown;
|
||||
status?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export const loadPlanWithRetry = async (
|
||||
handle: string,
|
||||
retries = 3,
|
||||
): Promise<PlanResponse> => {
|
||||
try {
|
||||
logger.log(`[Plan Service] Loading plan ${handle}, attempt 1/${retries}`);
|
||||
logger.log(
|
||||
`[Plan Service] Context: Deep link handle=${handle}, isClaimFlow=${handle.includes("claim")}`,
|
||||
);
|
||||
|
||||
// Different endpoint if this is a claim flow
|
||||
const response = await loadPlan(handle);
|
||||
logger.log(`[Plan Service] Plan ${handle} loaded successfully:`, {
|
||||
status: response?.status,
|
||||
headers: response?.headers,
|
||||
data: response?.data,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error: unknown) {
|
||||
logger.error(`[Plan Service] Error loading plan ${handle}:`, {
|
||||
message: (error as Error).message,
|
||||
status: (error as { response?: { status?: number } })?.response?.status,
|
||||
statusText: (error as { response?: { statusText?: string } })?.response
|
||||
?.statusText,
|
||||
data: (error as { response?: { data?: unknown } })?.response?.data,
|
||||
headers: (error as { response?: { headers?: unknown } })?.response
|
||||
?.headers,
|
||||
config: {
|
||||
url: (error as { config?: { url?: string } })?.config?.url,
|
||||
method: (error as { config?: { method?: string } })?.config?.method,
|
||||
baseURL: (error as { config?: { baseURL?: string } })?.config?.baseURL,
|
||||
headers: (error as { config?: { headers?: unknown } })?.config?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (retries > 1) {
|
||||
logger.log(
|
||||
`[Plan Service] Retrying plan ${handle}, ${retries - 1} attempts remaining`,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return loadPlanWithRetry(handle, retries - 1);
|
||||
}
|
||||
|
||||
return {
|
||||
error: `Failed to load plan ${handle} after ${4 - retries} attempts: ${(error as Error).message}`,
|
||||
status: (error as { response?: { status?: number } })?.response?.status,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const loadPlan = async (handle: string): Promise<PlanResponse> => {
|
||||
logger.log(`[Plan Service] Making API request for plan ${handle}`);
|
||||
|
||||
const endpoint = handle.includes("claim")
|
||||
? `/api/claims/${handle}`
|
||||
: `/api/plans/${handle}`;
|
||||
|
||||
logger.log(`[Plan Service] Using endpoint: ${endpoint}`);
|
||||
|
||||
try {
|
||||
const response = await axios.get(endpoint);
|
||||
return response;
|
||||
} catch (error: unknown) {
|
||||
logger.error(`[Plan Service] API request failed for ${handle}:`, {
|
||||
endpoint,
|
||||
error: (error as Error).message,
|
||||
response: (error as { response?: { data?: unknown } })?.response?.data,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { AppString } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db";
|
||||
import { SERVICE_ID } from "../libs/endorserServer";
|
||||
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
/**
|
||||
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
|
||||
*/
|
||||
@@ -58,5 +58,5 @@ export async function testServerRegisterUser() {
|
||||
};
|
||||
|
||||
const resp = await axios.post(url, payload, { headers });
|
||||
console.log("User registration result:", resp);
|
||||
logger.log("User registration result:", resp);
|
||||
}
|
||||
|
||||
81
src/types/deepLinks.ts
Normal file
81
src/types/deepLinks.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @file Deep Link Type Definitions and Validation Schemas
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This file defines the type system and validation schemas for deep linking in the TimeSafari app.
|
||||
* It uses Zod for runtime validation while providing TypeScript types for compile-time checking.
|
||||
*
|
||||
* Type Strategy:
|
||||
* 1. Define base URL schema to validate the fundamental deep link structure
|
||||
* 2. Define route-specific parameter schemas with exact validation rules
|
||||
* 3. Generate TypeScript types from Zod schemas for type safety
|
||||
* 4. Export both schemas and types for use in deep link handling
|
||||
*
|
||||
* Usage:
|
||||
* - Import schemas for runtime validation in deep link handlers
|
||||
* - Import types for type-safe parameter handling in components
|
||||
* - Use DeepLinkParams type for type-safe access to route parameters
|
||||
*
|
||||
* @example
|
||||
* // Runtime validation
|
||||
* const params = deepLinkSchemas.claim.parse({ id: "123", view: "details" });
|
||||
*
|
||||
* // Type-safe parameter access
|
||||
* function handleClaimParams(params: DeepLinkParams["claim"]) {
|
||||
* // TypeScript knows params.id exists and params.view is optional
|
||||
* }
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
// Base URL validation schema
|
||||
export const baseUrlSchema = z.object({
|
||||
scheme: z.literal("timesafari"),
|
||||
path: z.string(),
|
||||
queryParams: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
// Parameter validation schemas for each route type
|
||||
export const deepLinkSchemas = {
|
||||
"user-profile": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"project-details": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"onboard-meeting-setup": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"invite-one-accept": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"contact-import": z.object({
|
||||
jwt: z.string(),
|
||||
}),
|
||||
"confirm-gift": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
claim: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"claim-cert": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"claim-add-raw": z.object({
|
||||
id: z.string(),
|
||||
claim: z.string().optional(),
|
||||
claimJwtId: z.string().optional(),
|
||||
}),
|
||||
"contact-edit": z.object({
|
||||
did: z.string(),
|
||||
}),
|
||||
contacts: z.object({
|
||||
contacts: z.string(), // JSON string of contacts array
|
||||
}),
|
||||
did: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
export type DeepLinkParams = {
|
||||
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
|
||||
};
|
||||
18
src/utils/logger.ts
Normal file
18
src/utils/logger.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const logger = {
|
||||
log: (message: string, ...args: unknown[]) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message, ...args);
|
||||
}
|
||||
},
|
||||
warn: (message: string, ...args: unknown[]) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(message, ...args);
|
||||
}
|
||||
},
|
||||
error: (message: string, ...args: unknown[]) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message, ...args); // Errors should always be logged
|
||||
},
|
||||
};
|
||||
@@ -39,12 +39,15 @@
|
||||
:to="{ name: 'contact-qr' }"
|
||||
class="bg-slate-500 text-white px-1.5 py-1 rounded-md"
|
||||
>
|
||||
<fa icon="qrcode" class="fa-fw text-xl"></fa>
|
||||
<font-awesome icon="qrcode" class="fa-fw text-xl"></font-awesome>
|
||||
</router-link>
|
||||
</span>
|
||||
{{ givenName }}
|
||||
<router-link :to="{ name: 'new-edit-account' }">
|
||||
<fa icon="pen" class="text-xs text-blue-500 ml-2 mb-1"></fa>
|
||||
<font-awesome
|
||||
icon="pen"
|
||||
class="text-xs text-blue-500 ml-2 mb-1"
|
||||
></font-awesome>
|
||||
</router-link>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -53,13 +56,13 @@
|
||||
class="block w-full text-center text-md bg-amber-200 border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
|
||||
>
|
||||
<button
|
||||
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@click="
|
||||
() =>
|
||||
(this.$refs.userNameDialog as UserNameDialog).open(
|
||||
(name) => (this.givenName = name),
|
||||
($refs.userNameDialog as UserNameDialog).open(
|
||||
(name) => (givenName = name),
|
||||
)
|
||||
"
|
||||
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
>
|
||||
Set Your Name
|
||||
</button>
|
||||
@@ -69,23 +72,23 @@
|
||||
<span v-if="profileImageUrl" class="flex justify-between">
|
||||
<EntityIcon
|
||||
:icon-size="96"
|
||||
:profileImageUrl="profileImageUrl"
|
||||
:profile-image-url="profileImageUrl"
|
||||
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||
@click="showLargeIdenticonUrl = profileImageUrl"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="trash-can"
|
||||
@click="confirmDeleteImage"
|
||||
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
|
||||
@click="confirmDeleteImage"
|
||||
/>
|
||||
</span>
|
||||
<div v-else class="text-center">
|
||||
<div class @click="openImageDialog()">
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="image-portrait"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-l"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="camera"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-r"
|
||||
/>
|
||||
@@ -101,8 +104,8 @@
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<EntityIcon
|
||||
:entityId="activeDid"
|
||||
:iconSize="64"
|
||||
:entity-id="activeDid"
|
||||
:icon-size="64"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
@click="showLargeIdenticonId = activeDid"
|
||||
/>
|
||||
@@ -116,9 +119,9 @@
|
||||
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||
>
|
||||
<EntityIcon
|
||||
:entityId="showLargeIdenticonId"
|
||||
:iconSize="512"
|
||||
:profileImageUrl="showLargeIdenticonUrl"
|
||||
:entity-id="showLargeIdenticonId"
|
||||
:icon-size="512"
|
||||
:profile-image-url="showLargeIdenticonUrl"
|
||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||
@click="
|
||||
showLargeIdenticonId = undefined;
|
||||
@@ -135,12 +138,12 @@
|
||||
>
|
||||
<code class="truncate">{{ activeDid }}</code>
|
||||
<button
|
||||
class="ml-2"
|
||||
@click="
|
||||
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
<font-awesome icon="copy" class="text-slate-400 fa-fw"></font-awesome>
|
||||
</button>
|
||||
<span v-show="showDidCopy">Copied</span>
|
||||
</div>
|
||||
@@ -185,7 +188,7 @@
|
||||
<!-- label -->
|
||||
<div>
|
||||
Reminder Notification
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="question-circle"
|
||||
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
||||
@click.stop="showReminderNotificationInfo"
|
||||
@@ -197,7 +200,7 @@
|
||||
@click="showReminderNotificationChoice()"
|
||||
>
|
||||
<!-- input -->
|
||||
<input type="checkbox" v-model="notifyingReminder" class="sr-only" />
|
||||
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<!-- dot -->
|
||||
@@ -214,7 +217,7 @@
|
||||
<!-- label -->
|
||||
<div>
|
||||
New Activity Notification
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="question-circle"
|
||||
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
||||
@click.stop="showNewActivityNotificationInfo"
|
||||
@@ -227,8 +230,8 @@
|
||||
>
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="notifyingNewActivity"
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
/>
|
||||
<!-- line -->
|
||||
@@ -268,12 +271,15 @@
|
||||
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
|
||||
>
|
||||
<div v-if="loadingProfile" class="text-center mb-2">
|
||||
<fa icon="spinner" class="fa-spin text-slate-400"></fa> Loading
|
||||
profile...
|
||||
<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>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-slate-400 fa-fw ml-2 cursor-pointer"
|
||||
@click="showProfileInfo"
|
||||
@@ -289,9 +295,9 @@
|
||||
|
||||
<div class="flex items-center mb-4" @click="toggleUserProfileLocation">
|
||||
<input
|
||||
v-model="includeUserProfileLocation"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
v-model="includeUserProfileLocation"
|
||||
/>
|
||||
<label for="includeUserProfileLocation">Include Location</label>
|
||||
</div>
|
||||
@@ -327,17 +333,16 @@
|
||||
<div v-if="!loadingProfile && !savingProfile">
|
||||
<div class="flex justify-between items-center">
|
||||
<button
|
||||
@click="saveProfile"
|
||||
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||
:disabled="loadingProfile || savingProfile"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': loadingProfile || savingProfile,
|
||||
}"
|
||||
@click="saveProfile"
|
||||
>
|
||||
Save Profile
|
||||
</button>
|
||||
<button
|
||||
@click="confirmDeleteProfile"
|
||||
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
|
||||
:disabled="loadingProfile || savingProfile"
|
||||
:class="{
|
||||
@@ -346,6 +351,7 @@
|
||||
savingProfile ||
|
||||
(!userProfileDesc && !includeUserProfileLocation),
|
||||
}"
|
||||
@click="confirmDeleteProfile"
|
||||
>
|
||||
Delete Profile
|
||||
</button>
|
||||
@@ -363,7 +369,8 @@
|
||||
<div class="mb-2 font-bold">Usage Limits</div>
|
||||
<!-- show spinner if loading limits -->
|
||||
<div v-if="loadingLimits" class="text-center">
|
||||
Checking… <fa icon="spinner" class="fa-spin"></fa>
|
||||
Checking…
|
||||
<font-awesome icon="spinner" class="fa-spin"></font-awesome>
|
||||
</div>
|
||||
<div class="mb-4 text-center">
|
||||
{{ limitsMessage }}
|
||||
@@ -419,15 +426,15 @@
|
||||
>
|
||||
<div class="mb-2 font-bold">Data Export</div>
|
||||
<router-link
|
||||
:to="{ name: 'seed-backup' }"
|
||||
v-if="activeDid"
|
||||
:to="{ name: 'seed-backup' }"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
|
||||
>
|
||||
Backup Identifier Seed
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
v-bind:class="computedStartDownloadLinkClassNames()"
|
||||
:class="computedStartDownloadLinkClassNames()"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||
@click="exportDatabase()"
|
||||
>
|
||||
@@ -437,7 +444,7 @@
|
||||
</button>
|
||||
<a
|
||||
ref="downloadLink"
|
||||
v-bind:class="computedDownloadLinkClassNames()"
|
||||
:class="computedDownloadLinkClassNames()"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
||||
>
|
||||
If no download happened yet, click again here to download now.
|
||||
@@ -454,7 +461,7 @@
|
||||
</li>
|
||||
<li class="list-disc list-outside ml-4">
|
||||
On Android: Choose "Open" and then share
|
||||
<fa icon="share-nodes" class="fa-fw" />
|
||||
<font-awesome icon="share-nodes" class="fa-fw" />
|
||||
to your prefered place.
|
||||
</li>
|
||||
</ul>
|
||||
@@ -469,7 +476,7 @@
|
||||
>
|
||||
Advanced
|
||||
</h3>
|
||||
<div id="sectionAdvanced" v-if="showAdvanced || showGeneralAdvanced">
|
||||
<div v-if="showAdvanced || showGeneralAdvanced" id="sectionAdvanced">
|
||||
<p class="text-rose-600 mb-8">
|
||||
Beware: the features here can be confusing and even change data in ways
|
||||
you do not expect. But we support your freedom!
|
||||
@@ -489,12 +496,15 @@
|
||||
>
|
||||
<code class="truncate">{{ publicBase64 }}</code>
|
||||
<button
|
||||
class="ml-2"
|
||||
@click="
|
||||
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
<font-awesome
|
||||
icon="copy"
|
||||
class="text-slate-400 fa-fw"
|
||||
></font-awesome>
|
||||
</button>
|
||||
<span v-show="showB64Copy">Copied</span>
|
||||
</div>
|
||||
@@ -505,12 +515,15 @@
|
||||
>
|
||||
<code class="truncate">{{ publicHex }}</code>
|
||||
<button
|
||||
class="ml-2"
|
||||
@click="
|
||||
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
<font-awesome
|
||||
icon="copy"
|
||||
class="text-slate-400 fa-fw"
|
||||
></font-awesome>
|
||||
</button>
|
||||
<span v-show="showPubCopy">Copied</span>
|
||||
</div>
|
||||
@@ -522,15 +535,18 @@
|
||||
>
|
||||
<code class="truncate">{{ derivationPath }}</code>
|
||||
<button
|
||||
class="ml-2"
|
||||
@click="
|
||||
doCopyTwoSecRedo(
|
||||
derivationPath,
|
||||
() => (showDerCopy = !showDerCopy),
|
||||
)
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
<font-awesome
|
||||
icon="copy"
|
||||
class="text-slate-400 fa-fw"
|
||||
></font-awesome>
|
||||
</button>
|
||||
<span v-show="showDerCopy">Copied</span>
|
||||
</div>
|
||||
@@ -557,7 +573,7 @@
|
||||
</h2>
|
||||
|
||||
<div class="ml-4 mt-2">
|
||||
<input type="file" @change="uploadImportFile" class="ml-2" />
|
||||
<input type="file" class="ml-2" @change="uploadImportFile" />
|
||||
<transition
|
||||
enter-active-class="transform ease-out duration-300 transition"
|
||||
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
|
||||
@@ -604,8 +620,8 @@
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="showContactGives"
|
||||
type="checkbox"
|
||||
name="showContactGives"
|
||||
class="sr-only"
|
||||
/>
|
||||
@@ -622,16 +638,20 @@
|
||||
<h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
|
||||
<div class="px-4 py-4">
|
||||
<input
|
||||
v-model="apiServerInput"
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 px-4 py-2"
|
||||
v-model="apiServerInput"
|
||||
/>
|
||||
<button
|
||||
v-if="apiServerInput != apiServer"
|
||||
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
||||
@click="onClickSaveApiServer()"
|
||||
>
|
||||
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||
<font-awesome
|
||||
icon="floppy-disk"
|
||||
class="fa-fw"
|
||||
color="white"
|
||||
></font-awesome>
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@@ -663,7 +683,7 @@
|
||||
<!-- toggle -->
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
|
||||
<input v-model="warnIfProdServer" type="checkbox" class="sr-only" />
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<!-- dot -->
|
||||
@@ -683,7 +703,7 @@
|
||||
<!-- toggle -->
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
|
||||
<input v-model="warnIfTestServer" type="checkbox" class="sr-only" />
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
|
||||
<!-- dot -->
|
||||
@@ -699,16 +719,20 @@
|
||||
</h2>
|
||||
<div class="px-3 py-4">
|
||||
<input
|
||||
v-model="webPushServerInput"
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||
v-model="webPushServerInput"
|
||||
/>
|
||||
<button
|
||||
v-if="webPushServerInput != webPushServer"
|
||||
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
||||
@click="onClickSavePushServer()"
|
||||
>
|
||||
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||
<font-awesome
|
||||
icon="floppy-disk"
|
||||
class="fa-fw"
|
||||
color="white"
|
||||
></font-awesome>
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@@ -729,7 +753,7 @@
|
||||
Use Test 2
|
||||
</button>
|
||||
</div>
|
||||
<span class="px-4 text-sm" v-if="!webPushServerInput">
|
||||
<span v-if="!webPushServerInput" class="px-4 text-sm">
|
||||
When that setting is blank, this app will use the default web push
|
||||
server URL:
|
||||
{{ DEFAULT_PUSH_SERVER }}
|
||||
@@ -738,16 +762,20 @@
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Partner Server URL</h2>
|
||||
<div class="px-3 py-4">
|
||||
<input
|
||||
v-model="partnerApiServerInput"
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||
v-model="partnerApiServerInput"
|
||||
/>
|
||||
<button
|
||||
v-if="partnerApiServerInput != partnerApiServer"
|
||||
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
|
||||
@click="onClickSavePartnerServer()"
|
||||
>
|
||||
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
|
||||
<font-awesome
|
||||
icon="floppy-disk"
|
||||
class="fa-fw"
|
||||
color="white"
|
||||
></font-awesome>
|
||||
</button>
|
||||
<button
|
||||
class="px-3 rounded bg-slate-200 border border-slate-400"
|
||||
@@ -768,7 +796,7 @@
|
||||
Use Local
|
||||
</button>
|
||||
</div>
|
||||
<span class="px-4 text-sm" v-if="!partnerApiServerInput">
|
||||
<span v-if="!partnerApiServerInput" class="px-4 text-sm">
|
||||
When that setting is blank, this app will use the default partner server
|
||||
URL:
|
||||
{{ DEFAULT_PARTNER_API_SERVER }}
|
||||
@@ -793,8 +821,8 @@
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="hideRegisterPromptOnNewContact"
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
/>
|
||||
<!-- line -->
|
||||
@@ -818,7 +846,7 @@
|
||||
<!-- toggle -->
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input type="checkbox" v-model="showShortcutBvc" class="sr-only" />
|
||||
<input v-model="showShortcutBvc" type="checkbox" class="sr-only" />
|
||||
<!-- line -->
|
||||
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
|
||||
<!-- dot -->
|
||||
@@ -851,9 +879,9 @@
|
||||
</span>
|
||||
<div class="relative ml-2">
|
||||
<input
|
||||
v-model="passkeyExpirationMinutes"
|
||||
type="number"
|
||||
class="border border-slate-400 rounded px-2 py-2 text-center w-20"
|
||||
v-model="passkeyExpirationMinutes"
|
||||
@change="updatePasskeyExpiration"
|
||||
/>
|
||||
</div>
|
||||
@@ -872,8 +900,8 @@
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="showGeneralAdvanced"
|
||||
type="checkbox"
|
||||
class="sr-only"
|
||||
/>
|
||||
<!-- line -->
|
||||
@@ -895,13 +923,13 @@ import { AxiosError } from "axios";
|
||||
import { Buffer } from "buffer/";
|
||||
import Dexie from "dexie";
|
||||
import "dexie-export-import";
|
||||
import { ImportProgress } from "dexie-export-import/dist/import";
|
||||
import { ImportProgress } from "dexie-export-import";
|
||||
import { LeafletMouseEvent } from "leaflet";
|
||||
import * as R from "ramda";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { ref } from "vue";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||
|
||||
@@ -947,6 +975,8 @@ import {
|
||||
DIRECT_PUSH_TITLE,
|
||||
retrieveAccountMetadata,
|
||||
} from "../libs/util";
|
||||
import { UserProfile } from "@/libs/partnerServer";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
const inputImportFileNameRef = ref<Blob>();
|
||||
|
||||
@@ -966,6 +996,8 @@ const inputImportFileNameRef = ref<Blob>();
|
||||
})
|
||||
export default class AccountViewView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
AppConstants = AppString;
|
||||
DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
|
||||
@@ -1078,12 +1110,12 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
} catch (error) {
|
||||
// this can happen when running automated tests in dev mode because notifications don't work
|
||||
console.error(
|
||||
logger.error(
|
||||
"Telling user to clear cache at page create because:",
|
||||
error,
|
||||
);
|
||||
// this sometimes gives different information on the error
|
||||
console.error(
|
||||
logger.error(
|
||||
"To repeat with concatenated error: telling user to clear cache at page create because: " +
|
||||
error,
|
||||
);
|
||||
@@ -1490,7 +1522,7 @@ export default class AccountViewView extends Vue {
|
||||
* @param {Error} error - The error object.
|
||||
*/
|
||||
private handleExportError(error: unknown) {
|
||||
console.error("Export Error:", error);
|
||||
logger.error("Export Error:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -1560,7 +1592,7 @@ export default class AccountViewView extends Vue {
|
||||
query: { contacts: JSON.stringify(contactRows) },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error checking contact imports:", error);
|
||||
logger.error("Error checking contact imports:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -1576,7 +1608,7 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
private progressCallback(progress: ImportProgress) {
|
||||
console.log(
|
||||
logger.log(
|
||||
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
||||
);
|
||||
if (progress.done) {
|
||||
@@ -1628,7 +1660,7 @@ export default class AccountViewView extends Vue {
|
||||
await updateAccountSettings(did, { isRegistered: true });
|
||||
this.isRegistered = true;
|
||||
} catch (err) {
|
||||
console.error("Got an error updating settings:", err);
|
||||
logger.error("Got an error updating settings:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -1667,7 +1699,7 @@ export default class AccountViewView extends Vue {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.status == 400 || error.status == 404) {
|
||||
// no worries: they probably just aren't registered and don't have any limits
|
||||
console.log(
|
||||
logger.log(
|
||||
"Got 400 or 404 response retrieving limits which probably means they're not registered:",
|
||||
error,
|
||||
);
|
||||
@@ -1676,11 +1708,11 @@ export default class AccountViewView extends Vue {
|
||||
const data = error.response?.data as ErrorResponse;
|
||||
this.limitsMessage =
|
||||
(data?.error?.message as string) || "Bad server response.";
|
||||
console.error("Got bad response retrieving limits:", error);
|
||||
logger.error("Got bad response retrieving limits:", error);
|
||||
}
|
||||
} else {
|
||||
this.limitsMessage = "Got an error retrieving limits.";
|
||||
console.error("Got some error retrieving limits:", error);
|
||||
logger.error("Got some error retrieving limits:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1757,7 +1789,7 @@ export default class AccountViewView extends Vue {
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
) {
|
||||
console.log(
|
||||
logger.log(
|
||||
"Using shared image API server, so only users on that server can play with images.",
|
||||
);
|
||||
}
|
||||
@@ -1771,7 +1803,7 @@ export default class AccountViewView extends Vue {
|
||||
// don't bother with a notification
|
||||
// (either they'll simply continue or they're canceling and going back)
|
||||
} else {
|
||||
console.error("Non-success deleting image:", response);
|
||||
logger.error("Non-success deleting image:", response);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -1791,10 +1823,10 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
this.profileImageUrl = undefined;
|
||||
} catch (error) {
|
||||
console.error("Error deleting image:", error);
|
||||
logger.error("Error deleting image:", error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any).response.status === 404) {
|
||||
console.error("The image was already deleted:", error);
|
||||
logger.error("The image was already deleted:", error);
|
||||
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
profileImageUrl: undefined,
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<!-- Back -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw" />
|
||||
<font-awesome icon="chevron-left" class="fa-fw" />
|
||||
</button>
|
||||
Raw Claim
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<textarea rows="20" class="border-2 w-full" v-model="claimStr"></textarea>
|
||||
<textarea v-model="claimStr" rows="20" class="border-2 w-full"></textarea>
|
||||
</div>
|
||||
<button
|
||||
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { AxiosInstance } from "axios";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
@@ -38,76 +38,153 @@ import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { errorStringForLog } from "../libs/endorserServer";
|
||||
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* View component for adding or editing raw claim data
|
||||
* Allows direct JSON editing of claim data with validation and submission
|
||||
*/
|
||||
@Component({
|
||||
components: { QuickNav },
|
||||
})
|
||||
export default class ClaimAddRawView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
axios!: AxiosInstance;
|
||||
|
||||
accountIdentityStr: string = "null";
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
claimStr = "";
|
||||
|
||||
/**
|
||||
* Lifecycle hook that initializes the view
|
||||
* Workflow:
|
||||
* 1. Retrieves active DID and API server from settings
|
||||
* 2. Checks for existing claim data from query params:
|
||||
* - If "claim" param exists: Parses and formats JSON
|
||||
* - If "claimJwtId" param exists: Fetches claim data from API
|
||||
* 3. Populates textarea with formatted claim data
|
||||
*/
|
||||
async mounted() {
|
||||
await this.initializeSettings();
|
||||
await this.loadClaimData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize settings from active account
|
||||
*/
|
||||
private async initializeSettings() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
}
|
||||
|
||||
this.claimStr = (this.$route as Router).query["claim"];
|
||||
if (this.claimStr) {
|
||||
try {
|
||||
const veriClaim = JSON.parse(this.claimStr);
|
||||
this.claimStr = JSON.stringify(veriClaim, null, 2);
|
||||
} catch (e) {
|
||||
// ignore a parse error
|
||||
}
|
||||
} else {
|
||||
// there may be no link that uses this, meaning you'd have to enter it in a browser
|
||||
const claimJwtId = (this.$route as Router).query["claimJwtId"];
|
||||
if (claimJwtId) {
|
||||
const urlPath = libsUtil.isGlobalUri(claimJwtId)
|
||||
? "/api/claim/byHandle/"
|
||||
: "/api/claim/";
|
||||
const url = this.apiServer + urlPath + encodeURIComponent(claimJwtId);
|
||||
const headers = await serverUtil.getHeaders(this.activeDid);
|
||||
/**
|
||||
* Load claim data from query parameters or API
|
||||
*/
|
||||
private async loadClaimData() {
|
||||
// Try loading from direct claim parameter
|
||||
if (await this.loadClaimFromQueryParam()) return;
|
||||
|
||||
try {
|
||||
const response = await this.axios.get(url, { headers });
|
||||
if (response.status === 200) {
|
||||
const claim = response.data?.claim;
|
||||
claim.lastClaimId = serverUtil.stripEndorserPrefix(claimJwtId);
|
||||
this.claimStr = JSON.stringify(claim, null, 2);
|
||||
} else {
|
||||
throw {
|
||||
message: "Got an error loading that claim.",
|
||||
response: {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
// url is in "fetch" response but not in AxiosResponse
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
logConsoleAndDb(
|
||||
"Error retrieving claim: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Got an error retrieving claim data.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Try loading from claim JWT ID
|
||||
await this.loadClaimFromJwtId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to load claim from query parameter
|
||||
* @returns true if claim was loaded successfully
|
||||
*/
|
||||
private async loadClaimFromQueryParam(): Promise<boolean> {
|
||||
this.claimStr = (this.$route.query["claim"] as string) || "";
|
||||
if (!this.claimStr) return false;
|
||||
|
||||
try {
|
||||
const veriClaim = JSON.parse(this.claimStr);
|
||||
this.claimStr = JSON.stringify(veriClaim, null, 2);
|
||||
return true;
|
||||
} catch (e) {
|
||||
// ignore parse error
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load claim data from JWT ID via API
|
||||
*/
|
||||
private async loadClaimFromJwtId() {
|
||||
const claimJwtId = (this.$route.query["claimJwtId"] as string) || "";
|
||||
if (!claimJwtId) return;
|
||||
|
||||
const urlPath = libsUtil.isGlobalUri(claimJwtId)
|
||||
? "/api/claim/byHandle/"
|
||||
: "/api/claim/";
|
||||
const url = this.apiServer + urlPath + encodeURIComponent(claimJwtId);
|
||||
|
||||
try {
|
||||
const response = await this.fetchClaimData(url);
|
||||
this.formatClaimResponse(response, claimJwtId);
|
||||
} catch (error: unknown) {
|
||||
this.handleClaimError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch claim data from API
|
||||
*/
|
||||
private async fetchClaimData(url: string) {
|
||||
const headers = await serverUtil.getHeaders(this.activeDid);
|
||||
return await this.axios.get(url, { headers });
|
||||
}
|
||||
|
||||
/**
|
||||
* Format successful claim response data
|
||||
*/
|
||||
private formatClaimResponse(response: unknown, claimJwtId: string) {
|
||||
if (response.status === 200) {
|
||||
const claim = response.data?.claim;
|
||||
claim.lastClaimId = serverUtil.stripEndorserPrefix(claimJwtId);
|
||||
this.claimStr = JSON.stringify(claim, null, 2);
|
||||
} else {
|
||||
throw {
|
||||
message: "Got an error loading that claim.",
|
||||
response: {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle error loading claim data
|
||||
*/
|
||||
private handleClaimError(error: unknown) {
|
||||
logConsoleAndDb(
|
||||
"Error retrieving claim: " + errorStringForLog(error),
|
||||
true,
|
||||
);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Got an error retrieving claim data.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the edited claim data
|
||||
* Workflow:
|
||||
* 1. Parses JSON from textarea
|
||||
* 2. Sends to server via createAndSubmitClaim
|
||||
* 3. Shows success/error notification
|
||||
* @throws Will show error notification if submission fails
|
||||
*/
|
||||
async submitClaim() {
|
||||
const fullClaim = JSON.parse(this.claimStr);
|
||||
const result = await serverUtil.createAndSubmitClaim(
|
||||
@@ -127,7 +204,7 @@ export default class ClaimAddRawView extends Vue {
|
||||
5000,
|
||||
);
|
||||
} else {
|
||||
console.error("Got error submitting the claim:", result);
|
||||
logger.error("Got error submitting the claim:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<section id="Content">
|
||||
<div class="flex items-center justify-center h-screen">
|
||||
<div v-if="claimData">
|
||||
<router-link :to="'/claim/' + this.claimId">
|
||||
<canvas class="w-full block mx-auto" ref="claimCanvas"></canvas>
|
||||
<router-link :to="'/claim/' + claimId">
|
||||
<canvas ref="claimCanvas" class="w-full block mx-auto"></canvas>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -14,11 +14,11 @@
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { nextTick } from "vue";
|
||||
import QRCode from "qrcode";
|
||||
|
||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
|
||||
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
|
||||
import { logger } from "../utils/logger";
|
||||
@Component
|
||||
export default class ClaimCertificateView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
@@ -70,7 +70,7 @@ export default class ClaimCertificateView extends Vue {
|
||||
throw new Error(`Error fetching claim: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load claim:", error);
|
||||
logger.error("Failed to load claim:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
@@ -81,7 +81,7 @@ export default class ClaimCertificateView extends Vue {
|
||||
}
|
||||
|
||||
async drawCanvas(
|
||||
claimData: serverUtil.GenericCredWrapper<serverUtil.GenericVerifiableCredential>,
|
||||
claimData: GenericCredWrapper<GenericVerifiableCredential>,
|
||||
confirmerIds: Array<string>,
|
||||
) {
|
||||
await db.open();
|
||||
|
||||
@@ -6,16 +6,6 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { nextTick } from "vue";
|
||||
@@ -24,7 +14,7 @@ import QRCode from "qrcode";
|
||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import * as endorserServer from "../libs/endorserServer";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component
|
||||
export default class ClaimReportCertificateView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
@@ -63,7 +53,7 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
throw new Error(`Error fetching claim: ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load claim:", error);
|
||||
logger.error("Failed to load claim:", error);
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
@@ -188,3 +178,13 @@ export default class ClaimReportCertificateView extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<!-- Back -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw" />
|
||||
<font-awesome icon="chevron-left" class="fa-fw" />
|
||||
</button>
|
||||
Verifiable Claim Details
|
||||
</h1>
|
||||
@@ -22,7 +22,9 @@
|
||||
<div class="w-full">
|
||||
<div class="flex columns-3">
|
||||
<h2 class="text-md font-bold w-full">
|
||||
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
|
||||
{{
|
||||
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
|
||||
}}
|
||||
<button
|
||||
v-if="
|
||||
['GiveAction', 'Offer', 'PlanAction'].includes(
|
||||
@@ -32,11 +34,14 @@
|
||||
// but rather than add more Plan-specific logic to detect the agent
|
||||
// we'll let them click the Project link and edit from there
|
||||
"
|
||||
@click="onClickEditClaim"
|
||||
title="Edit"
|
||||
data-testId="editClaimButton"
|
||||
@click="onClickEditClaim"
|
||||
>
|
||||
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||
<font-awesome
|
||||
icon="pen"
|
||||
class="text-sm text-blue-500 ml-2 mb-1"
|
||||
/>
|
||||
</button>
|
||||
</h2>
|
||||
<div class="flex justify-center w-full">
|
||||
@@ -45,7 +50,10 @@
|
||||
class="text-blue-500 mt-2"
|
||||
title="Printable Certificate"
|
||||
>
|
||||
<fa icon="square" class="text-white bg-yellow-500 p-1" />
|
||||
<font-awesome
|
||||
icon="square"
|
||||
class="text-white bg-yellow-500 p-1"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- show link icon to copy this URL to the clipboard -->
|
||||
@@ -56,30 +64,37 @@
|
||||
copyToClipboard('A link to this page', window.location.href)
|
||||
"
|
||||
>
|
||||
<fa icon="link" class="text-slate-500" />
|
||||
<font-awesome icon="link" class="text-slate-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<div data-testId="description">
|
||||
<fa icon="message" class="fa-fw text-slate-400" />
|
||||
<font-awesome icon="message" class="fa-fw text-slate-400" />
|
||||
{{
|
||||
veriClaim.claim?.itemOffered?.description ||
|
||||
veriClaim.claim?.description
|
||||
(veriClaim.claim?.itemOffered as any)?.description ||
|
||||
(veriClaim.claim as any)?.description ||
|
||||
""
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
<fa icon="user" class="fa-fw text-slate-400" />
|
||||
<font-awesome icon="user" class="fa-fw text-slate-400" />
|
||||
{{ didInfo(veriClaim.issuer) }}
|
||||
</div>
|
||||
<div>
|
||||
<fa icon="calendar" class="fa-fw text-slate-400" />
|
||||
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
|
||||
Recorded
|
||||
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
|
||||
</div>
|
||||
<div v-if="veriClaim.claim.image" class="flex justify-center">
|
||||
<a :href="veriClaim.claim.image" target="_blank">
|
||||
<img :src="veriClaim.claim.image" class="h-24 rounded-xl" />
|
||||
<div
|
||||
v-if="(veriClaim.claim as any).image"
|
||||
class="flex justify-center"
|
||||
>
|
||||
<a :href="(veriClaim.claim as any).image" target="_blank">
|
||||
<img
|
||||
:src="(veriClaim.claim as any).image"
|
||||
class="h-24 rounded-xl"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -116,10 +131,10 @@
|
||||
>
|
||||
<!-- router-link to /claim/ only changes URL path -->
|
||||
<a
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
@click="
|
||||
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
|
||||
"
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
>
|
||||
Fulfills
|
||||
{{
|
||||
@@ -155,15 +170,15 @@
|
||||
<div class="flex gap-4">
|
||||
<div class="grow overflow-hidden">
|
||||
<a
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
@click="
|
||||
provider.identifier.startsWith('did:')
|
||||
? this.$router.push(
|
||||
? $router.push(
|
||||
'/did/' +
|
||||
encodeURIComponent(provider.identifier),
|
||||
)
|
||||
: showDifferentClaimPage(provider.identifier)
|
||||
"
|
||||
class="text-blue-500 mt-4 cursor-pointer"
|
||||
>
|
||||
an activity...
|
||||
</a>
|
||||
@@ -177,13 +192,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<fa icon="comment" class="text-slate-400" />
|
||||
<font-awesome icon="comment" class="text-slate-400" />
|
||||
{{ issuerName }} posted that.
|
||||
</div>
|
||||
<!--
|
||||
<div>
|
||||
<router-link :to="'/claim-cert/' + encodeURIComponent(veriClaim.id)">
|
||||
<fa icon="file-contract" class="text-slate-400" />
|
||||
<font-awesome icon="file-contract" class="text-slate-400" />
|
||||
<span class="ml-2 text-blue-500">Printable Certificate</span>
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -192,11 +207,14 @@
|
||||
<div class="mt-8">
|
||||
<button
|
||||
v-if="libsUtil.canFulfillOffer(veriClaim)"
|
||||
@click="openFulfillGiftDialog()"
|
||||
class="col-span-1 block w-fit text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||
@click="openFulfillGiftDialog()"
|
||||
>
|
||||
Affirm Delivery
|
||||
<fa icon="hand-holding-heart" class="ml-2 text-white cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="hand-holding-heart"
|
||||
class="ml-2 text-white cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<GiftedDialog ref="customGiveDialog" />
|
||||
@@ -204,7 +222,6 @@
|
||||
<div v-if="libsUtil.isGiveAction(veriClaim)">
|
||||
<div class="flex columns-3">
|
||||
<button
|
||||
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-4 py-2 rounded-md"
|
||||
v-if="
|
||||
libsUtil.isGiveRecordTheUserCanConfirm(
|
||||
isRegistered,
|
||||
@@ -213,10 +230,14 @@
|
||||
confirmerIdList,
|
||||
)
|
||||
"
|
||||
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-4 py-2 rounded-md"
|
||||
@click="confirmConfirmClaim()"
|
||||
>
|
||||
Confirm
|
||||
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="circle-check"
|
||||
class="ml-2 text-white cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
<h2 v-else class="font-bold uppercase text-xl mt-2">Confirmations</h2>
|
||||
|
||||
@@ -276,7 +297,10 @@
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -314,7 +338,10 @@
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -339,14 +366,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note that a similar section is found in ConfirmGiftView.vue, and kinda in HiddenDidDialog.vue -->
|
||||
<!--
|
||||
Note that a similar section is found in ConfirmGiftView.vue, and kinda in HiddenDidDialog.vue
|
||||
-->
|
||||
<h2
|
||||
class="font-bold uppercase text-xl text-blue-500 mt-8 cursor-pointer"
|
||||
@click="showVeriClaimDump = !showVeriClaimDump"
|
||||
>
|
||||
Details
|
||||
<fa v-if="showVeriClaimDump" icon="chevron-up" />
|
||||
<fa v-else icon="chevron-right" />
|
||||
<font-awesome v-if="showVeriClaimDump" icon="chevron-up" />
|
||||
<font-awesome v-else icon="chevron-right" />
|
||||
</h2>
|
||||
<div v-if="showVeriClaimDump">
|
||||
<div
|
||||
@@ -361,7 +390,7 @@
|
||||
<span v-if="canShare">
|
||||
You can ask one of your contacts to take a look and see if their
|
||||
contacts can see more details:
|
||||
<a @click="onClickShareClaim()" class="text-blue-500"
|
||||
<a class="text-blue-500" @click="onClickShareClaim()"
|
||||
>click to send them this page info</a
|
||||
>
|
||||
and see if they can make an introduction. Someone is connected to
|
||||
@@ -372,8 +401,8 @@
|
||||
You can ask one of your contacts to take a look and see if their
|
||||
contacts can see more details:
|
||||
<a
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
class="text-blue-500"
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
>click to copy this page info</a
|
||||
>
|
||||
and see if they can make an introduction. Someone is connected to
|
||||
@@ -387,7 +416,7 @@
|
||||
of your contacts.
|
||||
<span v-if="canShare">
|
||||
If you'd like an introduction,
|
||||
<a @click="onClickShareClaim()" class="text-blue-500"
|
||||
<a class="text-blue-500" @click="onClickShareClaim()"
|
||||
>click to share the information with them and ask if they'll tell
|
||||
you more about the participants.</a
|
||||
>
|
||||
@@ -395,8 +424,8 @@
|
||||
<span v-else>
|
||||
If you'd like an introduction,
|
||||
<a
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
class="text-blue-500"
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
>share this page with them and ask if they'll tell you more about
|
||||
about the participants.</a
|
||||
>
|
||||
@@ -408,7 +437,7 @@
|
||||
class="list-disc p-4"
|
||||
>
|
||||
<div class="text-sm">
|
||||
<fa icon="minus" class="fa-fw" />
|
||||
<font-awesome icon="minus" class="fa-fw" />
|
||||
The {{ visibleDidPath }} is visible to:
|
||||
</div>
|
||||
<div class="ml-12 p-1">
|
||||
@@ -427,7 +456,10 @@
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
<span v-if="veriClaim.publicUrls?.[visDid]"
|
||||
@@ -436,7 +468,7 @@
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<fa icon="globe" class="fa-fw" />
|
||||
<font-awesome icon="globe" class="fa-fw" />
|
||||
{{
|
||||
veriClaim.publicUrls[visDid].substring(
|
||||
veriClaim.publicUrls[visDid].indexOf("//") + 2,
|
||||
@@ -476,7 +508,7 @@
|
||||
class="text-blue-500 cursor-pointer"
|
||||
@click="showFullClaim(veriClaim.id as string)"
|
||||
>
|
||||
<fa icon="file-lines" class="fa-fw" />
|
||||
<font-awesome icon="file-lines" class="fa-fw" />
|
||||
Load Full Claim Details
|
||||
</button>
|
||||
</div>
|
||||
@@ -492,8 +524,8 @@
|
||||
target="_blank"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
>
|
||||
<fa icon="file-lines" class="fa-fw" />
|
||||
<fa icon="arrow-up-right-from-square" class="ml-1 fa-fw" />
|
||||
<font-awesome icon="file-lines" class="fa-fw" />
|
||||
<font-awesome icon="arrow-up-right-from-square" class="ml-1 fa-fw" />
|
||||
View on the Public Server
|
||||
</a>
|
||||
</div>
|
||||
@@ -505,9 +537,9 @@ import { AxiosError } from "axios";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as R from "ramda";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
|
||||
import { GenericVerifiableCredential } from "../interfaces";
|
||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
@@ -521,19 +553,18 @@ import * as serverUtil from "../libs/endorserServer";
|
||||
import {
|
||||
GenericCredWrapper,
|
||||
OfferVerifiableCredential,
|
||||
} from "../libs/endorserServer";
|
||||
ProviderInfo,
|
||||
} from "../interfaces";
|
||||
import * as libsUtil from "../libs/util";
|
||||
|
||||
interface ProviderInfo {
|
||||
identifier: string; // could be a DID or a handleId
|
||||
linkConfirmed: boolean;
|
||||
}
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@Component({
|
||||
components: { GiftedDialog, QuickNav },
|
||||
})
|
||||
export default class ClaimView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
allMyDids: Array<string> = [];
|
||||
@@ -544,8 +575,12 @@ export default class ClaimView extends Vue {
|
||||
confirmerIdList: string[] = []; // list of DIDs that have confirmed this claim excluding the issuer
|
||||
confsVisibleErrorMessage = "";
|
||||
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
|
||||
detailsForGive = null;
|
||||
detailsForOffer = null;
|
||||
detailsForGive: {
|
||||
fulfillsPlanHandleId?: string;
|
||||
fulfillsType?: string;
|
||||
fulfillsHandleId?: string;
|
||||
} | null = null;
|
||||
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
|
||||
fullClaim = null;
|
||||
fullClaimDump = "";
|
||||
fullClaimMessage = "";
|
||||
@@ -558,7 +593,7 @@ export default class ClaimView extends Vue {
|
||||
showVeriClaimDump = false;
|
||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||
veriClaimDump = "";
|
||||
veriClaimDidsVisible = {};
|
||||
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
||||
windowLocation = window.location.href;
|
||||
|
||||
R = R;
|
||||
@@ -585,6 +620,7 @@ export default class ClaimView extends Vue {
|
||||
}
|
||||
|
||||
async created() {
|
||||
logger.log("ClaimView created");
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
@@ -594,7 +630,6 @@ export default class ClaimView extends Vue {
|
||||
try {
|
||||
this.allMyDids = await libsUtil.retrieveAccountDids();
|
||||
} catch (error) {
|
||||
// continue because we want to see claims, even anonymously
|
||||
logConsoleAndDb(
|
||||
"Error retrieving all account DIDs on home page:" + error,
|
||||
true,
|
||||
@@ -610,10 +645,8 @@ export default class ClaimView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
const pathParam = window.location.pathname.substring("/claim/".length);
|
||||
let claimId;
|
||||
if (pathParam) {
|
||||
claimId = decodeURIComponent(pathParam);
|
||||
const claimId = this.$route.params.id as string;
|
||||
if (claimId) {
|
||||
await this.loadClaim(claimId, this.activeDid);
|
||||
} else {
|
||||
this.$notify(
|
||||
@@ -627,8 +660,6 @@ export default class ClaimView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
// When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
|
||||
// then use this truer check: navigator.canShare && navigator.canShare()
|
||||
this.canShare = !!navigator.share;
|
||||
}
|
||||
|
||||
@@ -659,6 +690,7 @@ export default class ClaimView extends Vue {
|
||||
}
|
||||
|
||||
async loadClaim(claimId: string, userDid: string) {
|
||||
logger.log("[ClaimView] loadClaim called with claimId:", claimId);
|
||||
const urlPath = libsUtil.isGlobalUri(claimId)
|
||||
? "/api/claim/byHandle/"
|
||||
: "/api/claim/";
|
||||
@@ -666,6 +698,7 @@ export default class ClaimView extends Vue {
|
||||
const headers = await serverUtil.getHeaders(userDid);
|
||||
|
||||
try {
|
||||
logger.log("[ClaimView] Making API request to:", url);
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
if (resp.status === 200) {
|
||||
this.veriClaim = resp.data;
|
||||
@@ -677,7 +710,7 @@ export default class ClaimView extends Vue {
|
||||
);
|
||||
} else {
|
||||
// actually, axios typically throws an error so we never get here
|
||||
console.error("Error getting claim:", resp);
|
||||
logger.error("Error getting claim:", resp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -705,7 +738,7 @@ export default class ClaimView extends Vue {
|
||||
if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
|
||||
this.detailsForGive = giveResp.data.data[0];
|
||||
} else {
|
||||
console.error("Error getting detailed give info:", giveResp);
|
||||
logger.error("Error getting detailed give info:", giveResp);
|
||||
}
|
||||
|
||||
// look for providers
|
||||
@@ -724,7 +757,7 @@ export default class ClaimView extends Vue {
|
||||
) {
|
||||
this.providersForGive = providerResp.data.data;
|
||||
} else {
|
||||
console.error("Error getting give providers:", giveResp);
|
||||
logger.error("Error getting give providers:", giveResp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -747,7 +780,7 @@ export default class ClaimView extends Vue {
|
||||
if (offerResp.status === 200) {
|
||||
this.detailsForOffer = offerResp.data.data[0];
|
||||
} else {
|
||||
console.error("Error getting detailed offer info:", offerResp);
|
||||
logger.error("Error getting detailed offer info:", offerResp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -777,7 +810,7 @@ export default class ClaimView extends Vue {
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const serverError = error as AxiosError;
|
||||
console.error("Error retrieving claim:", serverError);
|
||||
logger.error("Error retrieving claim:", serverError);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -802,7 +835,7 @@ export default class ClaimView extends Vue {
|
||||
this.fullClaimDump = yaml.dump(this.fullClaim);
|
||||
} else {
|
||||
// actually, axios typically throws an error so we never get here
|
||||
console.error("Error getting full claim:", resp);
|
||||
logger.error("Error getting full claim:", resp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -814,7 +847,7 @@ export default class ClaimView extends Vue {
|
||||
);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Error retrieving full claim:", error);
|
||||
logger.error("Error retrieving full claim:", error);
|
||||
const serverError = error as AxiosError;
|
||||
if (serverError.response?.status === 403) {
|
||||
let issuerPhrase = "";
|
||||
@@ -887,7 +920,7 @@ export default class ClaimView extends Vue {
|
||||
),
|
||||
),
|
||||
);
|
||||
const confirmationClaim: serverUtil.GenericVerifiableCredential = {
|
||||
const confirmationClaim: GenericVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "AgreeAction",
|
||||
object: goodClaim,
|
||||
@@ -909,7 +942,7 @@ export default class ClaimView extends Vue {
|
||||
5000,
|
||||
);
|
||||
} else {
|
||||
console.error("Got error submitting the confirmation:", result);
|
||||
logger.error("Got error submitting the confirmation:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -999,7 +1032,7 @@ export default class ClaimView extends Vue {
|
||||
};
|
||||
(this.$router as Router).push(route);
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Unrecognized claim type for edit:",
|
||||
this.veriClaim.claimType,
|
||||
);
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<router-link
|
||||
:to="{ name: 'account' }"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
><fa icon="chevron-left" class="fa-fw"></fa
|
||||
></router-link>
|
||||
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</router-link>
|
||||
|
||||
Confirm Contact
|
||||
</h1>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<!-- Back -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw" />
|
||||
<font-awesome icon="chevron-left" class="fa-fw" />
|
||||
</button>
|
||||
<span
|
||||
v-if="
|
||||
@@ -32,7 +32,6 @@
|
||||
<div v-if="giveDetails && !isLoading">
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
v-if="
|
||||
libsUtil.isGiveRecordTheUserCanConfirm(
|
||||
isRegistered,
|
||||
@@ -41,18 +40,25 @@
|
||||
confirmerIdList,
|
||||
)
|
||||
"
|
||||
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@click="confirmConfirmClaim()"
|
||||
>
|
||||
Confirm
|
||||
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="circle-check"
|
||||
class="ml-2 text-white cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click="notifyWhyCannotConfirm()"
|
||||
class="col-span-1 bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@click="notifyWhyCannotConfirm()"
|
||||
>
|
||||
Confirm
|
||||
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="circle-check"
|
||||
class="ml-2 text-white cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -62,26 +68,29 @@
|
||||
<div class="overflow-hidden">
|
||||
<div class="text-sm">
|
||||
<div>
|
||||
<fa icon="arrow-left" class="fa-fw text-slate-400" />
|
||||
<font-awesome icon="arrow-left" class="fa-fw text-slate-400" />
|
||||
{{ giverName }}
|
||||
</div>
|
||||
<div class="ml-6">gave</div>
|
||||
<div v-if="giveDetails.amount">
|
||||
<fa icon="hand-holding-dollar" class="fa-fw text-slate-400" />
|
||||
<font-awesome
|
||||
icon="hand-holding-dollar"
|
||||
class="fa-fw text-slate-400"
|
||||
/>
|
||||
{{ displayAmount(giveDetails.unit, giveDetails.amount) }}
|
||||
</div>
|
||||
<div v-if="giveDetails.description">
|
||||
<fa icon="message" class="fa-fw text-slate-400" />
|
||||
<font-awesome icon="message" class="fa-fw text-slate-400" />
|
||||
{{ giveDetails.amount ? "and:" : "" }}
|
||||
{{ giveDetails.description }}
|
||||
</div>
|
||||
<div class="ml-6">to</div>
|
||||
<div>
|
||||
<fa icon="arrow-right" class="fa-fw text-slate-400" />
|
||||
<font-awesome icon="arrow-right" class="fa-fw text-slate-400" />
|
||||
{{ recipientName }}
|
||||
</div>
|
||||
<div>
|
||||
<fa icon="calendar" class="fa-fw text-slate-400" />
|
||||
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
|
||||
on
|
||||
{{ giveDetails.issuedAt.substring(0, 10) }}
|
||||
</div>
|
||||
@@ -89,7 +98,7 @@
|
||||
<!-- Fullfills Links -->
|
||||
|
||||
<!-- fullfills links for a give -->
|
||||
<div class="mt-2" v-if="giveDetails?.fulfillsPlanHandleId">
|
||||
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2">
|
||||
<router-link
|
||||
:to="
|
||||
'/project/' +
|
||||
@@ -99,7 +108,10 @@
|
||||
target="_blank"
|
||||
>
|
||||
This fulfills a bigger plan
|
||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- if there's another, it's probably fulfilling an offer, too -->
|
||||
@@ -125,7 +137,10 @@
|
||||
giveDetails?.fulfillsType || "",
|
||||
)
|
||||
}}
|
||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,7 +148,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<fa icon="comment" class="text-slate-400" />
|
||||
<font-awesome icon="comment" class="text-slate-400" />
|
||||
{{ issuerName }} posted that.
|
||||
</div>
|
||||
|
||||
@@ -185,7 +200,10 @@
|
||||
)
|
||||
"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw" />
|
||||
<font-awesome
|
||||
icon="copy"
|
||||
class="text-slate-400 fa-fw"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -228,7 +246,10 @@
|
||||
)
|
||||
"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw" />
|
||||
<font-awesome
|
||||
icon="copy"
|
||||
class="text-slate-400 fa-fw"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -254,14 +275,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note that a similar section is found in ClaimView.vue, and kinda in HiddenDidDialog.vue -->
|
||||
<!--
|
||||
Note that a similar section is found in ClaimView.vue, and kinda in HiddenDidDialog.vue
|
||||
-->
|
||||
<h2
|
||||
class="font-bold uppercase text-xl text-blue-500 mt-8 cursor-pointer"
|
||||
@click="showVeriClaimDump = !showVeriClaimDump"
|
||||
>
|
||||
Details
|
||||
<fa v-if="showVeriClaimDump" icon="chevron-up" />
|
||||
<fa v-else icon="chevron-right" />
|
||||
<font-awesome v-if="showVeriClaimDump" icon="chevron-up" />
|
||||
<font-awesome v-else icon="chevron-right" />
|
||||
</h2>
|
||||
<div v-if="showVeriClaimDump">
|
||||
<div
|
||||
@@ -276,7 +299,7 @@
|
||||
<span v-if="canShare">
|
||||
You can ask one of your contacts to take a look and see if their
|
||||
contacts can see more details:
|
||||
<a @click="onClickShareClaim()" class="text-blue-500"
|
||||
<a class="text-blue-500" @click="onClickShareClaim()"
|
||||
>click to send them this page info</a
|
||||
>
|
||||
and see if they can make an introduction. Someone is connected to
|
||||
@@ -287,8 +310,8 @@
|
||||
You can ask one of your contacts to take a look and see if their
|
||||
contacts can see more details:
|
||||
<a
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
class="text-blue-500"
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
>click to copy this page info</a
|
||||
>
|
||||
and see if they can make an introduction. Someone is connected to
|
||||
@@ -302,7 +325,7 @@
|
||||
some of your contacts.
|
||||
<span v-if="canShare">
|
||||
If you'd like an introduction,
|
||||
<a @click="onClickShareClaim()" class="text-blue-500"
|
||||
<a class="text-blue-500" @click="onClickShareClaim()"
|
||||
>click to share the information with them and ask if they'll tell
|
||||
you more about the participants.</a
|
||||
>
|
||||
@@ -310,8 +333,8 @@
|
||||
<span v-else>
|
||||
If you'd like an introduction,
|
||||
<a
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
class="text-blue-500"
|
||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||
>share this page with them and ask if they'll tell you more about
|
||||
about the participants.</a
|
||||
>
|
||||
@@ -323,7 +346,7 @@
|
||||
class="list-disc p-4"
|
||||
>
|
||||
<div class="text-sm">
|
||||
<fa icon="minus" class="fa-fw" />
|
||||
<font-awesome icon="minus" class="fa-fw" />
|
||||
The {{ visibleDidPath }} is visible to:
|
||||
</div>
|
||||
<div class="ml-12 p-1">
|
||||
@@ -342,12 +365,18 @@
|
||||
copyToClipboard('The DID of ' + visDid, visDid)
|
||||
"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw" />
|
||||
<font-awesome
|
||||
icon="copy"
|
||||
class="text-slate-400 fa-fw"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="veriClaim.publicUrls?.[visDid]"
|
||||
>, found at
|
||||
<fa icon="globe" class="fa-fw text-slate-400" />
|
||||
<font-awesome
|
||||
icon="globe"
|
||||
class="fa-fw text-slate-400"
|
||||
/>
|
||||
<a
|
||||
:href="veriClaim.publicUrls?.[visDid]"
|
||||
class="text-blue-500"
|
||||
@@ -372,10 +401,10 @@
|
||||
>
|
||||
<div class="mt-2 ml-2">
|
||||
<a
|
||||
@click="showClaimPage(veriClaim.id)"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
@click="showClaimPage(veriClaim.id)"
|
||||
>
|
||||
<fa icon="file-lines" />
|
||||
<font-awesome icon="file-lines" />
|
||||
See All Generic Info
|
||||
</a>
|
||||
</div>
|
||||
@@ -385,7 +414,7 @@
|
||||
class="text-blue-500 cursor-pointer"
|
||||
:href="urlForNewGive"
|
||||
>
|
||||
<fa icon="file-lines" />
|
||||
<font-awesome icon="file-lines" />
|
||||
Record a Give Similar to the Original
|
||||
</a>
|
||||
</div>
|
||||
@@ -394,38 +423,55 @@
|
||||
<div v-else-if="!isLoading">This does not have details to confirm.</div>
|
||||
|
||||
<div
|
||||
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||
v-if="isLoading"
|
||||
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||
>
|
||||
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { AxiosError } from "axios";
|
||||
import * as yaml from "js-yaml";
|
||||
import * as R from "ramda";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import { displayAmount, GiveSummaryRecord } from "../libs/endorserServer";
|
||||
import { GenericVerifiableCredential, GiveSummaryRecord } from "../interfaces";
|
||||
import { displayAmount } from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { isGiveAction, retrieveAccountDids } from "../libs/util";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
/**
|
||||
* ConfirmGiftView Component
|
||||
*
|
||||
* Displays details about a gift claim and allows users to confirm it if eligible.
|
||||
* Shows gift details including giver, recipient, amount, description, and confirmation status.
|
||||
* Handles visibility of hidden DIDs and provides access to detailed claim information.
|
||||
*
|
||||
* Key features:
|
||||
* - Gift confirmation workflow
|
||||
* - Detailed gift information display
|
||||
* - Confirmation status tracking
|
||||
* - Hidden DID handling
|
||||
* - Claim details expansion
|
||||
*/
|
||||
@Component({
|
||||
methods: { displayAmount },
|
||||
components: { TopMessage, QuickNav },
|
||||
components: {
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
},
|
||||
})
|
||||
export default class ClaimView extends Vue {
|
||||
export default class ConfirmGiftView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
allMyDids: Array<string> = [];
|
||||
@@ -447,102 +493,104 @@ export default class ClaimView extends Vue {
|
||||
urlForNewGive = "";
|
||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||
veriClaimDump = "";
|
||||
veriClaimDidsVisible = {};
|
||||
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
||||
windowLocation = window.location.href;
|
||||
|
||||
R = R;
|
||||
yaml = yaml;
|
||||
libsUtil = libsUtil;
|
||||
serverUtil = serverUtil;
|
||||
displayAmount = displayAmount;
|
||||
|
||||
resetThisValues() {
|
||||
this.confirmerIdList = [];
|
||||
this.confsVisibleErrorMessage = "";
|
||||
this.confsVisibleToIdList = [];
|
||||
this.giveDetails = undefined;
|
||||
this.isRegistered = false;
|
||||
this.numConfsNotVisible = 0;
|
||||
this.urlForNewGive = "";
|
||||
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||
this.veriClaimDump = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the view with gift claim information
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Retrieves active account settings
|
||||
* 2. Loads gift claim details from ID in URL
|
||||
* 3. Processes claim information for display
|
||||
* 4. Checks user's ability to confirm the gift
|
||||
*/
|
||||
async mounted() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
await this.initializeSettings();
|
||||
await this.loadClaimFromUrl();
|
||||
} catch (error) {
|
||||
logger.error("Error in mounted:", error);
|
||||
this.handleMountError(error);
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes component settings and user data
|
||||
*/
|
||||
private async initializeSettings() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
this.isRegistered = settings.isRegistered || false;
|
||||
|
||||
this.allMyDids = await retrieveAccountDids();
|
||||
|
||||
const pathParam = window.location.pathname.substring(
|
||||
"/confirm-gift/".length,
|
||||
);
|
||||
let claimId;
|
||||
if (pathParam) {
|
||||
claimId = decodeURIComponent(pathParam);
|
||||
await this.loadClaim(claimId, this.activeDid);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "No claim ID was provided.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
|
||||
// Check share capability
|
||||
// When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
|
||||
// then use this truer check: navigator.canShare && navigator.canShare()
|
||||
this.canShare = !!navigator.share;
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
// insert a space before any capital letters except the initial letter
|
||||
// (and capitalize initial letter, just in case)
|
||||
capitalizeAndInsertSpacesBeforeCaps(text: string) {
|
||||
return !text
|
||||
? ""
|
||||
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||
/**
|
||||
* Loads and processes claim from URL parameters
|
||||
*/
|
||||
private async loadClaimFromUrl() {
|
||||
const pathParam = window.location.pathname.substring(
|
||||
"/confirm-gift/".length,
|
||||
);
|
||||
if (!pathParam) {
|
||||
throw new Error("No claim ID was provided.");
|
||||
}
|
||||
|
||||
const claimId = decodeURIComponent(pathParam);
|
||||
await this.loadClaim(claimId, this.activeDid);
|
||||
}
|
||||
|
||||
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string) {
|
||||
const word = this.capitalizeAndInsertSpacesBeforeCaps(text);
|
||||
if (word) {
|
||||
// if the word starts with a vowel, use "an" instead of "a"
|
||||
const firstLetter = word[0].toLowerCase();
|
||||
const vowels = ["a", "e", "i", "o", "u"];
|
||||
const particle = vowels.includes(firstLetter) ? "an" : "a";
|
||||
return particle + " " + word;
|
||||
} else {
|
||||
return "";
|
||||
/**
|
||||
* Handles errors during component mounting
|
||||
*/
|
||||
private handleMountError(error: unknown) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
error instanceof Error ? error.message : "No claim ID was provided.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads claim details and associated give information
|
||||
*
|
||||
* @param claimId - ID of claim to load
|
||||
* @param userDid - User's DID
|
||||
*/
|
||||
private async loadClaim(claimId: string, userDid: string) {
|
||||
await this.fetchClaimDetails(claimId, userDid);
|
||||
if (this.veriClaim.claimType === "GiveAction") {
|
||||
await this.fetchGiveDetails(claimId, userDid);
|
||||
await this.processGiveDetails();
|
||||
await this.fetchConfirmerInfo(claimId, userDid);
|
||||
}
|
||||
}
|
||||
|
||||
totalConfirmers() {
|
||||
return (
|
||||
this.numConfsNotVisible +
|
||||
this.confirmerIdList.length +
|
||||
this.confsVisibleToIdList.length
|
||||
);
|
||||
}
|
||||
|
||||
// Isn't there a better way to make this available to the template?
|
||||
didInfo(did: string | undefined) {
|
||||
return serverUtil.didInfo(
|
||||
did,
|
||||
this.activeDid,
|
||||
this.allMyDids,
|
||||
this.allContacts,
|
||||
);
|
||||
}
|
||||
|
||||
async loadClaim(claimId: string, userDid: string) {
|
||||
/**
|
||||
* Fetches basic claim details from server
|
||||
*/
|
||||
private async fetchClaimDetails(claimId: string, userDid: string) {
|
||||
const urlPath = libsUtil.isGlobalUri(claimId)
|
||||
? "/api/claim/byHandle/"
|
||||
: "/api/claim/";
|
||||
@@ -551,9 +599,7 @@ export default class ClaimView extends Vue {
|
||||
try {
|
||||
const headers = await serverUtil.getHeaders(userDid);
|
||||
const resp = await this.axios.get(url, { headers });
|
||||
// resp.data is:
|
||||
// - a Jwt from https://api.endorser.ch/api-docs/
|
||||
// - with a Give from https://endorser.ch/doc/html/transactions.html#id3
|
||||
|
||||
if (resp.status === 200) {
|
||||
this.veriClaim = resp.data;
|
||||
this.veriClaimDump = yaml.dump(this.veriClaim);
|
||||
@@ -561,138 +607,180 @@ export default class ClaimView extends Vue {
|
||||
this.veriClaim,
|
||||
true,
|
||||
);
|
||||
this.issuerName = this.didInfo(this.veriClaim.issuer);
|
||||
} else {
|
||||
// actually, axios typically throws an error so we never get here
|
||||
console.error("Error getting claim:", resp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was a problem retrieving that claim.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
return;
|
||||
throw new Error("Error getting claim: " + resp.status);
|
||||
}
|
||||
|
||||
// retrieve more details on Give, Offer, or Plan
|
||||
if (this.veriClaim.claimType !== "GiveAction") {
|
||||
// no need to go further... this page is for gifts
|
||||
return;
|
||||
}
|
||||
|
||||
this.issuerName = this.didInfo(this.veriClaim.issuer);
|
||||
|
||||
// use give record when possible since it may include edits
|
||||
const giveUrl =
|
||||
this.apiServer +
|
||||
"/api/v2/report/gives?handleId=" +
|
||||
encodeURIComponent(this.veriClaim.handleId as string);
|
||||
const giveHeaders = await serverUtil.getHeaders(userDid);
|
||||
const giveResp = await this.axios.get(giveUrl, {
|
||||
headers: giveHeaders,
|
||||
});
|
||||
// giveResp.data is a Give from https://api.endorser.ch/api-docs/
|
||||
if (giveResp.status === 200) {
|
||||
this.giveDetails = giveResp.data.data[0];
|
||||
} else {
|
||||
console.error("Error getting detailed give info:", giveResp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Something went wrong retrieving gift data.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// the logic already stops earlier if the claim doesn't exist but this helps with typechecking
|
||||
if (!this.giveDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.urlForNewGive = "/gifted-details?";
|
||||
if (this.giveDetails.amount) {
|
||||
this.urlForNewGive +=
|
||||
"&amountInput=" + encodeURIComponent(String(this.giveDetails.amount));
|
||||
}
|
||||
if (this.giveDetails.unit) {
|
||||
this.urlForNewGive +=
|
||||
"&unitCode=" + encodeURIComponent(this.giveDetails.unit);
|
||||
}
|
||||
if (this.giveDetails.description) {
|
||||
this.urlForNewGive +=
|
||||
"&description=" + encodeURIComponent(this.giveDetails.description);
|
||||
}
|
||||
this.giverName = this.didInfo(this.giveDetails.agentDid);
|
||||
if (this.giveDetails.agentDid) {
|
||||
this.urlForNewGive +=
|
||||
"&giverDid=" +
|
||||
encodeURIComponent(this.giveDetails.agentDid) +
|
||||
"&giverName=" +
|
||||
encodeURIComponent(this.giverName);
|
||||
}
|
||||
this.recipientName = this.didInfo(this.giveDetails.recipientDid);
|
||||
if (this.giveDetails.recipientDid) {
|
||||
this.urlForNewGive +=
|
||||
"&recipientDid=" +
|
||||
encodeURIComponent(this.giveDetails.recipientDid) +
|
||||
"&recipientName=" +
|
||||
encodeURIComponent(this.recipientName);
|
||||
}
|
||||
if (this.giveDetails.fullClaim.image) {
|
||||
this.urlForNewGive +=
|
||||
"&image=" + encodeURIComponent(this.giveDetails.fullClaim.image);
|
||||
}
|
||||
if (
|
||||
this.giveDetails.type == "Offer" &&
|
||||
this.giveDetails.fulfillsHandleId
|
||||
) {
|
||||
this.urlForNewGive +=
|
||||
"&offerId=" +
|
||||
encodeURIComponent(this.giveDetails?.fulfillsHandleId as string);
|
||||
}
|
||||
if (this.giveDetails.fulfillsPlanHandleId) {
|
||||
this.urlForNewGive +=
|
||||
"&fulfillsProjectId=" +
|
||||
encodeURIComponent(this.giveDetails.fulfillsPlanHandleId);
|
||||
}
|
||||
|
||||
// retrieve the list of confirmers
|
||||
const confirmerInfo = await libsUtil.retrieveConfirmerIdList(
|
||||
this.apiServer,
|
||||
claimId,
|
||||
this.veriClaim.issuer,
|
||||
userDid,
|
||||
);
|
||||
if (confirmerInfo) {
|
||||
this.confirmerIdList = confirmerInfo.confirmerIdList;
|
||||
this.confsVisibleToIdList = confirmerInfo.confsVisibleToIdList;
|
||||
this.numConfsNotVisible = confirmerInfo.numConfsNotVisible;
|
||||
} else {
|
||||
this.confsVisibleErrorMessage =
|
||||
"Had problems retrieving confirmations.";
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const serverError = error as AxiosError;
|
||||
console.error("Error retrieving claim:", serverError);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Something went wrong retrieving claim data.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error getting claim:", error);
|
||||
throw new Error("There was a problem retrieving that claim.");
|
||||
}
|
||||
}
|
||||
|
||||
confirmConfirmClaim() {
|
||||
/**
|
||||
* Fetches detailed give information
|
||||
*/
|
||||
private async fetchGiveDetails(claimId: string, userDid: string) {
|
||||
const param = libsUtil.isGlobalUri(claimId) ? "handleId" : "jwtId";
|
||||
const giveUrl = `${this.apiServer}/api/v2/report/gives?${param}=${encodeURIComponent(claimId)}`;
|
||||
|
||||
try {
|
||||
const headers = await serverUtil.getHeaders(userDid);
|
||||
const resp = await this.axios.get(giveUrl, { headers });
|
||||
|
||||
if (resp.status === 200) {
|
||||
this.giveDetails = resp.data.data[0];
|
||||
} else {
|
||||
throw new Error("Error getting detailed give info: " + resp.status);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error getting detailed give info:", error);
|
||||
throw new Error("Something went wrong retrieving gift data.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes give details and builds URL for new give
|
||||
*/
|
||||
private async processGiveDetails() {
|
||||
if (!this.giveDetails) return;
|
||||
|
||||
this.urlForNewGive = "/gifted-details?";
|
||||
|
||||
/**
|
||||
* Add basic give details to URL
|
||||
*/
|
||||
if (this.giveDetails?.amount) {
|
||||
this.urlForNewGive += `&amountInput=${encodeURIComponent(String(this.giveDetails.amount))}`;
|
||||
}
|
||||
if (this.giveDetails?.unit) {
|
||||
this.urlForNewGive += `&unitCode=${encodeURIComponent(this.giveDetails.unit)}`;
|
||||
}
|
||||
if (this.giveDetails?.description) {
|
||||
this.urlForNewGive += `&description=${encodeURIComponent(this.giveDetails.description)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add participant (giver/recipient) name & URL info
|
||||
*/
|
||||
if (this.giveDetails?.agentDid) {
|
||||
this.giverName = this.didInfo(this.giveDetails.agentDid);
|
||||
this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`;
|
||||
}
|
||||
if (this.giveDetails?.recipientDid) {
|
||||
this.recipientName = this.didInfo(this.giveDetails.recipientDid);
|
||||
this.urlForNewGive += `&recipientDid=${encodeURIComponent(this.giveDetails.recipientDid)}&recipientName=${encodeURIComponent(this.recipientName)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add additional give details to URL (image, offer, plan)
|
||||
*/
|
||||
if (this.giveDetails?.fullClaim.image) {
|
||||
this.urlForNewGive += `&image=${encodeURIComponent(this.giveDetails.fullClaim.image)}`;
|
||||
}
|
||||
if (
|
||||
this.giveDetails?.type === "Offer" &&
|
||||
this.giveDetails?.fulfillsHandleId
|
||||
) {
|
||||
this.urlForNewGive += `&offerId=${encodeURIComponent(this.giveDetails.fulfillsHandleId)}`;
|
||||
}
|
||||
if (this.giveDetails?.fulfillsPlanHandleId) {
|
||||
this.urlForNewGive += `&fulfillsProjectId=${encodeURIComponent(this.giveDetails.fulfillsPlanHandleId)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches confirmer information for the claim
|
||||
*/
|
||||
private async fetchConfirmerInfo(claimId: string, userDid: string) {
|
||||
const confirmerInfo = await libsUtil.retrieveConfirmerIdList(
|
||||
this.apiServer,
|
||||
claimId,
|
||||
this.veriClaim.issuer,
|
||||
userDid,
|
||||
);
|
||||
|
||||
if (confirmerInfo) {
|
||||
this.confirmerIdList = confirmerInfo.confirmerIdList;
|
||||
this.confsVisibleToIdList = confirmerInfo.confsVisibleToIdList;
|
||||
this.numConfsNotVisible = confirmerInfo.numConfsNotVisible;
|
||||
} else {
|
||||
this.confsVisibleErrorMessage = "Had problems retrieving confirmations.";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates total number of confirmers for the gift
|
||||
* Includes both direct confirmers and those visible through network
|
||||
*
|
||||
* @returns Total number of confirmers
|
||||
*/
|
||||
totalConfirmers(): number {
|
||||
return (
|
||||
this.numConfsNotVisible +
|
||||
this.confirmerIdList.length +
|
||||
this.confsVisibleToIdList.length
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves human-readable name for a DID
|
||||
* Falls back to DID if no name available
|
||||
*
|
||||
* @param did - DID to get name for
|
||||
* @returns Human-readable name
|
||||
*/
|
||||
didInfo(did: string): string {
|
||||
return serverUtil.didInfo(
|
||||
did,
|
||||
this.activeDid,
|
||||
this.allMyDids,
|
||||
this.allContacts,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies text to clipboard and shows notification
|
||||
*
|
||||
* @param description - Description of copied content
|
||||
* @param text - Text to copy
|
||||
*/
|
||||
copyToClipboard(description: string, text: string): void {
|
||||
useClipboard()
|
||||
.copy(text)
|
||||
.then(() => {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
title: "Copied",
|
||||
text: (description || "That") + " was copied to the clipboard.",
|
||||
},
|
||||
2000,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to claim page for detailed view
|
||||
*
|
||||
* @param claimId - ID of claim to view
|
||||
*/
|
||||
showClaimPage(claimId: string): void {
|
||||
const route = {
|
||||
path: "/claim/" + encodeURIComponent(claimId),
|
||||
};
|
||||
(this.$router as Router).push(route).then(async () => {
|
||||
this.resetThisValues();
|
||||
await this.loadClaim(claimId, this.activeDid);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates claim confirmation process
|
||||
* Verifies user eligibility and handles confirmation workflow
|
||||
*/
|
||||
async confirmConfirmClaim(): Promise<void> {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
@@ -719,7 +807,7 @@ export default class ClaimView extends Vue {
|
||||
),
|
||||
),
|
||||
);
|
||||
const confirmationClaim: serverUtil.GenericVerifiableCredential = {
|
||||
const confirmationClaim: GenericVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "AgreeAction",
|
||||
object: goodClaim,
|
||||
@@ -754,33 +842,11 @@ export default class ClaimView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
showClaimPage(claimId: string) {
|
||||
const route = {
|
||||
path: "/claim/" + encodeURIComponent(claimId),
|
||||
};
|
||||
(this.$router as Router).push(route).then(async () => {
|
||||
this.resetThisValues();
|
||||
await this.loadClaim(claimId, this.activeDid);
|
||||
});
|
||||
}
|
||||
|
||||
copyToClipboard(name: string, text: string) {
|
||||
useClipboard()
|
||||
.copy(text)
|
||||
.then(() => {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
title: "Copied",
|
||||
text: (name || "That") + " was copied to the clipboard.",
|
||||
},
|
||||
2000,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
notifyWhyCannotConfirm() {
|
||||
/**
|
||||
* Notifies user why they cannot confirm the gift
|
||||
* Explains requirements or restrictions preventing confirmation
|
||||
*/
|
||||
notifyWhyCannotConfirm(): void {
|
||||
libsUtil.notifyWhyCannotConfirm(
|
||||
this.$notify,
|
||||
this.isRegistered,
|
||||
@@ -791,71 +857,32 @@ export default class ClaimView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
notifyWhyCannotConfirmBak() {
|
||||
if (!this.isRegistered) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Not Registered",
|
||||
text: "Someone needs to register you before you can contribute.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
} else if (!isGiveAction(this.veriClaim)) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Not A Give",
|
||||
text: "This is not a giving action to confirm.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
} else if (this.confirmerIdList.includes(this.activeDid)) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Already Confirmed",
|
||||
text: "You already confirmed this claim.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
} else if (this.giveDetails?.issuerDid == this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Cannot Confirm",
|
||||
text: "You cannot confirm this because you issued this claim.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
} else if (serverUtil.containsHiddenDid(this.giveDetails?.fullClaim)) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Cannot Confirm",
|
||||
text: "You cannot confirm this because some people are hidden.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
/**
|
||||
* Formats type string for display by adding spaces before capitals
|
||||
* Optionally adds a prefix
|
||||
*
|
||||
* @param text - Text to format
|
||||
* @param prefix - Optional prefix to add
|
||||
* @returns Formatted string
|
||||
*/
|
||||
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string): string {
|
||||
const word = this.capitalizeAndInsertSpacesBeforeCaps(text);
|
||||
if (word) {
|
||||
// if the word starts with a vowel, use "an" instead of "a"
|
||||
const firstLetter = word[0].toLowerCase();
|
||||
const vowels = ["a", "e", "i", "o", "u"];
|
||||
const particle = vowels.includes(firstLetter) ? "an" : "a";
|
||||
return particle + " " + word;
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "info",
|
||||
title: "Cannot Confirm",
|
||||
text: "You cannot confirm this claim. There are no other details, but we can help more if you contact us and send us screenshots.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
onClickShareClaim() {
|
||||
/**
|
||||
* Initiates sharing of claim information
|
||||
* Handles share functionality based on platform capabilities
|
||||
*/
|
||||
async onClickShareClaim(): Promise<void> {
|
||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
||||
window.navigator.share({
|
||||
title: "Help Connect Me",
|
||||
@@ -863,5 +890,23 @@ export default class ClaimView extends Vue {
|
||||
url: this.windowLocation,
|
||||
});
|
||||
}
|
||||
|
||||
resetThisValues() {
|
||||
this.confirmerIdList = [];
|
||||
this.confsVisibleErrorMessage = "";
|
||||
this.confsVisibleToIdList = [];
|
||||
this.giveDetails = undefined;
|
||||
this.isRegistered = false;
|
||||
this.numConfsNotVisible = 0;
|
||||
this.urlForNewGive = "";
|
||||
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||
this.veriClaimDump = "";
|
||||
}
|
||||
|
||||
capitalizeAndInsertSpacesBeforeCaps(text: string) {
|
||||
return !text
|
||||
? ""
|
||||
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
<router-link
|
||||
:to="{ name: 'contacts' }"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
><fa icon="chevron-left" class="fa-fw"></fa
|
||||
></router-link>
|
||||
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</router-link>
|
||||
</h1>
|
||||
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
|
||||
@@ -59,10 +59,13 @@
|
||||
<div class="font-bold">
|
||||
{{ displayAmount(record.unit, record.amount) }}
|
||||
<span v-if="record.amountConfirmed" title="Confirmed">
|
||||
<fa icon="circle-check" class="text-green-600 fa-fw" />
|
||||
<font-awesome
|
||||
icon="circle-check"
|
||||
class="text-green-600 fa-fw"
|
||||
/>
|
||||
</span>
|
||||
<button v-else @click="confirm(record)" title="Unconfirmed">
|
||||
<fa icon="circle" class="text-blue-600 fa-fw" />
|
||||
<button v-else title="Unconfirmed" @click="confirm(record)">
|
||||
<font-awesome icon="circle" class="text-blue-600 fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="italic text-xs sm:text-sm text-slate-500">
|
||||
@@ -72,10 +75,10 @@
|
||||
</td>
|
||||
<td class="p-1">
|
||||
<span v-if="record.agentDid == contact?.did">
|
||||
<fa icon="arrow-left" class="text-slate-400 fa-fw" />
|
||||
<font-awesome icon="arrow-left" class="text-slate-400 fa-fw" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<fa icon="arrow-right" class="text-slate-400 fa-fw" />
|
||||
<font-awesome icon="arrow-right" class="text-slate-400 fa-fw" />
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-1">
|
||||
@@ -83,14 +86,17 @@
|
||||
<div class="font-bold">
|
||||
{{ displayAmount(record.unit, record.amount) }}
|
||||
<span v-if="record.amountConfirmed" title="Confirmed">
|
||||
<fa icon="circle-check" class="text-green-600 fa-fw" />
|
||||
<font-awesome
|
||||
icon="circle-check"
|
||||
class="text-green-600 fa-fw"
|
||||
/>
|
||||
</span>
|
||||
<button
|
||||
v-else
|
||||
@click="cannotConfirmMessage()"
|
||||
title="Unconfirmed"
|
||||
@click="cannotConfirmMessage()"
|
||||
>
|
||||
<fa icon="circle" class="text-slate-600 fa-fw" />
|
||||
<font-awesome icon="circle" class="text-slate-600 fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="italic text-xs sm:text-sm text-slate-500">
|
||||
@@ -108,7 +114,7 @@
|
||||
import { AxiosError, AxiosRequestHeaders } from "axios";
|
||||
import * as R from "ramda";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
@@ -116,18 +122,22 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import {
|
||||
AgreeVerifiableCredential,
|
||||
GiveSummaryRecord,
|
||||
GiveVerifiableCredential,
|
||||
} from "../interfaces";
|
||||
import {
|
||||
createEndorserJwtVcFromClaim,
|
||||
displayAmount,
|
||||
getHeaders,
|
||||
GiveSummaryRecord,
|
||||
GiveVerifiableCredential,
|
||||
SCHEMA_ORG_CONTEXT,
|
||||
} from "../libs/endorserServer";
|
||||
import { retrieveAccountCount } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class ContactAmountssView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
@@ -143,7 +153,7 @@ export default class ContactAmountssView extends Vue {
|
||||
|
||||
async created() {
|
||||
try {
|
||||
const contactDid = (this.$route as Router).query["contactDid"] as string;
|
||||
const contactDid = this.$route.query["contactDid"] as string;
|
||||
this.contact = (await db.contacts.get(contactDid)) || null;
|
||||
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
@@ -155,7 +165,7 @@ export default class ContactAmountssView extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error retrieving settings or gives.", err);
|
||||
logger.error("Error retrieving settings or gives.", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -184,7 +194,7 @@ export default class ContactAmountssView extends Vue {
|
||||
if (resp.status === 200) {
|
||||
result = resp.data.data;
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Got bad response status & data of",
|
||||
resp.status,
|
||||
resp.data,
|
||||
@@ -211,7 +221,7 @@ export default class ContactAmountssView extends Vue {
|
||||
if (resp2.status === 200) {
|
||||
result = R.concat(result, resp2.data.data);
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Got bad response status & data of",
|
||||
resp2.status,
|
||||
resp2.data,
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
<h1 class="text-4xl text-center font-light relative px-7">
|
||||
<!-- Back -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw" />
|
||||
<font-awesome icon="chevron-left" class="fa-fw" />
|
||||
</button>
|
||||
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
||||
</h1>
|
||||
@@ -25,9 +25,9 @@
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
v-model="contactName"
|
||||
type="text"
|
||||
class="block w-full ml-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
v-model="contactName"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -38,9 +38,9 @@
|
||||
</label>
|
||||
<textarea
|
||||
id="contactNotes"
|
||||
v-model="contactNotes"
|
||||
rows="4"
|
||||
class="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
v-model="contactNotes"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@@ -53,60 +53,60 @@
|
||||
class="flex mt-2"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="method.label"
|
||||
type="text"
|
||||
class="block w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="Label"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
v-model="method.type"
|
||||
type="text"
|
||||
class="block ml-2 w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="Type"
|
||||
/>
|
||||
<div class="relative">
|
||||
<button
|
||||
@click="toggleDropdown(index)"
|
||||
class="px-2 py-1 bg-gray-200 rounded-md"
|
||||
@click="toggleDropdown(index)"
|
||||
>
|
||||
<fa icon="caret-down" class="fa-fw" />
|
||||
<font-awesome icon="caret-down" class="fa-fw" />
|
||||
</button>
|
||||
<div
|
||||
v-if="dropdownIndex === index"
|
||||
class="absolute bg-white border border-gray-300 rounded-md mt-1"
|
||||
>
|
||||
<div
|
||||
@click="setMethodType(index, 'CELL')"
|
||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
||||
@click="setMethodType(index, 'CELL')"
|
||||
>
|
||||
CELL
|
||||
</div>
|
||||
<div
|
||||
@click="setMethodType(index, 'EMAIL')"
|
||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
||||
@click="setMethodType(index, 'EMAIL')"
|
||||
>
|
||||
EMAIL
|
||||
</div>
|
||||
<div
|
||||
@click="setMethodType(index, 'WHATSAPP')"
|
||||
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
|
||||
@click="setMethodType(index, 'WHATSAPP')"
|
||||
>
|
||||
WHATSAPP
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
v-model="method.value"
|
||||
type="text"
|
||||
class="block ml-2 w-1/2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
placeholder="Number, email, etc."
|
||||
/>
|
||||
<button @click="removeContactMethod(index)" class="ml-2 text-red-500">
|
||||
<fa icon="trash-can" class="fa-fw" />
|
||||
<button class="ml-2 text-red-500" @click="removeContactMethod(index)">
|
||||
<font-awesome icon="trash-can" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
<button @click="addContactMethod" class="mt-2">
|
||||
<fa
|
||||
<button class="mt-2" @click="addContactMethod">
|
||||
<font-awesome
|
||||
icon="plus"
|
||||
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
|
||||
/>
|
||||
@@ -134,7 +134,7 @@
|
||||
<script lang="ts">
|
||||
import * as R from "ramda";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocation, Router } from "vue-router";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
@@ -142,6 +142,37 @@ import { AppString, NotificationIface } from "../constants/app";
|
||||
import { db } from "../db/index";
|
||||
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||
|
||||
/**
|
||||
* Contact Edit View Component
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This component provides a full-featured contact editing interface with support for:
|
||||
* - Basic contact information (name, notes)
|
||||
* - Multiple contact methods with type selection
|
||||
* - Data validation and persistence
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Component loads with DID from route params
|
||||
* 2. Fetches existing contact data from IndexedDB
|
||||
* 3. Presents editable form with current values
|
||||
* 4. Validates and saves updates back to database
|
||||
*
|
||||
* Contact Method Types:
|
||||
* - CELL: Mobile phone numbers
|
||||
* - EMAIL: Email addresses
|
||||
* - WHATSAPP: WhatsApp contact info
|
||||
*
|
||||
* State Management:
|
||||
* - Maintains separate state for form fields to prevent direct mutation
|
||||
* - Handles array cloning for contact methods to prevent reference issues
|
||||
* - Manages dropdown state for method type selection
|
||||
*
|
||||
* Navigation:
|
||||
* - Back button returns to previous view
|
||||
* - Save redirects to contact detail view
|
||||
* - Cancel returns to previous view
|
||||
* - Invalid DID redirects to contacts list
|
||||
*/
|
||||
@Component({
|
||||
components: {
|
||||
QuickNav,
|
||||
@@ -149,22 +180,46 @@ import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||
},
|
||||
})
|
||||
export default class ContactEditView extends Vue {
|
||||
/** Notification function injected by Vue */
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
/** Current route instance */
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
/** Router instance for navigation */
|
||||
$router!: Router;
|
||||
|
||||
/** Current contact data */
|
||||
contact: Contact = {
|
||||
did: "",
|
||||
name: "",
|
||||
notes: "",
|
||||
};
|
||||
/** Editable contact name field */
|
||||
contactName = "";
|
||||
/** Editable contact notes field */
|
||||
contactNotes = "";
|
||||
/** Array of editable contact methods */
|
||||
contactMethods: Array<ContactMethod> = [];
|
||||
/** Currently open dropdown index, null if none open */
|
||||
dropdownIndex: number | null = null;
|
||||
|
||||
/** App string constants */
|
||||
AppString = AppString;
|
||||
|
||||
/**
|
||||
* Component lifecycle hook that initializes the contact edit form
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Extracts DID from route parameters
|
||||
* 2. Queries database for existing contact
|
||||
* 3. Populates form fields with contact data
|
||||
* 4. Handles missing contact error case
|
||||
*
|
||||
* @throws Will not throw but redirects on error
|
||||
* @emits Notification on contact not found
|
||||
* @emits Router navigation on error
|
||||
*/
|
||||
async created() {
|
||||
const contactDid = (this.$route as RouteLocation).params.did;
|
||||
const contactDid = this.$route.params.did;
|
||||
const contact = await db.contacts.get(contactDid || "");
|
||||
if (contact) {
|
||||
this.contact = contact;
|
||||
@@ -183,29 +238,75 @@ export default class ContactEditView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new empty contact method to the methods array
|
||||
*
|
||||
* Creates a new method object with empty fields for:
|
||||
* - label: Custom label for the method
|
||||
* - type: Communication type (CELL, EMAIL, WHATSAPP)
|
||||
* - value: The contact information value
|
||||
*/
|
||||
addContactMethod() {
|
||||
this.contactMethods.push({ label: "", type: "", value: "" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a contact method at the specified index
|
||||
*
|
||||
* @param index The array index of the method to remove
|
||||
*/
|
||||
removeContactMethod(index: number) {
|
||||
this.contactMethods.splice(index, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the type selection dropdown for a contact method
|
||||
*
|
||||
* If the clicked dropdown is already open, closes it.
|
||||
* If another dropdown is open, closes it and opens the clicked one.
|
||||
*
|
||||
* @param index The array index of the method whose dropdown to toggle
|
||||
*/
|
||||
toggleDropdown(index: number) {
|
||||
this.dropdownIndex = this.dropdownIndex === index ? null : index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type for a contact method and closes the dropdown
|
||||
*
|
||||
* @param index The array index of the method to update
|
||||
* @param type The new type value (CELL, EMAIL, WHATSAPP)
|
||||
*/
|
||||
setMethodType(index: number, type: string) {
|
||||
this.contactMethods[index].type = type;
|
||||
this.dropdownIndex = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the edited contact information to the database
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Clones contact methods array to prevent reference issues
|
||||
* 2. Normalizes method types to uppercase
|
||||
* 3. Checks for changes in method types
|
||||
* 4. Updates database with new values
|
||||
* 5. Notifies user of success
|
||||
* 6. Redirects to contact detail view
|
||||
*
|
||||
* @throws Will not throw but notifies on validation errors
|
||||
* @emits Notification on type changes or success
|
||||
* @emits Router navigation on success
|
||||
*/
|
||||
async saveEdit() {
|
||||
// without this conversion, "Failed to execute 'put' on 'IDBObjectStore': [object Array] could not be cloned."
|
||||
const contactMethodsObj = JSON.parse(JSON.stringify(this.contactMethods));
|
||||
|
||||
// Normalize method types to uppercase
|
||||
const contactMethods = contactMethodsObj.map((method: ContactMethod) =>
|
||||
R.set(R.lensProp("type"), method.type.toUpperCase(), method),
|
||||
);
|
||||
|
||||
// Check for type changes
|
||||
if (!R.equals(contactMethodsObj, contactMethods)) {
|
||||
this.contactMethods = contactMethods;
|
||||
this.$notify(
|
||||
@@ -219,11 +320,15 @@ export default class ContactEditView extends Vue {
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to database
|
||||
await db.contacts.update(this.contact.did, {
|
||||
name: this.contactName,
|
||||
notes: this.contactNotes,
|
||||
contactMethods: contactMethods,
|
||||
});
|
||||
|
||||
// Notify success and redirect
|
||||
this.$notify({
|
||||
group: "alert",
|
||||
type: "success",
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<router-link
|
||||
:to="{ name: 'home' }"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
><fa icon="chevron-left" class="fa-fw"></fa
|
||||
></router-link>
|
||||
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</router-link>
|
||||
Given by...
|
||||
</h1>
|
||||
</div>
|
||||
@@ -30,10 +30,10 @@
|
||||
<span class="text-right">
|
||||
<button
|
||||
type="button"
|
||||
@click="openDialog()"
|
||||
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
@click="openDialog()"
|
||||
>
|
||||
<fa icon="gift" class="fa-fw"></fa>
|
||||
<font-awesome icon="gift" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
</span>
|
||||
</h2>
|
||||
@@ -47,7 +47,7 @@
|
||||
<span class="grow font-semibold">
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:iconSize="32"
|
||||
:icon-size="32"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
/>
|
||||
{{ contact.name || "(no name)" }}
|
||||
@@ -55,23 +55,23 @@
|
||||
<span class="text-right">
|
||||
<button
|
||||
type="button"
|
||||
@click="openDialog(contact)"
|
||||
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||
@click="openDialog(contact)"
|
||||
>
|
||||
<fa icon="gift" class="fa-fw"></fa>
|
||||
<font-awesome icon="gift" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
</span>
|
||||
</h2>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<GiftedDialog ref="customDialog" :toProjectId="projectId" />
|
||||
<GiftedDialog ref="customDialog" :to-project-id="projectId" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
@@ -80,12 +80,14 @@ import { NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { GiverReceiverInputInfo } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
||||
})
|
||||
export default class ContactGiftingView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
@@ -107,13 +109,12 @@ export default class ContactGiftingView extends Vue {
|
||||
(a.name || "").localeCompare(b.name || ""),
|
||||
);
|
||||
|
||||
this.projectId = (this.$route as Router).query["projectId"] || "";
|
||||
|
||||
this.prompt = (this.$route as Router).query["prompt"] ?? this.prompt;
|
||||
this.projectId = (this.$route.query["projectId"] as string) || "";
|
||||
this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error retrieving settings & contacts:", err);
|
||||
logger.error("Error retrieving settings & contacts:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -17,14 +17,14 @@
|
||||
</h1>
|
||||
|
||||
<div v-if="checkingImports" class="text-center">
|
||||
<fa icon="spinner" class="animate-spin" />
|
||||
<font-awesome icon="spinner" class="animate-spin" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<span
|
||||
v-if="contactsImporting.length > sameCount"
|
||||
class="flex justify-center"
|
||||
>
|
||||
<input type="checkbox" v-model="makeVisible" class="mr-2" />
|
||||
<input v-model="makeVisible" type="checkbox" class="mr-2" />
|
||||
Make my activity visible to these contacts.
|
||||
</span>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4"
|
||||
>
|
||||
<h2 class="text-base font-semibold">
|
||||
<input type="checkbox" v-model="contactsSelected[index]" />
|
||||
<input v-model="contactsSelected[index]" type="checkbox" />
|
||||
{{ contact.name || AppString.NO_CONTACT_NAME }}
|
||||
-
|
||||
<span v-if="contactsExisting[contact.did]" class="text-orange-500"
|
||||
@@ -110,8 +110,8 @@
|
||||
/>
|
||||
<br />
|
||||
<button
|
||||
@click="() => processContactJwt(inputJwt)"
|
||||
class="ml-2 p-2 bg-blue-500 text-white rounded"
|
||||
@click="() => processContactJwt(inputJwt)"
|
||||
>
|
||||
Check Import
|
||||
</button>
|
||||
@@ -122,6 +122,77 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
/**
|
||||
* @file Contact Import View Component
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This component handles the import of contacts into the TimeSafari app.
|
||||
* It supports multiple import methods and handles duplicate detection,
|
||||
* contact validation, and visibility settings.
|
||||
*
|
||||
* Import Methods:
|
||||
* 1. Direct URL Query Parameters:
|
||||
* Example: /contact-import?contacts=[{"did":"did:example:123","name":"Alice"}]
|
||||
*
|
||||
* 2. JWT in URL Path:
|
||||
* Example: /contact-import/eyJhbGciOiJFUzI1NksifQ...
|
||||
* - Supports both single and bulk imports
|
||||
* - JWT payload can be either:
|
||||
* a) Array format: { contacts: [{did: "...", name: "..."}, ...] }
|
||||
* b) Single contact: { own: true, did: "...", name: "..." }
|
||||
*
|
||||
* 3. Manual JWT Input:
|
||||
* - Accepts pasted JWT strings
|
||||
* - Validates format and content before processing
|
||||
*
|
||||
* URL Examples:
|
||||
* ```
|
||||
* # Bulk import via query params
|
||||
* /contact-import?contacts=[
|
||||
* {"did":"did:example:123","name":"Alice"},
|
||||
* {"did":"did:example:456","name":"Bob"}
|
||||
* ]
|
||||
*
|
||||
* # Single contact via JWT
|
||||
* /contact-import/eyJhbGciOiJFUzI1NksifQ.eyJvd24iOnRydWUsImRpZCI6ImRpZDpleGFtcGxlOjEyMyJ9...
|
||||
*
|
||||
* # Bulk import via JWT
|
||||
* /contact-import/eyJhbGciOiJFUzI1NksifQ.eyJjb250YWN0cyI6W3siZGlkIjoiZGlkOmV4YW1wbGU6MTIzIn1dfQ...
|
||||
*
|
||||
* # Redirect to contacts page (single contact)
|
||||
* /contacts?contactJwt=eyJhbGciOiJFUzI1NksifQ...
|
||||
* ```
|
||||
*
|
||||
* Features:
|
||||
* - Automatic duplicate detection
|
||||
* - Field-by-field comparison for existing contacts
|
||||
* - Batch visibility settings
|
||||
* - Auto-import for single new contacts
|
||||
* - Error handling and validation
|
||||
*
|
||||
* State Management:
|
||||
* - Tracks existing contacts
|
||||
* - Maintains selection state for bulk imports
|
||||
* - Records differences for duplicate contacts
|
||||
* - Manages visibility settings
|
||||
*
|
||||
* Security Considerations:
|
||||
* - JWT validation for imported contacts
|
||||
* - Visibility control per contact
|
||||
* - Error handling for malformed data
|
||||
*
|
||||
* @example
|
||||
* // Component usage in router
|
||||
* {
|
||||
* path: "/contact-import/:jwt?",
|
||||
* name: "contact-import",
|
||||
* component: ContactImportView
|
||||
* }
|
||||
*
|
||||
* @see {@link Contact} for contact data structure
|
||||
* @see {@link setVisibilityUtil} for visibility management
|
||||
*/
|
||||
|
||||
import * as R from "ramda";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
@@ -145,22 +216,75 @@ import {
|
||||
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||
import { decodeEndorserJwt } from "../libs/crypto/vc";
|
||||
|
||||
/**
|
||||
* Contact Import View Component
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This component handles the secure import of contacts into TimeSafari via JWT tokens.
|
||||
* It supports both single and multiple contact imports with validation and duplicate detection.
|
||||
*
|
||||
* Import Workflows:
|
||||
* 1. JWT in URL Path (/contact-import/[JWT])
|
||||
* - Extracts JWT from path
|
||||
* - Decodes and validates contact data
|
||||
* - Handles both single and multiple contacts
|
||||
*
|
||||
* 2. JWT in Query Parameter (/contacts?contactJwt=[JWT])
|
||||
* - Used for single contact redirects
|
||||
* - Processes JWT from query parameter
|
||||
* - Redirects to appropriate view
|
||||
*
|
||||
* JWT Payload Structure:
|
||||
* ```json
|
||||
* {
|
||||
* "iat": 1740740453,
|
||||
* "contacts": [{
|
||||
* "did": "did:ethr:0x...",
|
||||
* "name": "Optional Name",
|
||||
* "nextPubKeyHashB64": "base64 string",
|
||||
* "publicKeyBase64": "base64 string"
|
||||
* }],
|
||||
* "iss": "did:ethr:0x..."
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Security Features:
|
||||
* - JWT validation
|
||||
* - Issuer verification
|
||||
* - Duplicate detection
|
||||
* - Contact data validation
|
||||
*
|
||||
* @component
|
||||
*/
|
||||
@Component({
|
||||
components: { EntityIcon, OfferDialog, QuickNav },
|
||||
})
|
||||
export default class ContactImportView extends Vue {
|
||||
/** Notification function injected by Vue */
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
/** Current route instance */
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
/** Router instance for navigation */
|
||||
$router!: Router;
|
||||
|
||||
// Constants
|
||||
AppString = AppString;
|
||||
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
|
||||
libsUtil = libsUtil;
|
||||
R = R;
|
||||
|
||||
// Component state
|
||||
/** Active user's DID for authentication and visibility settings */
|
||||
activeDid = "";
|
||||
/** API server URL for backend communication */
|
||||
apiServer = "";
|
||||
contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID
|
||||
contactsImporting: Array<Contact> = []; // contacts from the import
|
||||
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected
|
||||
/** Map of existing contacts keyed by DID for duplicate detection */
|
||||
contactsExisting: Record<string, Contact> = {};
|
||||
/** Array of contacts being imported from JWT */
|
||||
contactsImporting: Array<Contact> = [];
|
||||
/** Selection state for each importing contact */
|
||||
contactsSelected: Array<boolean> = [];
|
||||
/** Differences between existing and importing contacts */
|
||||
contactDifferences: Record<
|
||||
string,
|
||||
Record<
|
||||
@@ -170,69 +294,117 @@ export default class ContactImportView extends Vue {
|
||||
old: string | boolean | Array<ContactMethod> | undefined;
|
||||
}
|
||||
>
|
||||
> = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key
|
||||
> = {};
|
||||
/** Loading state for import operations */
|
||||
checkingImports = false;
|
||||
/** JWT input for manual contact import */
|
||||
inputJwt: string = "";
|
||||
/** Visibility setting for imported contacts */
|
||||
makeVisible = true;
|
||||
/** Count of duplicate contacts found */
|
||||
sameCount = 0;
|
||||
|
||||
/**
|
||||
* Component lifecycle hook that initializes the contact import process
|
||||
*
|
||||
* This method handles three distinct import scenarios:
|
||||
* 1. Query Parameter Import:
|
||||
* - Checks for contacts in URL query parameters
|
||||
* - Parses JSON array of contacts if present
|
||||
*
|
||||
* 2. JWT URL Import:
|
||||
* - Extracts JWT from URL path using regex pattern '/contact-import/(ey.+)$'
|
||||
* - Decodes JWT without validation (supports future-dated QR codes)
|
||||
* - Handles two JWT payload formats:
|
||||
* a. Array format: payload.contacts or direct array
|
||||
* b. Single contact format: redirects to contacts page with JWT
|
||||
*
|
||||
* 3. Auto-Import Logic:
|
||||
* - Automatically imports if exactly one new contact is present
|
||||
* - Only triggers if no existing contacts match
|
||||
*
|
||||
* @throws Will not throw but logs errors during JWT processing
|
||||
* @emits router.push when redirecting for single contact import
|
||||
*/
|
||||
async created() {
|
||||
await this.initializeSettings();
|
||||
await this.processQueryParams();
|
||||
await this.processJwtFromPath();
|
||||
await this.handleAutoImport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes component settings from active account
|
||||
*/
|
||||
private async initializeSettings() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
}
|
||||
|
||||
// look for any imported contact array from the query parameter
|
||||
const importedContacts = (this.$route as RouteLocationNormalizedLoaded)
|
||||
.query["contacts"] as string;
|
||||
/**
|
||||
* Processes contacts from URL query parameters
|
||||
*/
|
||||
private async processQueryParams() {
|
||||
const importedContacts = this.$route.query["contacts"] as string;
|
||||
if (importedContacts) {
|
||||
await this.setContactsSelected(JSON.parse(importedContacts));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes JWT from URL path and handles different JWT formats
|
||||
*/
|
||||
private async processJwtFromPath() {
|
||||
// JWT tokens always start with 'ey' (base64url encoded header)
|
||||
const JWT_PATTERN = /\/contact-import\/(ey.+)$/;
|
||||
const jwt = window.location.pathname.match(JWT_PATTERN)?.[1];
|
||||
|
||||
// look for a JWT after /contact-import/ in the window.location.pathname
|
||||
const jwt = window.location.pathname.match(
|
||||
/\/contact-import\/(ey.+)$/,
|
||||
)?.[1];
|
||||
if (jwt) {
|
||||
// would prefer to validate but we've got an error with JWTs on QR codes generated in the future
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
// const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt);
|
||||
// decode the JWT
|
||||
const parsedJwt = decodeEndorserJwt(jwt);
|
||||
|
||||
const contacts: Array<Contact> =
|
||||
parsedJwt.payload.contacts || // someday this will be the only payload sent to this page
|
||||
parsedJwt.payload.contacts ||
|
||||
(Array.isArray(parsedJwt.payload) ? parsedJwt.payload : undefined);
|
||||
|
||||
if (!contacts && parsedJwt.payload.own) {
|
||||
// handle this single-contact JWT in the contacts page, better suited to single additions
|
||||
(this.$router as Router).push({
|
||||
this.$router.push({
|
||||
name: "contacts",
|
||||
query: { contactJwt: jwt },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (contacts) {
|
||||
await this.setContactsSelected(contacts);
|
||||
} else {
|
||||
// no contacts found so default message should be OK
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles automatic import for single new contacts
|
||||
*/
|
||||
private async handleAutoImport() {
|
||||
if (
|
||||
this.contactsImporting.length === 1 &&
|
||||
R.isEmpty(this.contactsExisting)
|
||||
) {
|
||||
// if there is only one contact and it's new, then we will automatically import it
|
||||
this.contactsSelected[0] = true;
|
||||
this.importContacts(); // ... which routes to the contacts list
|
||||
await this.importContacts();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes contacts for import and checks for duplicates
|
||||
* @param contacts Array of contacts to process
|
||||
*/
|
||||
async setContactsSelected(contacts: Array<Contact>) {
|
||||
this.contactsImporting = contacts;
|
||||
this.contactsSelected = new Array(this.contactsImporting.length).fill(true);
|
||||
|
||||
await db.open();
|
||||
const baseContacts = await db.contacts.toArray();
|
||||
// set the existing contacts, keyed by DID, if they exist in contactsImporting
|
||||
|
||||
// Check for existing contacts and differences
|
||||
for (let i = 0; i < this.contactsImporting.length; i++) {
|
||||
const contactIn = this.contactsImporting[i];
|
||||
const existingContact = baseContacts.find(
|
||||
@@ -241,6 +413,7 @@ export default class ContactImportView extends Vue {
|
||||
if (existingContact) {
|
||||
this.contactsExisting[contactIn.did] = existingContact;
|
||||
|
||||
// Compare contact fields for differences
|
||||
const differences: Record<
|
||||
string,
|
||||
{
|
||||
@@ -249,8 +422,12 @@ export default class ContactImportView extends Vue {
|
||||
}
|
||||
> = {};
|
||||
Object.keys(contactIn).forEach((key) => {
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
if (!R.equals(contactIn[key as keyof Contact], existingContact[key as keyof Contact])) {
|
||||
if (
|
||||
!R.equals(
|
||||
contactIn[key as keyof Contact],
|
||||
existingContact[key as keyof Contact],
|
||||
)
|
||||
) {
|
||||
differences[key] = {
|
||||
old: existingContact[key as keyof Contact],
|
||||
new: contactIn[key as keyof Contact],
|
||||
@@ -262,13 +439,16 @@ export default class ContactImportView extends Vue {
|
||||
this.sameCount++;
|
||||
}
|
||||
|
||||
// don't automatically import previous data
|
||||
// Don't auto-select duplicates
|
||||
this.contactsSelected[i] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check the contact-import JWT
|
||||
/**
|
||||
* Validates contact import JWT format
|
||||
* @param jwtInput JWT string to validate
|
||||
*/
|
||||
async checkContactJwt(jwtInput: string) {
|
||||
if (
|
||||
jwtInput.endsWith(APP_SERVER) ||
|
||||
@@ -288,14 +468,15 @@ export default class ContactImportView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
// process the invite JWT and/or text message containing the URL with the JWT
|
||||
/**
|
||||
* Processes contact import JWT and updates contacts
|
||||
* @param jwtInput JWT string containing contact data
|
||||
*/
|
||||
async processContactJwt(jwtInput: string) {
|
||||
this.checkingImports = true;
|
||||
|
||||
try {
|
||||
// (For another approach used with invites, see InviteOneAcceptView.processInvite)
|
||||
const jwt: string = getContactJwtFromJwtUrl(jwtInput);
|
||||
// JWT format: { header, payload, signature, data }
|
||||
const payload = decodeEndorserJwt(jwt).payload;
|
||||
|
||||
if (Array.isArray(payload.contacts)) {
|
||||
@@ -319,10 +500,16 @@ export default class ContactImportView extends Vue {
|
||||
this.checkingImports = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports selected contacts and sets visibility if requested
|
||||
* Updates existing contacts or adds new ones
|
||||
*/
|
||||
async importContacts() {
|
||||
this.checkingImports = true;
|
||||
let importedCount = 0,
|
||||
updatedCount = 0;
|
||||
|
||||
// Process selected contacts
|
||||
for (let i = 0; i < this.contactsImporting.length; i++) {
|
||||
if (this.contactsSelected[i]) {
|
||||
const contact = this.contactsImporting[i];
|
||||
@@ -338,6 +525,8 @@ export default class ContactImportView extends Vue {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set visibility if requested
|
||||
if (this.makeVisible) {
|
||||
const failedVisibileToContacts = [];
|
||||
for (let i = 0; i < this.contactsImporting.length; i++) {
|
||||
@@ -375,6 +564,7 @@ export default class ContactImportView extends Vue {
|
||||
|
||||
this.checkingImports = false;
|
||||
|
||||
// Show success notification
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -386,7 +576,7 @@ export default class ContactImportView extends Vue {
|
||||
},
|
||||
3000,
|
||||
);
|
||||
(this.$router as Router).push({ name: "contacts" });
|
||||
this.$router.push({ name: "contacts" });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw" />
|
||||
<font-awesome icon="chevron-left" class="fa-fw" />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -26,10 +26,8 @@
|
||||
You aren't sharing your name, so quickly
|
||||
<br />
|
||||
<span
|
||||
@click="
|
||||
() => $refs.userNameDialog.open((name) => (this.givenName = name))
|
||||
"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md"
|
||||
@click="() => $refs.userNameDialog.open((name) => (givenName = name))"
|
||||
>
|
||||
click here to set it for them.
|
||||
</span>
|
||||
@@ -38,18 +36,18 @@
|
||||
<UserNameDialog ref="userNameDialog" />
|
||||
|
||||
<div
|
||||
@click="onCopyUrlToClipboard()"
|
||||
v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
|
||||
class="text-center"
|
||||
@click="onCopyUrlToClipboard()"
|
||||
>
|
||||
<!--
|
||||
Play with display options: https://qr-code-styling.com/
|
||||
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
|
||||
-->
|
||||
<QRCodeVue3
|
||||
:value="this.qrValue"
|
||||
:cornersSquareOptions="{ type: 'extra-rounded' }"
|
||||
:dotsOptions="{ type: 'square' }"
|
||||
:value="qrValue"
|
||||
:corners-square-options="{ type: 'extra-rounded' }"
|
||||
:dots-options="{ type: 'square' }"
|
||||
class="flex justify-center"
|
||||
/>
|
||||
<span>
|
||||
@@ -58,14 +56,14 @@
|
||||
</div>
|
||||
<div v-else-if="activeDid" class="text-center">
|
||||
<!-- Not an ETHR DID so force them to paste it. (Passkey Peer DIDs are too big.) -->
|
||||
<span @click="onCopyDidToClipboard()" class="text-blue-500">
|
||||
<span class="text-blue-500" @click="onCopyDidToClipboard()">
|
||||
Click here to copy your DID to your clipboard.
|
||||
</span>
|
||||
<span>
|
||||
Then give it to them so they can paste it in their list of People.
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-center" v-else>
|
||||
<div v-else class="text-center">
|
||||
You have no identitifiers yet, so
|
||||
<router-link
|
||||
:to="{ name: 'start' }"
|
||||
@@ -110,7 +108,8 @@ import {
|
||||
} from "../libs/endorserServer";
|
||||
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
||||
import { retrieveAccountMetadata } from "../libs/util";
|
||||
|
||||
import { Router } from "vue-router";
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: {
|
||||
QrcodeStream,
|
||||
@@ -121,6 +120,7 @@ import { retrieveAccountMetadata } from "../libs/util";
|
||||
})
|
||||
export default class ContactQRScanShow extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
@@ -210,7 +210,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing QR info:", e);
|
||||
logger.error("Error parsing QR info:", e);
|
||||
this.danger("Could not parse the QR info.", "Read Error");
|
||||
return;
|
||||
}
|
||||
@@ -274,7 +274,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error saving contact info:", e);
|
||||
logger.error("Error saving contact info:", e);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -310,7 +310,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
if (result.error) {
|
||||
this.danger(result.error as string, "Error Setting Visibility");
|
||||
} else if (!result.success) {
|
||||
console.error("Got strange result from setting visibility:", result);
|
||||
logger.error("Got strange result from setting visibility:", result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error when registering:", error);
|
||||
logger.error("Error when registering:", error);
|
||||
let userMessage = "There was an error.";
|
||||
const serverError = error as AxiosError;
|
||||
if (serverError) {
|
||||
@@ -389,7 +389,7 @@ export default class ContactQRScanShow extends Vue {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onScanError(error: any) {
|
||||
console.error("Scan was invalid:", error);
|
||||
logger.error("Scan was invalid:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<router-link
|
||||
:to="{ name: 'account' }"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
><fa icon="chevron-left" class="fa-fw"></fa
|
||||
></router-link>
|
||||
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</router-link>
|
||||
|
||||
Scan Contact
|
||||
</h1>
|
||||
|
||||
@@ -23,26 +23,26 @@
|
||||
|
||||
<!-- New Contact -->
|
||||
<div id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
|
||||
<span class="flex" v-if="isRegistered">
|
||||
<span v-if="isRegistered" class="flex">
|
||||
<router-link
|
||||
:to="{ name: 'invite-one' }"
|
||||
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||
>
|
||||
<fa icon="envelope-open-text" class="fa-fw text-2xl" />
|
||||
<font-awesome icon="envelope-open-text" class="fa-fw text-2xl" />
|
||||
</router-link>
|
||||
|
||||
<button
|
||||
@click="showOnboardMeetingDialog()"
|
||||
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||
@click="showOnboardMeetingDialog()"
|
||||
>
|
||||
<fa icon="chair" class="fa-fw text-2xl" />
|
||||
<font-awesome icon="chair" class="fa-fw text-2xl" />
|
||||
</button>
|
||||
</span>
|
||||
<span v-else class="flex">
|
||||
<span
|
||||
class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||
>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="envelope-open-text"
|
||||
class="fa-fw text-2xl"
|
||||
@click="
|
||||
@@ -56,7 +56,7 @@
|
||||
<span
|
||||
class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||
>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="chair"
|
||||
class="fa-fw text-2xl"
|
||||
@click="
|
||||
@@ -73,38 +73,39 @@
|
||||
:to="{ name: 'contact-qr' }"
|
||||
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
|
||||
>
|
||||
<fa icon="qrcode" class="fa-fw text-2xl" />
|
||||
<font-awesome icon="qrcode" class="fa-fw text-2xl" />
|
||||
</router-link>
|
||||
|
||||
<textarea
|
||||
v-model="contactInput"
|
||||
type="text"
|
||||
placeholder="New URL or DID, Name, Public Key, Next Public Key Hash"
|
||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
|
||||
v-model="contactInput"
|
||||
/>
|
||||
<button
|
||||
class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
|
||||
@click="onClickNewContact()"
|
||||
>
|
||||
<fa icon="plus" class="fa-fw" />
|
||||
<font-awesome icon="plus" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between" v-if="contacts.length > 0">
|
||||
<div v-if="contacts.length > 0" class="flex justify-between">
|
||||
<div class="w-full text-left">
|
||||
<div v-if="!showGiveNumbers">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="contactsSelected.length === contacts.length"
|
||||
class="align-middle ml-2 h-6 w-6"
|
||||
data-testId="contactCheckAllTop"
|
||||
@click="
|
||||
contactsSelected.length === contacts.length
|
||||
? (contactsSelected = [])
|
||||
: (contactsSelected = contacts.map((contact) => contact.did))
|
||||
"
|
||||
class="align-middle ml-2 h-6 w-6"
|
||||
data-testId="contactCheckAllTop"
|
||||
/>
|
||||
<button
|
||||
v-if="!showGiveNumbers"
|
||||
href=""
|
||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
|
||||
:style="
|
||||
@@ -112,14 +113,16 @@
|
||||
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
||||
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
||||
"
|
||||
@click="copySelectedContacts()"
|
||||
v-if="!showGiveNumbers"
|
||||
data-testId="copySelectedContactsButtonTop"
|
||||
@click="copySelectedContacts()"
|
||||
>
|
||||
Copy Selections
|
||||
</button>
|
||||
<button @click="showCopySelectionsInfo()">
|
||||
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-xl text-blue-500 ml-4"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,20 +139,20 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between mt-1" v-if="showGiveNumbers">
|
||||
<div v-if="showGiveNumbers" class="flex justify-between mt-1">
|
||||
<div class="w-full text-right">
|
||||
In the following, only the most recent hours are included. To see more,
|
||||
click
|
||||
<span
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
|
||||
>
|
||||
<fa icon="file-lines" class="fa-fw" />
|
||||
<font-awesome icon="file-lines" class="fa-fw" />
|
||||
</span>
|
||||
<br />
|
||||
<button
|
||||
href=""
|
||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
|
||||
v-bind:class="showGiveAmountsClassNames()"
|
||||
:class="showGiveAmountsClassNames()"
|
||||
@click="toggleShowGiveTotals()"
|
||||
>
|
||||
{{
|
||||
@@ -159,36 +162,38 @@
|
||||
? "Confirmed Amounts"
|
||||
: "Unconfirmed Amounts"
|
||||
}}
|
||||
<fa icon="left-right" class="fa-fw" />
|
||||
<font-awesome icon="left-right" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results List -->
|
||||
<ul
|
||||
id="listContacts"
|
||||
v-if="contacts.length > 0"
|
||||
id="listContacts"
|
||||
class="border-t border-slate-300 mt-1"
|
||||
>
|
||||
<li
|
||||
class="border-b border-slate-300 pt-1 pb-1"
|
||||
v-for="contact in filteredContacts()"
|
||||
:key="contact.did"
|
||||
class="border-b border-slate-300 pt-1 pb-1"
|
||||
data-testId="contactListItem"
|
||||
>
|
||||
<div class="grow overflow-hidden">
|
||||
<div class="flex items-center">
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:iconSize="24"
|
||||
:icon-size="24"
|
||||
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
|
||||
@click="showLargeIdenticon = contact"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="checkbox"
|
||||
v-if="!showGiveNumbers"
|
||||
type="checkbox"
|
||||
:checked="contactsSelected.includes(contact.did)"
|
||||
class="ml-2 h-6 w-6 flex-shrink-0"
|
||||
data-testId="contactCheckOne"
|
||||
@click="
|
||||
contactsSelected.includes(contact.did)
|
||||
? contactsSelected.splice(
|
||||
@@ -197,8 +202,6 @@
|
||||
)
|
||||
: contactsSelected.push(contact.did)
|
||||
"
|
||||
class="ml-2 h-6 w-6 flex-shrink-0"
|
||||
data-testId="contactCheckOne"
|
||||
/>
|
||||
|
||||
<h2
|
||||
@@ -215,7 +218,10 @@
|
||||
}"
|
||||
title="See more about this person"
|
||||
>
|
||||
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-xl text-blue-500 ml-4"
|
||||
/>
|
||||
</router-link>
|
||||
|
||||
<span class="ml-4 text-sm overflow-hidden">{{
|
||||
@@ -234,17 +240,17 @@
|
||||
>
|
||||
<button
|
||||
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md"
|
||||
@click="confirmShowGiftedDialog(contact.did, activeDid)"
|
||||
:title="givenToMeDescriptions[contact.did] || ''"
|
||||
@click="confirmShowGiftedDialog(contact.did, activeDid)"
|
||||
>
|
||||
From:
|
||||
<br />
|
||||
{{
|
||||
/* eslint-disable prettier/prettier */
|
||||
this.showGiveTotals
|
||||
showGiveTotals
|
||||
? ((givenToMeConfirmed[contact.did] || 0)
|
||||
+ (givenToMeUnconfirmed[contact.did] || 0))
|
||||
: this.showGiveConfirmed
|
||||
: showGiveConfirmed
|
||||
? (givenToMeConfirmed[contact.did] || 0)
|
||||
: (givenToMeUnconfirmed[contact.did] || 0)
|
||||
/* eslint-enable prettier/prettier */
|
||||
@@ -253,17 +259,17 @@
|
||||
|
||||
<button
|
||||
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
|
||||
@click="confirmShowGiftedDialog(activeDid, contact.did)"
|
||||
:title="givenByMeDescriptions[contact.did] || ''"
|
||||
@click="confirmShowGiftedDialog(activeDid, contact.did)"
|
||||
>
|
||||
To:
|
||||
<br />
|
||||
{{
|
||||
/* eslint-disable prettier/prettier */
|
||||
this.showGiveTotals
|
||||
showGiveTotals
|
||||
? ((givenByMeConfirmed[contact.did] || 0)
|
||||
+ (givenByMeUnconfirmed[contact.did] || 0))
|
||||
: this.showGiveConfirmed
|
||||
: showGiveConfirmed
|
||||
? (givenByMeConfirmed[contact.did] || 0)
|
||||
: (givenByMeUnconfirmed[contact.did] || 0)
|
||||
/* eslint-enable prettier/prettier */
|
||||
@@ -272,8 +278,8 @@
|
||||
|
||||
<button
|
||||
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-blue-400"
|
||||
@click="openOfferDialog(contact.did, contact.name)"
|
||||
data-testId="offerButton"
|
||||
@click="openOfferDialog(contact.did, contact.name)"
|
||||
>
|
||||
Offer
|
||||
</button>
|
||||
@@ -286,7 +292,7 @@
|
||||
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-slate-400"
|
||||
title="See more given activity"
|
||||
>
|
||||
<fa icon="file-lines" class="fa-fw" />
|
||||
<font-awesome icon="file-lines" class="fa-fw" />
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,20 +301,21 @@
|
||||
</ul>
|
||||
<p v-else>There are no contacts.</p>
|
||||
|
||||
<div class="mt-2 w-full text-left" v-if="contacts.length > 0">
|
||||
<div v-if="contacts.length > 0" class="mt-2 w-full text-left">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-if="!showGiveNumbers"
|
||||
type="checkbox"
|
||||
:checked="contactsSelected.length === contacts.length"
|
||||
class="align-middle ml-2 h-6 w-6"
|
||||
data-testId="contactCheckAllBottom"
|
||||
@click="
|
||||
contactsSelected.length === contacts.length
|
||||
? (contactsSelected = [])
|
||||
: (contactsSelected = contacts.map((contact) => contact.did))
|
||||
"
|
||||
class="align-middle ml-2 h-6 w-6"
|
||||
data-testId="contactCheckAllBottom"
|
||||
/>
|
||||
<button
|
||||
v-if="!showGiveNumbers"
|
||||
href=""
|
||||
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
|
||||
:style="
|
||||
@@ -317,7 +324,6 @@
|
||||
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
||||
"
|
||||
@click="copySelectedContacts()"
|
||||
v-if="!showGiveNumbers"
|
||||
>
|
||||
Copy Selections
|
||||
</button>
|
||||
@@ -333,7 +339,7 @@
|
||||
>
|
||||
<EntityIcon
|
||||
:contact="showLargeIdenticon"
|
||||
:iconSize="512"
|
||||
:icon-size="512"
|
||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||
@click="showLargeIdenticon = undefined"
|
||||
/>
|
||||
@@ -386,7 +392,7 @@ import {
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { generateSaveAndActivateIdentity } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: {
|
||||
GiftedDialog,
|
||||
@@ -399,6 +405,8 @@ import { generateSaveAndActivateIdentity } from "../libs/util";
|
||||
})
|
||||
export default class ContactsView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
@@ -465,8 +473,7 @@ export default class ContactsView extends Vue {
|
||||
//
|
||||
// For external links, use /contact-import/:jwt with a JWT that has an array of contacts
|
||||
// because that will do better error checking for things like missing data on iOS platforms.
|
||||
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
|
||||
.query["contactJwt"] as string;
|
||||
const importedContactJwt = this.$route.query["contactJwt"] as string;
|
||||
if (importedContactJwt) {
|
||||
// really should fully verify contents
|
||||
const { payload } = decodeEndorserJwt(importedContactJwt);
|
||||
@@ -481,14 +488,13 @@ export default class ContactsView extends Vue {
|
||||
} as Contact;
|
||||
await this.addContact(newContact);
|
||||
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
||||
(this.$router as Router).push({ path: "/contacts" });
|
||||
this.$router.push({ path: "/contacts" });
|
||||
}
|
||||
}
|
||||
|
||||
private async processInviteJwt() {
|
||||
// handle an invite JWT sent via URL
|
||||
const importedInviteJwt = (this.$route as RouteLocationNormalizedLoaded)
|
||||
.query["inviteJwt"] as string;
|
||||
const importedInviteJwt = this.$route.query["inviteJwt"] as string;
|
||||
if (importedInviteJwt === "") {
|
||||
// this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
|
||||
this.$notify(
|
||||
@@ -590,7 +596,7 @@ export default class ContactsView extends Vue {
|
||||
);
|
||||
}
|
||||
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
||||
(this.$router as Router).push({ path: "/contacts" });
|
||||
this.$router.push({ path: "/contacts" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,7 +636,7 @@ export default class ContactsView extends Vue {
|
||||
title: "They're Added To Your List",
|
||||
text: "Would you like to go to the main page now?",
|
||||
onYes: async () => {
|
||||
(this.$router as Router).push({ name: "home" });
|
||||
this.$router.push({ name: "home" });
|
||||
},
|
||||
},
|
||||
-1,
|
||||
@@ -677,7 +683,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Got bad response status & data of",
|
||||
resp.status,
|
||||
resp.data,
|
||||
@@ -767,9 +773,7 @@ export default class ContactsView extends Vue {
|
||||
|
||||
if (contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
|
||||
const jwt = getContactJwtFromJwtUrl(contactInput);
|
||||
(this.$router as Router).push({
|
||||
path: "/contact-import/" + jwt,
|
||||
});
|
||||
this.$router.push({ path: "/contact-import/" + jwt });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -877,7 +881,7 @@ export default class ContactsView extends Vue {
|
||||
);
|
||||
try {
|
||||
const contacts = JSON.parse(jsonContactInput);
|
||||
(this.$router as Router).push({
|
||||
this.$router.push({
|
||||
name: "contact-import",
|
||||
query: { contacts: JSON.stringify(contacts) },
|
||||
});
|
||||
@@ -1157,7 +1161,7 @@ export default class ContactsView extends Vue {
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Got strange result from setting visibility. It can happen when setting visibility on oneself.",
|
||||
result,
|
||||
);
|
||||
@@ -1203,7 +1207,7 @@ export default class ContactsView extends Vue {
|
||||
this.showGiftedDialog(giverDid, recipientDid);
|
||||
},
|
||||
onYes: async () => {
|
||||
(this.$router as Router).push({
|
||||
this.$router.push({
|
||||
name: "contact-amounts",
|
||||
query: { contactDid: giverDid },
|
||||
});
|
||||
@@ -1403,10 +1407,10 @@ export default class ContactsView extends Vue {
|
||||
|
||||
if (hostResponse.data.data) {
|
||||
// They're the host, take them to setup
|
||||
(this.$router as Router).push({ name: "onboard-meeting-setup" });
|
||||
this.$router.push({ name: "onboard-meeting-setup" });
|
||||
} else {
|
||||
// They're not the host, take them to list
|
||||
(this.$router as Router).push({ name: "onboard-meeting-list" });
|
||||
this.$router.push({ name: "onboard-meeting-list" });
|
||||
}
|
||||
} else {
|
||||
// They're not in a meeting, show the dialog
|
||||
@@ -1417,11 +1421,11 @@ export default class ContactsView extends Vue {
|
||||
title: "Onboarding Meeting",
|
||||
text: "Would you like to start a new meeting?",
|
||||
onYes: async () => {
|
||||
(this.$router as Router).push({ name: "onboard-meeting-setup" });
|
||||
this.$router.push({ name: "onboard-meeting-setup" });
|
||||
},
|
||||
yesText: "Start New Meeting",
|
||||
onNo: async () => {
|
||||
(this.$router as Router).push({ name: "onboard-meeting-list" });
|
||||
this.$router.push({ name: "onboard-meeting-list" });
|
||||
},
|
||||
noText: "Join Existing Meeting",
|
||||
},
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7">
|
||||
<!-- Back -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
Identifier Details
|
||||
</h1>
|
||||
@@ -29,16 +29,20 @@
|
||||
<router-link
|
||||
:to="{ name: 'contact-edit', params: { did: contactFromDid?.did } }"
|
||||
>
|
||||
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||
</router-link>
|
||||
</h2>
|
||||
<button
|
||||
@click="showDidDetails = !showDidDetails"
|
||||
class="ml-2 mr-2 mt-4"
|
||||
@click="showDidDetails = !showDidDetails"
|
||||
>
|
||||
Details
|
||||
<fa v-if="showDidDetails" icon="chevron-down" class="text-blue-400" />
|
||||
<fa v-else icon="chevron-right" class="text-blue-400" />
|
||||
<font-awesome
|
||||
v-if="showDidDetails"
|
||||
icon="chevron-down"
|
||||
class="text-blue-400"
|
||||
/>
|
||||
<font-awesome v-else icon="chevron-right" class="text-blue-400" />
|
||||
</button>
|
||||
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
|
||||
<pre
|
||||
@@ -54,7 +58,7 @@
|
||||
>
|
||||
<EntityIcon
|
||||
:icon-size="96"
|
||||
:profileImageUrl="contactFromDid?.profileImageUrl"
|
||||
:profile-image-url="contactFromDid?.profileImageUrl"
|
||||
class="inline-block align-text-bottom border border-slate-300 rounded"
|
||||
@click="showLargeIdenticonUrl = contactFromDid?.profileImageUrl"
|
||||
/>
|
||||
@@ -69,61 +73,65 @@
|
||||
contactFromDid?.seesMe && contactFromDid.did !== activeDid
|
||||
"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
@click="confirmSetVisibility(contactFromDid, false)"
|
||||
title="They can see you"
|
||||
@click="confirmSetVisibility(contactFromDid, false)"
|
||||
>
|
||||
<fa icon="eye" class="fa-fw" />
|
||||
<font-awesome icon="eye" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
v-else-if="
|
||||
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
|
||||
"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
@click="confirmSetVisibility(contactFromDid, true)"
|
||||
title="They cannot see you"
|
||||
@click="confirmSetVisibility(contactFromDid, true)"
|
||||
>
|
||||
<fa icon="eye-slash" class="fa-fw" />
|
||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
@click="checkVisibility(contactFromDid)"
|
||||
title="Check Visibility"
|
||||
v-if="contactFromDid?.did !== activeDid"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="Check Visibility"
|
||||
@click="checkVisibility(contactFromDid)"
|
||||
>
|
||||
<fa icon="rotate" class="fa-fw" />
|
||||
<font-awesome icon="rotate" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="confirmRegister(contactFromDid)"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
v-if="contactFromDid?.did !== activeDid"
|
||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="Registration"
|
||||
@click="confirmRegister(contactFromDid)"
|
||||
>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="contactFromDid?.registered"
|
||||
icon="person-circle-check"
|
||||
class="fa-fw"
|
||||
/>
|
||||
<fa v-else icon="person-circle-question" class="fa-fw" />
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="person-circle-question"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="confirmDeleteContact(contactFromDid)"
|
||||
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||
title="Delete"
|
||||
@click="confirmDeleteContact(contactFromDid)"
|
||||
>
|
||||
<fa icon="trash-can" class="fa-fw" />
|
||||
<font-awesome icon="trash-can" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!contactFromDid?.profileImageUrl">
|
||||
<div>Auto-Generated Icon</div>
|
||||
<div class="flex justify-center">
|
||||
<EntityIcon
|
||||
:entityId="viewingDid"
|
||||
:iconSize="64"
|
||||
:entity-id="viewingDid"
|
||||
:icon-size="64"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
@click="showLargeIdenticonId = viewingDid"
|
||||
/>
|
||||
@@ -138,9 +146,9 @@
|
||||
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
|
||||
>
|
||||
<EntityIcon
|
||||
:entityId="showLargeIdenticonId"
|
||||
:iconSize="512"
|
||||
:profileImageUrl="showLargeIdenticonUrl"
|
||||
:entity-id="showLargeIdenticonId"
|
||||
:icon-size="512"
|
||||
:profile-image-url="showLargeIdenticonUrl"
|
||||
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
|
||||
@click="
|
||||
showLargeIdenticonId = undefined;
|
||||
@@ -161,10 +169,10 @@
|
||||
|
||||
<!-- Loading Animation -->
|
||||
<div
|
||||
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||
v-if="isLoading"
|
||||
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||
>
|
||||
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
|
||||
</div>
|
||||
<!-- Results List -->
|
||||
<div v-if="claims.length > 0" class="mt-4">
|
||||
@@ -175,9 +183,9 @@
|
||||
<InfiniteScroll @reached-bottom="loadMoreData">
|
||||
<ul>
|
||||
<li
|
||||
class="border-b border-slate-300"
|
||||
v-for="claim in claims"
|
||||
:key="claim.handleId"
|
||||
class="border-b border-slate-300"
|
||||
>
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<span class="col-span-2">
|
||||
@@ -193,8 +201,11 @@
|
||||
{{ claimDescription(claim) }}
|
||||
</span>
|
||||
<span class="col-span-1">
|
||||
<a @click="onClickLoadClaim(claim.id)" class="cursor-pointer">
|
||||
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
|
||||
<a class="cursor-pointer" @click="onClickLoadClaim(claim.id)">
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="pl-2 pt-1 text-blue-500"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -216,7 +227,7 @@
|
||||
import { AxiosError } from "axios";
|
||||
import * as yaml from "js-yaml";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import InfiniteScroll from "../components/InfiniteScroll.vue";
|
||||
@@ -226,20 +237,34 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { BoundingBox } from "../db/tables/settings";
|
||||
import {
|
||||
capitalizeAndInsertSpacesBeforeCaps,
|
||||
didInfoForContact,
|
||||
displayAmount,
|
||||
getHeaders,
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
GiveVerifiableCredential,
|
||||
OfferVerifiableCredential,
|
||||
} from "../interfaces";
|
||||
import {
|
||||
capitalizeAndInsertSpacesBeforeCaps,
|
||||
didInfoForContact,
|
||||
displayAmount,
|
||||
getHeaders,
|
||||
register,
|
||||
setVisibilityUtil,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
/**
|
||||
* DIDView Component
|
||||
*
|
||||
* Displays detailed information about a DID (Decentralized Identifier) entity, including:
|
||||
* - Basic identity information (name, profile image)
|
||||
* - Contact management controls (visibility, registration status)
|
||||
* - Associated claims and their details
|
||||
*
|
||||
* The view supports both viewing one's own DID and other contacts' DIDs.
|
||||
* It provides infinite scrolling for claims and interactive controls for contact management.
|
||||
*/
|
||||
@Component({
|
||||
components: {
|
||||
EntityIcon,
|
||||
@@ -250,6 +275,8 @@ import EntityIcon from "../components/EntityIcon.vue";
|
||||
})
|
||||
export default class DIDView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
libsUtil = libsUtil;
|
||||
yaml = yaml;
|
||||
@@ -272,51 +299,111 @@ export default class DIDView extends Vue {
|
||||
didInfoForContact = didInfoForContact;
|
||||
displayAmount = displayAmount;
|
||||
|
||||
/**
|
||||
* Initializes the view with DID information
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Retrieves active account settings (DID and API server)
|
||||
* 2. Determines which DID to display from URL params or defaults to active DID
|
||||
* 3. Loads contact information if available
|
||||
* 4. Loads associated claims
|
||||
* 5. Determines if viewing own DID
|
||||
*/
|
||||
async mounted() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
|
||||
const pathParam = window.location.pathname.substring("/did/".length);
|
||||
let showDid = pathParam;
|
||||
if (!showDid) {
|
||||
showDid = this.activeDid;
|
||||
if (showDid) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
title: "Your Info",
|
||||
text: "No user was specified so showing your info.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (showDid) {
|
||||
this.viewingDid = decodeURIComponent(showDid);
|
||||
this.contactFromDid = await db.contacts.get(this.viewingDid);
|
||||
if (this.contactFromDid) {
|
||||
this.contactYaml = yaml.dump(this.contactFromDid);
|
||||
}
|
||||
await this.initializeSettings();
|
||||
await this.determineDIDToDisplay();
|
||||
if (this.viewingDid) {
|
||||
await this.loadContactInformation();
|
||||
await this.loadClaimsAbout();
|
||||
|
||||
const allAccountDids = await libsUtil.retrieveAccountDids();
|
||||
this.isMyDid = allAccountDids.includes(this.viewingDid);
|
||||
await this.checkIfOwnDID();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data loader used by infinite scroller
|
||||
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||
**/
|
||||
* Initializes component settings from active account
|
||||
*/
|
||||
private async initializeSettings() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which DID to display based on URL parameters
|
||||
* Falls back to active DID if no parameter provided
|
||||
*/
|
||||
private async determineDIDToDisplay() {
|
||||
const pathParam = window.location.pathname.substring("/did/".length);
|
||||
let showDid = pathParam;
|
||||
|
||||
if (!showDid) {
|
||||
showDid = this.activeDid;
|
||||
if (showDid) {
|
||||
this.notifyDefaultToActiveDID();
|
||||
}
|
||||
}
|
||||
|
||||
if (showDid) {
|
||||
this.viewingDid = decodeURIComponent(showDid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies user that we're showing their DID info by default
|
||||
*/
|
||||
private notifyDefaultToActiveDID() {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
title: "Your Info",
|
||||
text: "No user was specified so showing your info.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads contact information for the viewing DID
|
||||
* Updates contact YAML representation if contact exists
|
||||
*/
|
||||
private async loadContactInformation() {
|
||||
if (!this.viewingDid) return;
|
||||
|
||||
this.contactFromDid = await db.contacts.get(this.viewingDid);
|
||||
if (this.contactFromDid) {
|
||||
this.contactYaml = yaml.dump(this.contactFromDid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the viewing DID belongs to the current user
|
||||
*/
|
||||
private async checkIfOwnDID() {
|
||||
if (!this.viewingDid) return;
|
||||
|
||||
const allAccountDids = await libsUtil.retrieveAccountDids();
|
||||
this.isMyDid = allAccountDids.includes(this.viewingDid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads additional claims when user scrolls to bottom
|
||||
* Used by infinite scroll component to implement pagination
|
||||
*
|
||||
* @param payload - Boolean indicating if more data should be loaded
|
||||
*/
|
||||
async loadMoreData(payload: boolean) {
|
||||
if (this.claims.length > 0 && !this.hitEnd && payload) {
|
||||
this.loadClaimsAbout();
|
||||
}
|
||||
}
|
||||
|
||||
// prompt with confirmation if they want to delete a contact
|
||||
/**
|
||||
* Prompts user to confirm contact deletion
|
||||
* Shows additional warning if contact has visibility permissions
|
||||
*
|
||||
* @param contact - Contact object to be deleted
|
||||
*/
|
||||
confirmDeleteContact(contact: Contact) {
|
||||
let message =
|
||||
"Are you sure you want to remove " +
|
||||
@@ -340,6 +427,11 @@ export default class DIDView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes contact from local database and navigates back to contacts list
|
||||
*
|
||||
* @param contact - Contact object to be deleted
|
||||
*/
|
||||
async deleteContact(contact: Contact) {
|
||||
await db.open();
|
||||
await db.contacts.delete(contact.did);
|
||||
@@ -352,10 +444,15 @@ export default class DIDView extends Vue {
|
||||
},
|
||||
3000,
|
||||
);
|
||||
(this.$router as Router).push({ name: "contacts" });
|
||||
this.$router.push({ name: "contacts" });
|
||||
}
|
||||
|
||||
// confirm to register a new contact
|
||||
/**
|
||||
* Prompts user to confirm registering a contact
|
||||
* Shows additional warning if contact is already registered
|
||||
*
|
||||
* @param contact - Contact to be registered
|
||||
*/
|
||||
async confirmRegister(contact: Contact) {
|
||||
this.$notify(
|
||||
{
|
||||
@@ -377,7 +474,12 @@ export default class DIDView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
// note that this is also in ContactView.vue
|
||||
/**
|
||||
* Registers a contact with the endorser server
|
||||
* Updates local database with registration status
|
||||
*
|
||||
* @param contact - Contact to register
|
||||
*/
|
||||
async register(contact: Contact) {
|
||||
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
|
||||
|
||||
@@ -416,7 +518,7 @@ export default class DIDView extends Vue {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error when registering:", error);
|
||||
logger.error("Error when registering:", error);
|
||||
let userMessage = "There was an error.";
|
||||
const serverError = error as AxiosError;
|
||||
if (serverError) {
|
||||
@@ -443,9 +545,14 @@ export default class DIDView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads claims that involve the viewed DID
|
||||
* Implements pagination using beforeId parameter
|
||||
* Updates loading state and hit-end status
|
||||
*/
|
||||
public async loadClaimsAbout() {
|
||||
if (!this.viewingDid) {
|
||||
console.error("This should never be called without a DID.");
|
||||
logger.error("This should never be called without a DID.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -467,7 +574,7 @@ export default class DIDView extends Vue {
|
||||
|
||||
if (response.status !== 200) {
|
||||
const details = await response.text();
|
||||
console.error("Problem with full search:", details);
|
||||
logger.error("Problem with full search:", details);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -486,7 +593,7 @@ export default class DIDView extends Vue {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
console.error("Error with feed load:", e);
|
||||
logger.error("Error with feed load:", e);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -501,13 +608,25 @@ export default class DIDView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to detailed claim view
|
||||
*
|
||||
* @param jwtId - JWT ID of the claim to view
|
||||
*/
|
||||
onClickLoadClaim(jwtId: string) {
|
||||
const route = {
|
||||
path: "/claim/" + encodeURIComponent(jwtId),
|
||||
};
|
||||
(this.$router as Router).push(route);
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and formats claim amount information
|
||||
* Handles different claim types (GiveAction, Offer)
|
||||
*
|
||||
* @param claim - Claim object to process
|
||||
* @returns Formatted amount string or empty string if no amount
|
||||
*/
|
||||
public claimAmount(claim: GenericVerifiableCredential) {
|
||||
if (claim.claimType === "GiveAction") {
|
||||
const giveClaim = claim.claim as GiveVerifiableCredential;
|
||||
@@ -536,11 +655,23 @@ export default class DIDView extends Vue {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts claim description
|
||||
* Falls back to name if no description available
|
||||
*
|
||||
* @param claim - Claim to get description from
|
||||
* @returns Description string or empty string
|
||||
*/
|
||||
claimDescription(claim: GenericVerifiableCredential) {
|
||||
return claim.claim.name || claim.claim.description || "";
|
||||
}
|
||||
|
||||
// note that this is also in ContactView.vue
|
||||
/**
|
||||
* Prompts user to confirm visibility change for a contact
|
||||
*
|
||||
* @param contact - Contact to modify visibility for
|
||||
* @param visibility - New visibility state to set
|
||||
*/
|
||||
async confirmSetVisibility(contact: Contact, visibility: boolean) {
|
||||
const visibilityPrompt = visibility
|
||||
? "Are you sure you want to make your activity visible to them?"
|
||||
@@ -562,7 +693,14 @@ export default class DIDView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
// note that this is also in ContactView.vue
|
||||
/**
|
||||
* Updates contact visibility on server and local database
|
||||
*
|
||||
* @param contact - Contact to update visibility for
|
||||
* @param visibility - New visibility state
|
||||
* @param showSuccessAlert - Whether to show success notification
|
||||
* @returns Boolean indicating success
|
||||
*/
|
||||
async setVisibility(
|
||||
contact: Contact,
|
||||
visibility: boolean,
|
||||
@@ -596,7 +734,7 @@ export default class DIDView extends Vue {
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
console.error("Got strange result from setting visibility:", result);
|
||||
logger.error("Got strange result from setting visibility:", result);
|
||||
const message =
|
||||
(result.error as string) || "Could not set visibility on the server.";
|
||||
this.$notify(
|
||||
@@ -612,7 +750,12 @@ export default class DIDView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
// note that this is also in ContactView.vue
|
||||
/**
|
||||
* Checks current visibility status of contact on server
|
||||
* Updates local database with current status
|
||||
*
|
||||
* @param contact - Contact to check visibility for
|
||||
*/
|
||||
async checkVisibility(contact: Contact) {
|
||||
const url =
|
||||
this.apiServer +
|
||||
@@ -654,7 +797,7 @@ export default class DIDView extends Vue {
|
||||
3000,
|
||||
);
|
||||
} else {
|
||||
console.error("Got bad server response checking visibility:", resp);
|
||||
logger.error("Got bad server response checking visibility:", resp);
|
||||
const message = resp.data.error?.message || "Got bad server response.";
|
||||
this.$notify(
|
||||
{
|
||||
@@ -667,7 +810,7 @@ export default class DIDView extends Vue {
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Caught error from request to check visibility:", err);
|
||||
logger.error("Caught error from request to check visibility:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -18,17 +18,17 @@
|
||||
:style="{ visibility: isSearchVisible ? 'visible' : 'hidden' }"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchTerms"
|
||||
type="text"
|
||||
placeholder="Search…"
|
||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
|
||||
v-on:keyup.enter="searchSelected()"
|
||||
@keyup.enter="searchSelected()"
|
||||
/>
|
||||
<button
|
||||
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
||||
@click="searchSelected()"
|
||||
>
|
||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||
<font-awesome icon="magnifying-glass" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
:class="computedProjectsTabStyleClassNames()"
|
||||
@click="
|
||||
projects = [];
|
||||
userProfiles = [];
|
||||
@@ -46,7 +47,6 @@
|
||||
isPeopleActive = false;
|
||||
searchSelected();
|
||||
"
|
||||
v-bind:class="computedProjectsTabStyleClassNames()"
|
||||
>
|
||||
Projects
|
||||
</a>
|
||||
@@ -54,6 +54,7 @@
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
:class="computedPeopleTabStyleClassNames()"
|
||||
@click="
|
||||
projects = [];
|
||||
userProfiles = [];
|
||||
@@ -61,7 +62,6 @@
|
||||
isPeopleActive = true;
|
||||
searchSelected();
|
||||
"
|
||||
v-bind:class="computedPeopleTabStyleClassNames()"
|
||||
>
|
||||
People
|
||||
</a>
|
||||
@@ -75,6 +75,7 @@
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
:class="computedLocalTabStyleClassNames()"
|
||||
@click="
|
||||
projects = [];
|
||||
userProfiles = [];
|
||||
@@ -85,7 +86,6 @@
|
||||
tempSearchBox = null;
|
||||
searchLocal();
|
||||
"
|
||||
v-bind:class="computedLocalTabStyleClassNames()"
|
||||
>
|
||||
Nearby
|
||||
<!-- restore when the links don't jump around for different numbers
|
||||
@@ -101,6 +101,7 @@
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
:class="computedMappedTabStyleClassNames()"
|
||||
@click="
|
||||
projects = [];
|
||||
userProfiles = [];
|
||||
@@ -111,7 +112,6 @@
|
||||
searchTerms = '';
|
||||
tempSearchBox = null;
|
||||
"
|
||||
v-bind:class="computedMappedTabStyleClassNames()"
|
||||
>
|
||||
<!-- search is triggered when map component gets to "ready" state -->
|
||||
Mapped
|
||||
@@ -120,6 +120,7 @@
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
:class="computedRemoteTabStyleClassNames()"
|
||||
@click="
|
||||
projects = [];
|
||||
userProfiles = [];
|
||||
@@ -130,7 +131,6 @@
|
||||
tempSearchBox = null;
|
||||
searchAll();
|
||||
"
|
||||
v-bind:class="computedRemoteTabStyleClassNames()"
|
||||
>
|
||||
Anywhere
|
||||
<!-- restore when the links don't jump around for different numbers
|
||||
@@ -152,7 +152,7 @@
|
||||
class="ml-2 mt-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="$router.push({ name: 'search-area' })"
|
||||
>
|
||||
<fa icon="location-dot" class="fa-fw" />
|
||||
<font-awesome icon="location-dot" class="fa-fw" />
|
||||
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
|
||||
</button>
|
||||
</div>
|
||||
@@ -179,10 +179,10 @@
|
||||
|
||||
<!-- Loading Animation -->
|
||||
<div
|
||||
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||
v-if="isLoading"
|
||||
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||
>
|
||||
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="projects.length === 0 && userProfiles.length === 0"
|
||||
@@ -191,7 +191,10 @@
|
||||
<p class="text-lg text-slate-500">
|
||||
<span v-if="isLocalActive">
|
||||
<span v-if="searchBox"> None found in the selected area. </span>
|
||||
<!-- Otherwise there's no search area selected so we'll just leave the search box for them to click. -->
|
||||
<!--
|
||||
Otherwise there's no search area selected so we'll just leave the search box for them
|
||||
to click.
|
||||
-->
|
||||
</span>
|
||||
<span v-else-if="isAnywhereActive"
|
||||
>No projects were found with that search.</span
|
||||
@@ -205,19 +208,19 @@
|
||||
<!-- Projects List -->
|
||||
<template v-if="isProjectsActive">
|
||||
<li
|
||||
class="border-b border-slate-300"
|
||||
v-for="project in projects"
|
||||
:key="project.handleId"
|
||||
class="border-b border-slate-300"
|
||||
>
|
||||
<a
|
||||
@click="onClickLoadItem(project.handleId)"
|
||||
class="block py-4 flex gap-4 cursor-pointer"
|
||||
@click="onClickLoadItem(project.handleId)"
|
||||
>
|
||||
<div>
|
||||
<ProjectIcon
|
||||
:entityId="project.handleId"
|
||||
:iconSize="48"
|
||||
:imageUrl="project.image"
|
||||
:entity-id="project.handleId"
|
||||
:icon-size="48"
|
||||
:image-url="project.image"
|
||||
class="block border border-slate-300 rounded-md max-h-12 max-w-12"
|
||||
/>
|
||||
</div>
|
||||
@@ -225,7 +228,10 @@
|
||||
<div class="grow">
|
||||
<h2 class="text-base font-semibold">{{ project.name }}</h2>
|
||||
<div class="text-sm">
|
||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||
<font-awesome
|
||||
icon="user"
|
||||
class="fa-fw text-slate-400"
|
||||
></font-awesome>
|
||||
{{
|
||||
didInfo(
|
||||
project.issuerDid,
|
||||
@@ -243,17 +249,20 @@
|
||||
<!-- Profiles List -->
|
||||
<template v-else>
|
||||
<li
|
||||
class="border-b border-slate-300"
|
||||
v-for="profile in userProfiles"
|
||||
:key="profile.issuerDid"
|
||||
class="border-b border-slate-300"
|
||||
>
|
||||
<a
|
||||
@click="onClickLoadItem(profile?.rowId || '')"
|
||||
class="block py-4 flex gap-4 cursor-pointer"
|
||||
@click="onClickLoadItem(profile?.rowId || '')"
|
||||
>
|
||||
<div class="grow">
|
||||
<div class="text-sm">
|
||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||
<font-awesome
|
||||
icon="user"
|
||||
class="fa-fw text-slate-400"
|
||||
></font-awesome>
|
||||
{{
|
||||
didInfo(
|
||||
profile.issuerDid,
|
||||
@@ -273,7 +282,10 @@
|
||||
v-if="isAnywhereActive && profile.locLat && profile.locLon"
|
||||
class="mt-1 text-xs text-slate-500"
|
||||
>
|
||||
<fa icon="location-dot" class="fa-fw"></fa>
|
||||
<font-awesome
|
||||
icon="location-dot"
|
||||
class="fa-fw"
|
||||
></font-awesome>
|
||||
{{
|
||||
(profile.locLat > 0 ? "North" : "South") +
|
||||
" in " +
|
||||
@@ -313,14 +325,14 @@ import {
|
||||
} from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { BoundingBox } from "../db/tables/settings";
|
||||
import { PlanData } from "../interfaces";
|
||||
import {
|
||||
didInfo,
|
||||
errorStringForLog,
|
||||
getHeaders,
|
||||
PlanData,
|
||||
} from "../libs/endorserServer";
|
||||
import { OnboardPage, retrieveAccountDids } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
interface Tile {
|
||||
indexLat: number;
|
||||
indexLon: number;
|
||||
@@ -493,9 +505,9 @@ export default class DiscoverView extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
console.error("Error with search all:", e);
|
||||
logger.error("Error with search all:", e);
|
||||
// this sometimes gives different information
|
||||
console.error("Error with search all (error added): " + e);
|
||||
logger.error("Error with search all (error added): " + e);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -587,7 +599,7 @@ export default class DiscoverView extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
console.error("Error with search local:", e);
|
||||
logger.error("Error with search local:", e);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="cancelBack()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
>
|
||||
</h1>
|
||||
<textarea
|
||||
v-model="description"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
placeholder="What was received"
|
||||
v-model="description"
|
||||
/>
|
||||
<div class="flex flex-row justify-center">
|
||||
<span
|
||||
@@ -59,18 +59,18 @@
|
||||
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="amountInput === '0' ? null : decrement()"
|
||||
>
|
||||
<fa icon="chevron-left" />
|
||||
<font-awesome icon="chevron-left" />
|
||||
</div>
|
||||
<input
|
||||
v-model="amountInput"
|
||||
type="number"
|
||||
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
|
||||
v-model="amountInput"
|
||||
/>
|
||||
<div
|
||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="increment()"
|
||||
>
|
||||
<fa icon="chevron-right" />
|
||||
<font-awesome icon="chevron-right" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,14 +79,14 @@
|
||||
<a :href="imageUrl" target="_blank">
|
||||
<img :src="imageUrl" class="h-24 rounded-xl" />
|
||||
</a>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="trash-can"
|
||||
@click="confirmDeleteImage"
|
||||
class="text-red-500 fa-fw ml-8 mt-10"
|
||||
@click="confirmDeleteImage"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="camera"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
|
||||
@click="openImageDialog"
|
||||
@@ -101,11 +101,11 @@
|
||||
<div class="flex">
|
||||
<input
|
||||
v-if="giverDid && !providedByProject"
|
||||
v-model="providedByGiver"
|
||||
type="checkbox"
|
||||
class="h-6 w-6 mr-2"
|
||||
v-model="providedByGiver"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="square"
|
||||
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
@@ -117,7 +117,7 @@
|
||||
: "No named individual gave."
|
||||
}}
|
||||
</label>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="!giverDid || providedByProject"
|
||||
icon="info-circle"
|
||||
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
@@ -128,11 +128,11 @@
|
||||
<div class="flex">
|
||||
<input
|
||||
v-if="providerProjectId && !providedByGiver"
|
||||
v-model="providedByProject"
|
||||
type="checkbox"
|
||||
class="h-6 w-6 mr-2"
|
||||
v-model="providedByProject"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="square"
|
||||
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
@@ -144,7 +144,7 @@
|
||||
: "This was not provided by a project."
|
||||
}}
|
||||
</label>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="!providerProjectId || providedByGiver"
|
||||
icon="info-circle"
|
||||
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
@@ -154,7 +154,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink flex justify-center items-center">
|
||||
<fa icon="arrow-right" class="fa-fw h-7" />
|
||||
<font-awesome icon="arrow-right" class="fa-fw h-7" />
|
||||
</div>
|
||||
|
||||
<!-- Third Column for Recipient -->
|
||||
@@ -162,11 +162,11 @@
|
||||
<div class="flex">
|
||||
<input
|
||||
v-if="recipientDid && !givenToProject"
|
||||
v-model="givenToRecipient"
|
||||
type="checkbox"
|
||||
class="h-6 w-6 mr-2"
|
||||
v-model="givenToRecipient"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="square"
|
||||
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
@@ -178,7 +178,7 @@
|
||||
: "No individual benefitted."
|
||||
}}
|
||||
</label>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="!recipientDid || givenToProject"
|
||||
icon="info-circle"
|
||||
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
@@ -189,11 +189,11 @@
|
||||
<div class="flex">
|
||||
<input
|
||||
v-if="fulfillsProjectId && !givenToRecipient"
|
||||
v-model="givenToProject"
|
||||
type="checkbox"
|
||||
class="h-6 w-6 mr-2"
|
||||
v-model="givenToProject"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="square"
|
||||
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
@@ -205,7 +205,7 @@
|
||||
: "No project benefitted."
|
||||
}}
|
||||
</label>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="!fulfillsProjectId || givenToRecipient"
|
||||
icon="info-circle"
|
||||
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
|
||||
@@ -216,7 +216,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex">
|
||||
<input type="checkbox" class="h-6 w-6 mr-2" v-model="isTrade" />
|
||||
<input v-model="isTrade" type="checkbox" class="h-6 w-6 mr-2" />
|
||||
<label class="text-sm mt-1">This was a trade (not a gift)</label>
|
||||
</div>
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
|
||||
<p class="text-center mb-2 mt-6 italic">
|
||||
Sign & Send to publish to the world
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
@click="explainData()"
|
||||
@@ -261,25 +261,25 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { GenericCredWrapper, GiveVerifiableCredential } from "../interfaces";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
editAndSubmitGive,
|
||||
GenericCredWrapper,
|
||||
getHeaders,
|
||||
getPlanFromCache,
|
||||
GiveVerifiableCredential,
|
||||
hydrateGive,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -290,6 +290,8 @@ import { retrieveAccountDids } from "../libs/util";
|
||||
})
|
||||
export default class GiftedDetails extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
@@ -322,9 +324,9 @@ export default class GiftedDetails extends Vue {
|
||||
|
||||
async mounted() {
|
||||
try {
|
||||
this.prevCredToEdit = (this.$route as Router).query["prevCredToEdit"]
|
||||
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
|
||||
? (JSON.parse(
|
||||
(this.$route as Router).query["prevCredToEdit"],
|
||||
this.$route.query["prevCredToEdit"] as string,
|
||||
) as GenericCredWrapper<GiveVerifiableCredential>)
|
||||
: undefined;
|
||||
} catch (error) {
|
||||
@@ -341,24 +343,23 @@ export default class GiftedDetails extends Vue {
|
||||
|
||||
const prevAmount = this.prevCredToEdit?.claim?.object?.amountOfThisGood;
|
||||
this.amountInput =
|
||||
(this.$route as Router).query["amountInput"] ||
|
||||
(this.$route.query["amountInput"] as string) ||
|
||||
(prevAmount ? String(prevAmount) : "") ||
|
||||
this.amountInput;
|
||||
this.description =
|
||||
(this.$route as Router).query["description"] ||
|
||||
(this.$route.query["description"] as string) ||
|
||||
this.prevCredToEdit?.claim?.description ||
|
||||
this.description;
|
||||
this.destinationPathAfter = (this.$route as Router).query[
|
||||
"destinationPathAfter"
|
||||
];
|
||||
this.giverDid = ((this.$route as Router).query["giverDid"] ||
|
||||
this.prevCredToEdit?.claim?.agent?.identifier ||
|
||||
this.destinationPathAfter =
|
||||
(this.$route.query["destinationPathAfter"] as string) || "";
|
||||
this.giverDid = ((this.$route.query["giverDid"] as string) ||
|
||||
(this.prevCredToEdit?.claim?.agent as unknown as { identifier: string })
|
||||
?.identifier ||
|
||||
this.giverDid) as string;
|
||||
this.giverName =
|
||||
((this.$route as Router).query["giverName"] as string) || "";
|
||||
this.giverName = (this.$route.query["giverName"] as string) || "";
|
||||
this.hideBackButton =
|
||||
(this.$route as Router).query["hideBackButton"] === "true";
|
||||
this.message = ((this.$route as Router).query["message"] as string) || "";
|
||||
(this.$route.query["hideBackButton"] as string) === "true";
|
||||
this.message = (this.$route.query["message"] as string) || "";
|
||||
|
||||
// find any offer ID
|
||||
const fulfills = this.prevCredToEdit?.claim?.fulfills;
|
||||
@@ -368,7 +369,7 @@ export default class GiftedDetails extends Vue {
|
||||
? [fulfills]
|
||||
: [];
|
||||
const offer = fulfillsArray.find((rec) => rec["@type"] === "Offer");
|
||||
this.offerId = ((this.$route as Router).query["offerId"] ||
|
||||
this.offerId = ((this.$route.query["offerId"] as string) ||
|
||||
offer?.identifier ||
|
||||
this.offerId) as string;
|
||||
|
||||
@@ -378,7 +379,7 @@ export default class GiftedDetails extends Vue {
|
||||
);
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
this.fulfillsProjectId =
|
||||
((this.$route as Router).query["fulfillsProjectId"] ||
|
||||
((this.$route.query["fulfillsProjectId"] as string) ||
|
||||
fulfillsProject?.identifier ||
|
||||
this.fulfillsProjectId) as string;
|
||||
|
||||
@@ -392,40 +393,38 @@ export default class GiftedDetails extends Vue {
|
||||
const providerProject = providerArray.find(
|
||||
(rec) => rec["@type"] === "PlanAction",
|
||||
);
|
||||
this.providerProjectId = ((this.$route as Router).query[
|
||||
this.providerProjectId = ((this.$route.query[
|
||||
"providerProjectId"
|
||||
] ||
|
||||
] as string) ||
|
||||
providerProject?.identifier ||
|
||||
this.providerProjectId) as string;
|
||||
|
||||
this.recipientDid = ((this.$route as Router).query["recipientDid"] ||
|
||||
this.recipientDid = ((this.$route.query["recipientDid"] as string) ||
|
||||
this.prevCredToEdit?.claim?.recipient?.identifier) as string;
|
||||
this.recipientName =
|
||||
((this.$route as Router).query["recipientName"] as string) || "";
|
||||
this.unitCode = ((this.$route as Router).query["unitCode"] ||
|
||||
this.recipientName = (this.$route.query["recipientName"] as string) || "";
|
||||
this.unitCode = ((this.$route.query["unitCode"] as string) ||
|
||||
this.prevCredToEdit?.claim?.object?.unitCode ||
|
||||
this.unitCode) as string;
|
||||
|
||||
this.imageUrl =
|
||||
((this.$route as Router).query["imageUrl"] as string) ||
|
||||
this.imageUrl = ((this.$route.query["imageUrl"] as string) ||
|
||||
this.prevCredToEdit?.claim?.image ||
|
||||
localStorage.getItem("imageUrl") ||
|
||||
this.imageUrl;
|
||||
this.imageUrl) as string;
|
||||
|
||||
// this is an endpoint for sharing project info to highlight something given
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target
|
||||
if ((this.$route as Router).query["shareTitle"]) {
|
||||
if (this.$route.query["shareTitle"] as string) {
|
||||
this.description =
|
||||
((this.$route as Router).query["shareTitle"] as string) +
|
||||
((this.$route.query["shareTitle"] as string) || "") +
|
||||
(this.description ? "\n" + this.description : "");
|
||||
}
|
||||
if ((this.$route as Router).query["shareText"]) {
|
||||
if (this.$route.query["shareText"] as string) {
|
||||
this.description =
|
||||
(this.description ? this.description + "\n" : "") +
|
||||
((this.$route as Router).query["shareText"] as string);
|
||||
((this.$route.query["shareText"] as string) || "");
|
||||
}
|
||||
if ((this.$route as Router).query["shareUrl"]) {
|
||||
this.imageUrl = (this.$route as Router).query["shareUrl"] as string;
|
||||
if (this.$route.query["shareUrl"] as string) {
|
||||
this.imageUrl = this.$route.query["shareUrl"] as string;
|
||||
}
|
||||
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
@@ -551,7 +550,7 @@ export default class GiftedDetails extends Vue {
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
) {
|
||||
console.log(
|
||||
logger.log(
|
||||
"Using shared image API server, so only users on that server can play with images.",
|
||||
);
|
||||
}
|
||||
@@ -565,7 +564,7 @@ export default class GiftedDetails extends Vue {
|
||||
// don't bother with a notification
|
||||
// (either they'll simply continue or they're canceling and going back)
|
||||
} else {
|
||||
console.error("Problem deleting image:", response);
|
||||
logger.error("Problem deleting image:", response);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -582,10 +581,10 @@ export default class GiftedDetails extends Vue {
|
||||
localStorage.removeItem("imageUrl");
|
||||
this.imageUrl = "";
|
||||
} catch (error) {
|
||||
console.error("Error deleting image:", error);
|
||||
logger.error("Error deleting image:", error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any).response.status === 404) {
|
||||
console.log("Weird: the image was already deleted.", error);
|
||||
logger.log("Weird: the image was already deleted.", error);
|
||||
|
||||
localStorage.removeItem("imageUrl");
|
||||
this.imageUrl = "";
|
||||
@@ -818,7 +817,7 @@ export default class GiftedDetails extends Vue {
|
||||
this.isGiveCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
console.error("Error with give creation result:", result);
|
||||
logger.error("Error with give creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -847,7 +846,7 @@ export default class GiftedDetails extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Error with give recordation caught:", error);
|
||||
logger.error("Error with give recordation caught:", error);
|
||||
const errorMessage =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -42,16 +42,21 @@
|
||||
<h2 class="text-xl font-semibold mt-4">New Activity Notifications</h2>
|
||||
<div>
|
||||
<p>
|
||||
The New Activity Notification will be sent to you when there is new, relevant activity for you.
|
||||
The New Activity Notification will be sent to you when there is new, relevant activity
|
||||
for you.
|
||||
It will only trigger if something involves you or a project of interest; it will not
|
||||
bug you for other, general activity.
|
||||
</p>
|
||||
<p>
|
||||
This type is not as reliable as a Reminder Notification because mobile devices often suppress
|
||||
such notifications to save battery. (If you want to quickly check for relevant activity daily,
|
||||
use the Reminder Notification and open the app and look for a large green button that points out new
|
||||
activity that is personal to you. We are working on other ways to notify you more
|
||||
reliably -- <router-link class="text-blue-500" to="/help">go here to follow us or contact us</router-link>.)
|
||||
This type is not as reliable as a Reminder Notification because mobile devices often
|
||||
suppress such notifications to save battery. (If you want to quickly check for relevant
|
||||
activity daily, use the Reminder Notification and open the app and look for a large green
|
||||
button that points out new activity that is personal to you. We are working on other
|
||||
ways to notify you more reliably.
|
||||
<router-link class="text-blue-500" to="/help">
|
||||
go here to follow us or contact us
|
||||
<font-awesome icon="chevron-right" class="fa-fw"></font-awesome>
|
||||
</router-link>.)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
<p>
|
||||
If this works then you're all set.
|
||||
<button
|
||||
@click="sendTestWebPushMessage(true)"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2"
|
||||
@click="sendTestWebPushMessage(true)"
|
||||
>
|
||||
Send Yourself a Test Web Push Message (Through Push Server but
|
||||
Skipping Client Filter)
|
||||
@@ -140,7 +140,7 @@
|
||||
class="text-blue-500"
|
||||
target="_blank"
|
||||
>
|
||||
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
here <font-awesome icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
class="text-blue-500"
|
||||
target="_blank"
|
||||
>
|
||||
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
here <font-awesome icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,7 +199,7 @@
|
||||
<p>
|
||||
Of course, you'll want to back up all your data first -- all seeds as
|
||||
well as the contacts & settings -- on the Profile
|
||||
<fa icon="circle-user" /> page.
|
||||
<font-awesome icon="circle-user" /> page.
|
||||
</p>
|
||||
<p>
|
||||
Here are instructions to uninstall the app and clear out caches and storage.
|
||||
@@ -246,8 +246,8 @@
|
||||
|
||||
<h2 class="text-xl font-semibold mt-4">Tests</h2>
|
||||
<button
|
||||
@click="showTestNotification()"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
||||
@click="showTestNotification()"
|
||||
>
|
||||
Send Test Notification Directly to Device (Not Through Push Server)
|
||||
</button>
|
||||
@@ -259,8 +259,8 @@
|
||||
</p>
|
||||
|
||||
<button
|
||||
@click="alertWebPushSubscription()"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
||||
@click="alertWebPushSubscription()"
|
||||
>
|
||||
Show Web Push Subscription Info
|
||||
</button>
|
||||
@@ -272,8 +272,8 @@
|
||||
</p>
|
||||
|
||||
<button
|
||||
@click="sendTestWebPushMessage(true)"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
||||
@click="sendTestWebPushMessage(true)"
|
||||
>
|
||||
Send Yourself a Test Web Push Message (Through Push Server but Skipping
|
||||
Client Filter)
|
||||
@@ -285,8 +285,8 @@
|
||||
</p>
|
||||
|
||||
<button
|
||||
@click="sendTestWebPushMessage()"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4 mb-2"
|
||||
@click="sendTestWebPushMessage()"
|
||||
>
|
||||
Send Yourself a Test Web Push Message (Through Push Server and Client
|
||||
Filter)
|
||||
@@ -313,11 +313,12 @@ import { DIRECT_PUSH_TITLE, sendTestThroughPushServer } from "../libs/util";
|
||||
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
|
||||
import { db } from "../db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
|
||||
import { Router } from "vue-router";
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({ components: { PushNotificationPermission, QuickNav } })
|
||||
export default class HelpNotificationsView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
$router!: Router;
|
||||
subscriptionJSON?: PushSubscriptionJSON;
|
||||
|
||||
async mounted() {
|
||||
@@ -326,7 +327,7 @@ export default class HelpNotificationsView extends Vue {
|
||||
const fullSub = await registration?.pushManager.getSubscription();
|
||||
this.subscriptionJSON = fullSub?.toJSON();
|
||||
} catch (error) {
|
||||
console.error("Mount error:", error);
|
||||
logger.error("Mount error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +369,7 @@ export default class HelpNotificationsView extends Vue {
|
||||
5000,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Got an error sending test notification:", error);
|
||||
logger.error("Got an error sending test notification:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -401,7 +402,7 @@ export default class HelpNotificationsView extends Vue {
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Got a notification error:", error);
|
||||
logger.error("Got a notification error:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
:to="{ name: 'invite-one' }"
|
||||
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||
>
|
||||
<fa icon="envelope-open-text" class="fa-fw text-xl"
|
||||
<font-awesome icon="envelope-open-text" class="fa-fw text-xl"
|
||||
/></router-link>
|
||||
</p>
|
||||
<p>Then watch that page to see when they accept their invite.</p>
|
||||
<p>
|
||||
(That page is also reachable from the Contacts <fa icon="users" /> page
|
||||
though the invitation <fa icon="envelope-open-text" /> icon.)
|
||||
(That page is also reachable from the Contacts
|
||||
<font-awesome icon="users" /> page though the invitation
|
||||
<font-awesome icon="envelope-open-text" /> icon.)
|
||||
</p>
|
||||
|
||||
<h1 class="mt-4 font-bold text-xl">Next Steps</h1>
|
||||
@@ -35,12 +36,13 @@
|
||||
<h1 class="font-bold text-xl">Without a backup, you can lose data.</h1>
|
||||
<div>
|
||||
<p>
|
||||
Exporting backups (from the Account <fa icon="circle-user" /> screen)
|
||||
is important for the case where they lose their device. This is
|
||||
especially true for the Identifier Seed: that is theirs and and theirs
|
||||
alone, and currently nobody else can recover it if they lose it. The
|
||||
good thing is that anyone can create a new account and simply inform
|
||||
their network of their new ID.
|
||||
Exporting backups (from the Account
|
||||
<font-awesome icon="circle-user" /> screen) is important for the case
|
||||
where they lose their device. This is especially true for the
|
||||
Identifier Seed: that is theirs and and theirs alone, and currently
|
||||
nobody else can recover it if they lose it. The good thing is that
|
||||
anyone can create a new account and simply inform their network of
|
||||
their new ID.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +56,7 @@
|
||||
<h1 class="font-bold text-xl">Add Contact & Register</h1>
|
||||
<p>
|
||||
You share even more information such as your picture and name when
|
||||
you share with your QR code at these links: <fa icon="qrcode" />
|
||||
you share with your QR code at these links: <font-awesome icon="qrcode" />
|
||||
</p>
|
||||
<p>
|
||||
Scanning
|
||||
@@ -70,14 +72,14 @@
|
||||
</p>
|
||||
<p>
|
||||
2) Scan their QR, or have them tap on it to copy their info and send it to you.
|
||||
Then you can add them to your Contacts <fa icon="users" />
|
||||
Then you can add them to your Contacts <font-awesome icon="users" />
|
||||
</p>
|
||||
<p>
|
||||
3) You can register them at their info page <fa icon="circle-info" />
|
||||
and click on the register button <fa icon="person-circle-question" />
|
||||
3) You can register them at their info page <font-awesome icon="circle-info" />
|
||||
and click on the register button <font-awesome icon="person-circle-question" />
|
||||
</p>
|
||||
<p>
|
||||
4) Add yourself to their Contacts <fa icon="users" />
|
||||
4) Add yourself to their Contacts <font-awesome icon="users" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -94,9 +96,10 @@
|
||||
<h1 class="font-bold text-xl">Enable Notifications</h1>
|
||||
<div>
|
||||
<p>
|
||||
Enable notifications from the Account page <fa icon="circle-user" />.
|
||||
Enable notifications from the Account page <font-awesome icon="circle-user" />.
|
||||
Those notifications might show up on the device depending on your settings.
|
||||
For the most reliable habits, set an alarm or do some other ritual to record gratitude every day.
|
||||
For the most reliable habits, set an alarm or do some other ritual to record gratitude
|
||||
every day.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw" />
|
||||
<font-awesome icon="chevron-left" class="fa-fw" />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
<p class="ml-4">
|
||||
If you'd like to see the page-by-page help,
|
||||
<span
|
||||
@click="unsetFinishedOnboarding()"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
@click="unsetFinishedOnboarding()"
|
||||
>click here</span>.
|
||||
</p>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<h2 class="text-xl font-semibold">I want to know more because...</h2>
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li class="p-2">
|
||||
<div @click="showAlpha = !showAlpha" class="text-blue-500">... I'm a member of Alpha chat.</div>
|
||||
<div class="text-blue-500" @click="showAlpha = !showAlpha">... I'm a member of Alpha chat.</div>
|
||||
<div v-if="showAlpha">
|
||||
<p>
|
||||
This is a project for public benefit. You are invited to add your gratitude
|
||||
@@ -98,7 +98,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2">
|
||||
<div @click="showGroup = !showGroup" class="text-blue-500">... I want to find a group I'll enjoy working with.</div>
|
||||
<div class="text-blue-500" @click="showGroup = !showGroup">... I want to find a group I'll enjoy working with.</div>
|
||||
<div v-if="showGroup">
|
||||
<p>
|
||||
This app encourages people to offer small bits of time to one another. It's a way to
|
||||
@@ -114,7 +114,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2">
|
||||
<div @click="showCommunity = !showCommunity" class="text-blue-500">... I want to participate in community projects.</div>
|
||||
<div class="text-blue-500" @click="showCommunity = !showCommunity">... I want to participate in community projects.</div>
|
||||
<div v-if="showCommunity">
|
||||
<p>
|
||||
These are mostly at the beginning stages, so any of them will appreciate your offers that show interest.
|
||||
@@ -127,7 +127,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2">
|
||||
<div @click="showVerifiable = !showVerifiable" class="text-blue-500">... I want to build with verifiable, private data.</div>
|
||||
<div class="text-blue-500" @click="showVerifiable = !showVerifiable">... I want to build with verifiable, private data.</div>
|
||||
<div v-if="showVerifiable">
|
||||
<p>
|
||||
Make your claims and get others to confirm them. Then you can use the API to pull your copy of all that
|
||||
@@ -153,7 +153,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2">
|
||||
<div @click="showGovernance = !showGovernance" class="text-blue-500">... I want to build governance organically.</div>
|
||||
<div class="text-blue-500" @click="showGovernance = !showGovernance">... I want to build governance organically.</div>
|
||||
<div v-if="showGovernance">
|
||||
<p>
|
||||
This requires motivated, dedicated citizens. The good thing is that dedication the primary ingredient;
|
||||
@@ -172,7 +172,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li class="p-2">
|
||||
<div @click="showBasics = !showBasics" class="text-blue-500">... I want to supply life's basics freely.</div>
|
||||
<div class="text-blue-500" @click="showBasics = !showBasics">... I want to supply life's basics freely.</div>
|
||||
<div v-if="showBasics">
|
||||
<p>
|
||||
This platform is not optimal for balancing needs and resources at this point,
|
||||
@@ -191,7 +191,7 @@
|
||||
<h2 class="text-xl font-semibold">How do I get started?</h2>
|
||||
<p>
|
||||
Someone -- like the person who told you about this app -- needs to register you
|
||||
on the Contacts <fa icon="users" class="fa-fw" /> page.
|
||||
on the Contacts <font-awesome icon="users" class="fa-fw" /> page.
|
||||
If you heard about this from our outreach, feel free to contact us (below) for a chat.
|
||||
After someone registers you, you can register others.
|
||||
</p>
|
||||
@@ -219,7 +219,7 @@
|
||||
</p>
|
||||
<p>
|
||||
If they are not nearby to scan QR codes, you each can tap on the QR code
|
||||
and paste it into the text box on the Contacts <fa icon="users" class="fa-fw" /> page.
|
||||
and paste it into the text box on the Contacts <font-awesome icon="users" class="fa-fw" /> page.
|
||||
</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">
|
||||
@@ -244,7 +244,7 @@
|
||||
</h2>
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>
|
||||
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
|
||||
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page.
|
||||
</li>
|
||||
<li>
|
||||
Click on "Backup Identifier Seed" and follow the instructions.
|
||||
@@ -260,7 +260,7 @@
|
||||
</h2>
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>
|
||||
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
|
||||
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page.
|
||||
</li>
|
||||
<li>
|
||||
Click on "Download Settings...". That will save a file to your
|
||||
@@ -274,7 +274,7 @@
|
||||
</h2>
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>
|
||||
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
|
||||
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
|
||||
tap on your image, and save it.
|
||||
</li>
|
||||
</ul>
|
||||
@@ -315,7 +315,7 @@
|
||||
</h2>
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>
|
||||
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
|
||||
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
|
||||
click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
|
||||
Beware that this will erase your existing contact & settings.
|
||||
</li>
|
||||
@@ -384,7 +384,7 @@
|
||||
</h2>
|
||||
<p>
|
||||
There is an "Advanced" section at the bottom of the Profile
|
||||
<fa icon="circle-user" /> page.
|
||||
<font-awesome icon="circle-user" /> page.
|
||||
</p>
|
||||
<p>
|
||||
There is even more functionality in a mobile app (and more
|
||||
@@ -402,8 +402,8 @@
|
||||
because they have not given you permission to see their information. Ask
|
||||
them to add you to their contact list, and ask specifically to make sure
|
||||
the eye next to your name is open like this
|
||||
<fa icon="eye" class="fa-fw" /> and not closed like this
|
||||
<fa icon="eye-slash" class="fa-fw" />.
|
||||
<font-awesome icon="eye" class="fa-fw" /> and not closed like this
|
||||
<font-awesome icon="eye-slash" class="fa-fw" />.
|
||||
</p>
|
||||
<p>
|
||||
Sometimes the reason you don't see something is because the search
|
||||
@@ -444,7 +444,7 @@
|
||||
</li>
|
||||
<li>
|
||||
There may be a problem with your identity. Go to the Identity
|
||||
<fa icon="circle-user" class="fa-fw" /> page, then "Advanced", and "Switch Identifier"
|
||||
<font-awesome icon="circle-user" class="fa-fw" /> page, then "Advanced", and "Switch Identifier"
|
||||
and you may see helpful info there. If it shows a problem, try adding your identifier again.
|
||||
</li>
|
||||
<li>
|
||||
@@ -505,7 +505,7 @@
|
||||
<ul class="list-disc list-outside ml-4">
|
||||
<li>
|
||||
If using notifications, a server stores push token data. That can be revoked at any time
|
||||
by disabling notifications on the Profile <fa icon="circle-user" class="fa-fw" /> page.
|
||||
by disabling notifications on the Profile <font-awesome icon="circle-user" class="fa-fw" /> page.
|
||||
</li>
|
||||
<li>
|
||||
If sending images, a server stores them, too. They can be removed by editing the claim
|
||||
@@ -529,17 +529,17 @@
|
||||
If you have skills, contact us below.
|
||||
If you have Bitcoin, donate to
|
||||
<button
|
||||
class="text-blue-500 ml-2"
|
||||
@click="
|
||||
doCopyTwoSecRedo(
|
||||
'bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma',
|
||||
() => (showDidCopy = !showDidCopy)
|
||||
)
|
||||
"
|
||||
class="text-blue-500 ml-2"
|
||||
>
|
||||
bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma
|
||||
<fa v-show="!showDidCopy" icon="copy" class="text-sm text-slate-400 fa-fw" />
|
||||
<fa v-show="showDidCopy" icon="circle-check" class="text-sm text-green-500 fa-fw"/>
|
||||
<font-awesome v-show="!showDidCopy" icon="copy" class="text-sm text-slate-400 fa-fw" />
|
||||
<font-awesome v-show="showDidCopy" icon="circle-check" class="text-sm text-green-500 fa-fw"/>
|
||||
</button>
|
||||
You can donate online via
|
||||
<a href="https://www.patreon.com/TimeSafari" target="_blank" class="text-blue-500">Patreon here</a>.
|
||||
@@ -588,6 +588,7 @@ import {
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class Help extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
package = Package;
|
||||
commitHash = import.meta.env.VITE_GIT_HASH;
|
||||
@@ -614,7 +615,7 @@ export default class Help extends Vue {
|
||||
finishedOnboarding: false,
|
||||
});
|
||||
}
|
||||
(this.$router as Router).push({ name: "home" });
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
|
||||
<OnboardingDialog ref="onboardingDialog" />
|
||||
|
||||
<!-- prompt to install notifications with notificationsSupported, which we're making an advanced feature -->
|
||||
<!--
|
||||
prompt to install notifications with notificationsSupported, which we're making an advanced
|
||||
feature
|
||||
-->
|
||||
<div class="mb-8 mt-8">
|
||||
<div
|
||||
v-if="false"
|
||||
@@ -28,7 +31,7 @@
|
||||
width="30"
|
||||
style="display: inline; margin: 0 5px; vertical-align: middle"
|
||||
/>and then "Add to Home Screen"
|
||||
<fa icon="square-plus" title="Apple 'Add' icon" />
|
||||
<font-awesome icon="square-plus" title="Apple 'Add' icon" />
|
||||
and go click on that new app.
|
||||
</span>
|
||||
<span
|
||||
@@ -36,12 +39,8 @@
|
||||
>
|
||||
You should see a prompt to install, or you can click on the
|
||||
top-right dots
|
||||
<fa
|
||||
icon="ellipsis-vertical"
|
||||
title="vertical ellipsis"
|
||||
class="fa-fw"
|
||||
/>
|
||||
and then "Install"<img
|
||||
<font-awesome icon="ellipsis-vertical" title="vertical ellipsis" />
|
||||
/> and then "Install"<img
|
||||
src="../assets/help/install-android-chrome.png"
|
||||
alt="Android 'install' icon"
|
||||
width="30"
|
||||
@@ -73,14 +72,16 @@
|
||||
<div class="mb-8">
|
||||
<div v-if="isCreatingIdentifier">
|
||||
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
||||
<fa icon="spinner" class="fa-spin-pulse" />
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
Loading…
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- !isCreatingIdentifier -->
|
||||
<!-- They should have an identifier, even if it's an auto-generated one that they'll never use. -->
|
||||
<!--
|
||||
They should have an identifier, even if it's an auto-generated one that they'll never use.
|
||||
-->
|
||||
<div class="mb-4">
|
||||
<div
|
||||
v-if="!isRegistered"
|
||||
@@ -91,8 +92,8 @@
|
||||
To share, someone must register you.
|
||||
<div class="block text-center">
|
||||
<button
|
||||
@click="showNameThenIdDialog()"
|
||||
class="text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
||||
@click="showNameThenIdDialog()"
|
||||
>
|
||||
Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
|
||||
info
|
||||
@@ -116,10 +117,10 @@
|
||||
<div class="flex">
|
||||
<h2 class="text-xl font-bold">What have you seen someone do?</h2>
|
||||
<button
|
||||
@click="openGiftedPrompts()"
|
||||
class="ml-2 block text-xs text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
|
||||
@click="openGiftedPrompts()"
|
||||
>
|
||||
<fa icon="lightbulb" class="fa-fw" />
|
||||
<font-awesome icon="lightbulb" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -147,7 +148,7 @@
|
||||
>
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:iconSize="64"
|
||||
:icon-size="64"
|
||||
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
@@ -181,7 +182,7 @@
|
||||
class="absolute right-6 bottom-0 transform translate-y-1/2 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
|
||||
@click="openDialog()"
|
||||
>
|
||||
<fa icon="plus" class="fa-fw" />
|
||||
<font-awesome icon="plus" class="fa-fw" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -192,12 +193,12 @@
|
||||
Latest Activity
|
||||
<button @click="openFeedFilters()">
|
||||
<span class="text-xs text-white">
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="resultsAreFiltered()"
|
||||
icon="filter"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="filter"
|
||||
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md"
|
||||
@@ -208,8 +209,8 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
@click="goToActivityToUserPage()"
|
||||
class="border-t p-2 border-slate-300"
|
||||
@click="goToActivityToUserPage()"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
@@ -251,13 +252,13 @@
|
||||
<InfiniteScroll @reached-bottom="loadMoreGives">
|
||||
<ul id="listLatestActivity" class="border-t border-slate-300">
|
||||
<li
|
||||
class="border-b border-slate-300 py-2"
|
||||
v-for="record in feedData"
|
||||
:key="record.jwtId"
|
||||
class="border-b border-slate-300 py-2"
|
||||
>
|
||||
<div
|
||||
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
|
||||
v-if="record.jwtId == feedLastViewedClaimId"
|
||||
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
|
||||
>
|
||||
You've already seen all the following
|
||||
</div>
|
||||
@@ -265,7 +266,7 @@
|
||||
<div class="grid grid-cols-12">
|
||||
<span class="pt-1 col-span-1 justify-self-start">
|
||||
<span>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="circle-user"
|
||||
:class="
|
||||
computeKnownPersonIconStyleClassNames(
|
||||
@@ -274,7 +275,7 @@
|
||||
"
|
||||
@click="toastUser('This involves your contacts.')"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="gift"
|
||||
class="pl-3 text-slate-500"
|
||||
@click="toastUser('This is a gift.')"
|
||||
@@ -282,44 +283,11 @@
|
||||
</span>
|
||||
</span>
|
||||
<span class="col-span-10 justify-self-stretch overflow-hidden">
|
||||
<!-- show giver and/or receiver profiles... which seemed like a good idea but actually adds clutter
|
||||
<span
|
||||
v-if="
|
||||
record.giver.profileImageUrl ||
|
||||
record.receiver.profileImageUrl
|
||||
"
|
||||
>
|
||||
<EntityIcon
|
||||
v-if="record.agentDid !== activeDid"
|
||||
:icon-size="32"
|
||||
:profile-image-url="record.giver.profileImageUrl"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
|
||||
/>
|
||||
<fa
|
||||
v-if="
|
||||
record.agentDid !== activeDid &&
|
||||
record.recipientDid !== activeDid &&
|
||||
!record.fulfillsPlanHandleId
|
||||
"
|
||||
icon="ellipsis"
|
||||
class="text-slate"
|
||||
/>
|
||||
<EntityIcon
|
||||
v-if="
|
||||
record.recipientDid !== activeDid &&
|
||||
!record.fulfillsPlanHandleId
|
||||
"
|
||||
:iconSize="32"
|
||||
:profile-image-url="record.receiver.profileImageUrl"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md ml-1"
|
||||
/>
|
||||
</span>
|
||||
-->
|
||||
<span class="pl-2 block break-words">
|
||||
{{ giveDescription(record) }}
|
||||
</span>
|
||||
<a @click="onClickLoadClaim(record.jwtId)">
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="pl-2 text-slate-500 cursor-pointer"
|
||||
/>
|
||||
@@ -333,7 +301,7 @@
|
||||
encodeURIComponent(record.fulfillsPlanHandleId)
|
||||
"
|
||||
>
|
||||
<fa icon="hammer" class="text-blue-500" />
|
||||
<font-awesome icon="hammer" class="text-blue-500" />
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="record.providerPlanHandleId"
|
||||
@@ -342,7 +310,7 @@
|
||||
encodeURIComponent(record.providerPlanHandleId)
|
||||
"
|
||||
>
|
||||
<fa icon="hammer" class="text-blue-500" />
|
||||
<font-awesome icon="hammer" class="text-blue-500" />
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
@@ -364,7 +332,7 @@
|
||||
</InfiniteScroll>
|
||||
<div v-if="isFeedLoading">
|
||||
<p class="text-slate-500 text-center italic mt-4 mb-4">
|
||||
<fa icon="spinner" class="fa-spin-pulse" /> Loading…
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" /> Loading…
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="!isFeedLoading && feedData.length === 0">
|
||||
@@ -378,9 +346,9 @@
|
||||
<ChoiceButtonDialog ref="choiceButtonDialog" />
|
||||
|
||||
<ImageViewer
|
||||
v-model:is-open="isImageViewerOpen"
|
||||
:image-url="selectedImage"
|
||||
:image-data="selectedImageData"
|
||||
v-model:is-open="isImageViewerOpen"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -427,17 +395,16 @@ import {
|
||||
getNewOffersToUser,
|
||||
getNewOffersToUserProjects,
|
||||
getPlanFromCache,
|
||||
GiveSummaryRecord,
|
||||
} from "../libs/endorserServer";
|
||||
import {
|
||||
generateSaveAndActivateIdentity,
|
||||
retrieveAccountDids,
|
||||
GiverReceiverInputInfo,
|
||||
OnboardPage,
|
||||
registerSaveAndActivatePasskey,
|
||||
} from "../libs/util";
|
||||
|
||||
import { GiveSummaryRecord } from "../interfaces";
|
||||
interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
||||
jwtId: string;
|
||||
giver: {
|
||||
displayName: string;
|
||||
known: boolean;
|
||||
@@ -452,7 +419,16 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
|
||||
profileImageUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
/**
|
||||
* HomeView - Main view component for the application's home page
|
||||
*
|
||||
* Workflow:
|
||||
* 1. On mount, initializes user identity, settings, and data
|
||||
* 2. Handles user registration status
|
||||
* 3. Manages feed of activities and offers
|
||||
* 4. Provides interface for creating and viewing claims
|
||||
*/
|
||||
@Component({
|
||||
components: {
|
||||
EntityIcon,
|
||||
@@ -506,127 +482,208 @@ export default class HomeView extends Vue {
|
||||
isImageViewerOpen = false;
|
||||
imageCache: Map<string, Blob | null> = new Map();
|
||||
|
||||
/**
|
||||
* Initializes the component on mount
|
||||
* Sequence:
|
||||
* 1. Initialize identity (create if needed)
|
||||
* 2. Load user settings
|
||||
* 3. Load contacts
|
||||
* 4. Check registration status
|
||||
* 5. Load feed data
|
||||
* 6. Load new offers
|
||||
* 7. Check onboarding status
|
||||
*/
|
||||
async mounted() {
|
||||
try {
|
||||
try {
|
||||
this.allMyDids = await retrieveAccountDids();
|
||||
if (this.allMyDids.length === 0) {
|
||||
this.isCreatingIdentifier = true;
|
||||
const newDid = await generateSaveAndActivateIdentity();
|
||||
this.isCreatingIdentifier = false;
|
||||
this.allMyDids = [newDid];
|
||||
}
|
||||
} catch (error) {
|
||||
// continue because we want the feed to work, even anonymously
|
||||
logConsoleAndDb(
|
||||
"Error retrieving all account DIDs on home page:" + error,
|
||||
true,
|
||||
);
|
||||
// some other piece will display an error about personal info
|
||||
await this.initializeIdentity();
|
||||
await this.loadSettings();
|
||||
await this.loadContacts();
|
||||
await this.checkRegistrationStatus();
|
||||
await this.loadFeedData();
|
||||
await this.loadNewOffers();
|
||||
await this.checkOnboarding();
|
||||
} catch (err: unknown) {
|
||||
this.handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes user identity
|
||||
* - Retrieves existing DIDs
|
||||
* - Creates new DID if none exists
|
||||
* @throws Logs error if DID retrieval fails
|
||||
*/
|
||||
private async initializeIdentity() {
|
||||
try {
|
||||
this.allMyDids = await retrieveAccountDids();
|
||||
if (this.allMyDids.length === 0) {
|
||||
this.isCreatingIdentifier = true;
|
||||
const newDid = await generateSaveAndActivateIdentity();
|
||||
this.isCreatingIdentifier = false;
|
||||
this.allMyDids = [newDid];
|
||||
}
|
||||
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
this.feedLastViewedClaimId = settings.lastViewedClaimId;
|
||||
this.givenName = settings.firstName || "";
|
||||
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
|
||||
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
|
||||
this.lastAckedOfferToUserProjectsJwtId =
|
||||
settings.lastAckedOfferToUserProjectsJwtId;
|
||||
this.searchBoxes = settings.searchBoxes || [];
|
||||
this.showShortcutBvc = !!settings.showShortcutBvc;
|
||||
|
||||
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
|
||||
|
||||
if (!settings.finishedOnboarding) {
|
||||
(this.$refs.onboardingDialog as OnboardingDialog).open(
|
||||
OnboardPage.Home,
|
||||
);
|
||||
}
|
||||
|
||||
// someone may have have registered after sharing contact info, so recheck
|
||||
if (!this.isRegistered && this.activeDid) {
|
||||
try {
|
||||
const resp = await fetchEndorserRateLimits(
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
this.activeDid,
|
||||
);
|
||||
if (resp.status === 200) {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
isRegistered: true,
|
||||
});
|
||||
this.isRegistered = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore the error... just keep us unregistered
|
||||
}
|
||||
}
|
||||
|
||||
// this returns a Promise but we don't need to wait for it
|
||||
this.updateAllFeed();
|
||||
|
||||
if (this.activeDid) {
|
||||
const offersToUserData = await getNewOffersToUser(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
this.activeDid,
|
||||
this.lastAckedOfferToUserJwtId,
|
||||
);
|
||||
this.numNewOffersToUser = offersToUserData.data.length;
|
||||
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
|
||||
}
|
||||
|
||||
if (this.activeDid) {
|
||||
const offersToUserProjects = await getNewOffersToUserProjects(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
this.activeDid,
|
||||
this.lastAckedOfferToUserProjectsJwtId,
|
||||
);
|
||||
this.numNewOffersToUserProjects = offersToUserProjects.data.length;
|
||||
this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
logConsoleAndDb("Error retrieving settings or feed: " + err, true);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
err.userMessage ||
|
||||
"There was an error retrieving your settings or the latest activity.",
|
||||
},
|
||||
5000,
|
||||
} catch (error) {
|
||||
logConsoleAndDb(
|
||||
"Error retrieving all account DIDs on home page:" + error,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async generatePasskeyIdentifier() {
|
||||
this.isCreatingIdentifier = true;
|
||||
const account = await registerSaveAndActivatePasskey(
|
||||
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : ""),
|
||||
);
|
||||
this.activeDid = account.did;
|
||||
this.allMyDids = this.allMyDids.concat(this.activeDid);
|
||||
this.isCreatingIdentifier = false;
|
||||
/**
|
||||
* Loads user settings from storage
|
||||
* Sets component state for:
|
||||
* - API server
|
||||
* - Active DID
|
||||
* - Feed filters and view settings
|
||||
* - Registration status
|
||||
* - Notification acknowledgments
|
||||
*/
|
||||
private async loadSettings() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.feedLastViewedClaimId = settings.lastViewedClaimId;
|
||||
this.givenName = settings.firstName || "";
|
||||
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
|
||||
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
|
||||
this.lastAckedOfferToUserProjectsJwtId =
|
||||
settings.lastAckedOfferToUserProjectsJwtId;
|
||||
this.searchBoxes = settings.searchBoxes || [];
|
||||
this.showShortcutBvc = !!settings.showShortcutBvc;
|
||||
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads user contacts from database
|
||||
* Used for displaying contact info in feed and actions
|
||||
*/
|
||||
private async loadContacts() {
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies user registration status with endorser service
|
||||
* - Checks if unregistered user can access API
|
||||
* - Updates registration status if successful
|
||||
* - Preserves unregistered state on failure
|
||||
*/
|
||||
private async checkRegistrationStatus() {
|
||||
if (!this.isRegistered && this.activeDid) {
|
||||
try {
|
||||
const resp = await fetchEndorserRateLimits(
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
this.activeDid,
|
||||
);
|
||||
if (resp.status === 200) {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
apiServer: this.apiServer,
|
||||
isRegistered: true,
|
||||
...(await retrieveSettingsForActiveAccount()),
|
||||
});
|
||||
this.isRegistered = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore the error... just keep us unregistered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes feed data
|
||||
* Triggers updateAllFeed() to populate activity feed
|
||||
*/
|
||||
private async loadFeedData() {
|
||||
await this.updateAllFeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads new offers for user and their projects
|
||||
* Updates:
|
||||
* - Number of new direct offers
|
||||
* - Number of new project offers
|
||||
* - Rate limit status for both
|
||||
* @requires Active DID
|
||||
*/
|
||||
private async loadNewOffers() {
|
||||
if (this.activeDid) {
|
||||
const offersToUserData = await getNewOffersToUser(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
this.activeDid,
|
||||
this.lastAckedOfferToUserJwtId,
|
||||
);
|
||||
this.numNewOffersToUser = offersToUserData.data.length;
|
||||
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
|
||||
|
||||
const offersToUserProjects = await getNewOffersToUserProjects(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
this.activeDid,
|
||||
this.lastAckedOfferToUserProjectsJwtId,
|
||||
);
|
||||
this.numNewOffersToUserProjects = offersToUserProjects.data.length;
|
||||
this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user needs onboarding
|
||||
* Opens onboarding dialog if not completed
|
||||
*/
|
||||
private async checkOnboarding() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
if (!settings.finishedOnboarding) {
|
||||
(this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors during initialization
|
||||
* - Logs error to console and database
|
||||
* - Displays user notification
|
||||
* @param err Error object with optional userMessage
|
||||
*/
|
||||
private handleError(err: unknown) {
|
||||
logConsoleAndDb("Error retrieving settings or feed: " + err, true);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
err.userMessage ||
|
||||
"There was an error retrieving your settings or the latest activity.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if feed results are being filtered
|
||||
* @returns true if visible or nearby filters are active
|
||||
*/
|
||||
resultsAreFiltered() {
|
||||
return this.isFeedFilteredByVisible || this.isFeedFilteredByNearby;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if browser notifications are supported
|
||||
* @returns true if Notification API is available
|
||||
*/
|
||||
notificationsSupported() {
|
||||
return "Notification" in window;
|
||||
}
|
||||
|
||||
// only called when a setting was changed
|
||||
/**
|
||||
* Reloads feed when filter settings change
|
||||
* - Updates filter states
|
||||
* - Clears existing feed data
|
||||
* - Triggers new feed load
|
||||
*/
|
||||
async reloadFeedOnChange() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
|
||||
@@ -639,9 +696,9 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Data loader used by infinite scroller
|
||||
* @param payload is the flag from the InfiniteScroll indicating if it should load
|
||||
**/
|
||||
* Loads more feed items for infinite scroll
|
||||
* @param payload Boolean indicating if more items should be loaded
|
||||
*/
|
||||
async loadMoreGives(payload: boolean) {
|
||||
// Since feed now loads projects along the way, it takes longer
|
||||
// and the InfiniteScroll component triggers a load before finished.
|
||||
@@ -664,6 +721,12 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates feed with latest activity
|
||||
* - Handles filtering of results
|
||||
* - Updates last viewed claim ID
|
||||
* - Manages loading state
|
||||
*/
|
||||
async updateAllFeed() {
|
||||
this.isFeedLoading = true;
|
||||
let endOfResults = true;
|
||||
@@ -731,6 +794,7 @@ export default class HomeView extends Vue {
|
||||
|
||||
const newRecord: GiveRecordWithContactInfo = {
|
||||
...record,
|
||||
jwtId: record.jwtId,
|
||||
giver: didInfoForContact(
|
||||
giverDid,
|
||||
this.activeDid,
|
||||
@@ -765,7 +829,7 @@ export default class HomeView extends Vue {
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("Error with feed load:", e);
|
||||
logger.error("Error with feed load:", e);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -919,6 +983,11 @@ export default class HomeView extends Vue {
|
||||
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens dialog for creating new gift/claim
|
||||
* @param giver Optional contact info for giver
|
||||
* @param description Optional gift description
|
||||
*/
|
||||
openDialog(giver?: GiverReceiverInputInfo, description?: string) {
|
||||
(this.$refs.customDialog as GiftedDialog).open(
|
||||
giver,
|
||||
@@ -932,12 +1001,20 @@ export default class HomeView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens prompts for gift ideas
|
||||
* Links to openDialog for selected prompt
|
||||
*/
|
||||
openGiftedPrompts() {
|
||||
(this.$refs.giftedPrompts as GiftedPrompts).open((giver, description) =>
|
||||
this.openDialog(giver as GiverReceiverInputInfo, description),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens feed filter configuration
|
||||
* @param reloadFeedOnChange Callback for when filters are updated
|
||||
*/
|
||||
openFeedFilters() {
|
||||
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
|
||||
}
|
||||
@@ -993,7 +1070,7 @@ export default class HomeView extends Vue {
|
||||
// The Web Share API will handle sharing the URL appropriately
|
||||
this.imageCache.set(imageUrl, null);
|
||||
} catch (error) {
|
||||
console.warn("Failed to cache image:", error);
|
||||
logger.warn("Failed to cache image:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<router-link
|
||||
:to="{ name: 'account' }"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
><fa icon="chevron-left" class="fa-fw"></fa>
|
||||
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</router-link>
|
||||
|
||||
Switch Identity
|
||||
@@ -22,7 +22,10 @@
|
||||
v-if="activeDid && !activeDidInIdentities"
|
||||
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4"
|
||||
>
|
||||
<fa icon="circle-check" class="fa-fw text-red-600 text-xl mr-3"></fa>
|
||||
<font-awesome
|
||||
icon="circle-check"
|
||||
class="fa-fw text-red-600 text-xl mr-3"
|
||||
></font-awesome>
|
||||
<div class="text-sm text-slate-500">
|
||||
<div class="overflow-hidden truncate">
|
||||
<b>ID:</b> <code>{{ activeDid }}</code>
|
||||
@@ -45,12 +48,12 @@
|
||||
class="flex flex-grow items-center bg-slate-100 rounded-md px-4 py-3 mb-2 truncate cursor-pointer"
|
||||
@click="switchAccount(ident.did)"
|
||||
>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="ident.did === activeDid"
|
||||
icon="circle-check"
|
||||
class="fa-fw text-blue-600 text-xl mr-3"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="circle"
|
||||
class="fa-fw text-slate-400 text-xl mr-3"
|
||||
@@ -62,13 +65,13 @@
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="ident.did === activeDid"
|
||||
icon="trash-can"
|
||||
class="text-slate-400 text-xl ml-2 mr-2 cursor-pointer"
|
||||
@click="notifyCannotDelete()"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="trash-can"
|
||||
class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
|
||||
@@ -110,10 +113,11 @@ import {
|
||||
} from "../db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { retrieveAllAccountsMetadata } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class IdentitySwitcherView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
public activeDid = "";
|
||||
public activeDidInIdentities = false;
|
||||
@@ -131,7 +135,10 @@ export default class IdentitySwitcherView extends Vue {
|
||||
const accounts = await retrieveAllAccountsMetadata();
|
||||
for (let n = 0; n < accounts.length; n++) {
|
||||
const acct = accounts[n];
|
||||
this.otherIdentities.push({ id: acct.id as string, did: acct.did });
|
||||
this.otherIdentities.push({
|
||||
id: (acct.id ?? 0).toString(),
|
||||
did: acct.did,
|
||||
});
|
||||
if (acct.did && this.activeDid === acct.did) {
|
||||
this.activeDidInIdentities = true;
|
||||
}
|
||||
@@ -146,7 +153,7 @@ export default class IdentitySwitcherView extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
console.error("Telling user to clear cache at page create because:", err);
|
||||
logger.error("Telling user to clear cache at page create because:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +166,7 @@ export default class IdentitySwitcherView extends Vue {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: did,
|
||||
});
|
||||
(this.$router as Router).push({ name: "account" });
|
||||
this.$router.push({ name: "account" });
|
||||
}
|
||||
|
||||
async deleteAccount(id: string) {
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<!-- Cancel -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<fa icon="chevron-left"></fa>
|
||||
<font-awesome icon="chevron-left"></font-awesome>
|
||||
</button>
|
||||
Import Existing Identifier
|
||||
</h1>
|
||||
@@ -20,10 +20,10 @@
|
||||
<!-- id used by puppeteer test script -->
|
||||
<textarea
|
||||
id="seed-input"
|
||||
v-model="mnemonic"
|
||||
type="text"
|
||||
placeholder="Seed Phrase"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
v-model="mnemonic"
|
||||
/>
|
||||
|
||||
<h3
|
||||
@@ -35,22 +35,22 @@
|
||||
<div v-if="showAdvanced">
|
||||
Enter a custom derivation path
|
||||
<input
|
||||
v-model="derivationPath"
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
v-model="derivationPath"
|
||||
/>
|
||||
<span class="ml-4">
|
||||
For previous uPort or Endorser users,
|
||||
<a
|
||||
@click="derivationPath = UPORT_DERIVATION_PATH"
|
||||
class="text-blue-500"
|
||||
@click="derivationPath = UPORT_DERIVATION_PATH"
|
||||
>
|
||||
click here to use that value.
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<div class="mt-4" v-if="numAccounts == 1">
|
||||
<input type="checkbox" class="mr-2" v-model="shouldErase" />
|
||||
<div v-if="numAccounts == 1" class="mt-4">
|
||||
<input v-model="shouldErase" type="checkbox" class="mr-2" />
|
||||
<label>Erase the previous identifier.</label>
|
||||
</div>
|
||||
|
||||
@@ -65,15 +65,15 @@
|
||||
<div class="mt-8">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<button
|
||||
@click="fromMnemonic()"
|
||||
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
||||
@click="fromMnemonic()"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
@click="onCancelClick()"
|
||||
type="button"
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||
@click="onCancelClick()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -99,7 +99,7 @@ import {
|
||||
newIdentifier,
|
||||
} from "../libs/crypto";
|
||||
import { retrieveAccountCount } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: {},
|
||||
})
|
||||
@@ -111,6 +111,7 @@ export default class ImportAccountView extends Vue {
|
||||
AppString = AppString;
|
||||
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
apiServer = "";
|
||||
address = "";
|
||||
@@ -130,7 +131,7 @@ export default class ImportAccountView extends Vue {
|
||||
}
|
||||
|
||||
public onCancelClick() {
|
||||
(this.$router as Router).back();
|
||||
this.$router.back();
|
||||
}
|
||||
|
||||
public isNotProdServer() {
|
||||
@@ -170,10 +171,10 @@ export default class ImportAccountView extends Vue {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: newId.did,
|
||||
});
|
||||
(this.$router as Router).push({ name: "account" });
|
||||
this.$router.push({ name: "account" });
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error saving mnemonic & updating settings:", err);
|
||||
logger.error("Error saving mnemonic & updating settings:", err);
|
||||
if (err == "Error: invalid mnemonic") {
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<!-- Cancel -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.go(-1)"
|
||||
>
|
||||
<fa icon="chevron-left"></fa>
|
||||
<font-awesome icon="chevron-left"></font-awesome>
|
||||
</button>
|
||||
Derive from Existing Identity
|
||||
</h1>
|
||||
@@ -25,21 +25,21 @@
|
||||
</p>
|
||||
<ul class="mb-4">
|
||||
<li
|
||||
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
|
||||
v-for="dids in didArrays"
|
||||
:key="dids[0]"
|
||||
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
|
||||
@click="switchAccount(dids[0])"
|
||||
>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="dids[0] == selectedArrayFirstDid"
|
||||
icon="circle"
|
||||
class="fa-fw text-blue-500 text-xl mr-3"
|
||||
></fa>
|
||||
<fa
|
||||
></font-awesome>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="circle"
|
||||
class="fa-fw text-slate-400 text-xl mr-3"
|
||||
></fa>
|
||||
></font-awesome>
|
||||
<span class="overflow-hidden">
|
||||
<div class="text-sm text-slate-500 truncate">
|
||||
<code>{{ dids.join(",") }}</code>
|
||||
@@ -51,15 +51,15 @@
|
||||
<div class="mt-8">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<button
|
||||
@click="incrementDerivation()"
|
||||
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
||||
@click="incrementDerivation()"
|
||||
>
|
||||
Increment and Import
|
||||
</button>
|
||||
<button
|
||||
@click="onCancelClick()"
|
||||
type="button"
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||
@click="onCancelClick()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
||||
|
||||
import {
|
||||
DEFAULT_ROOT_DERIVATION_PATH,
|
||||
@@ -81,11 +81,14 @@ import {
|
||||
import { accountsDBPromise, db } from "../db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { retrieveAllFullyDecryptedAccounts } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: {},
|
||||
})
|
||||
export default class ImportAccountView extends Vue {
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
|
||||
didArrays: Array<Array<string>> = [];
|
||||
selectedArrayFirstDid = "";
|
||||
@@ -102,7 +105,7 @@ export default class ImportAccountView extends Vue {
|
||||
}
|
||||
|
||||
public onCancelClick() {
|
||||
(this.$router as Router).back();
|
||||
this.$router.back();
|
||||
}
|
||||
|
||||
public switchAccount(did: string) {
|
||||
@@ -151,9 +154,9 @@ export default class ImportAccountView extends Vue {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: newId.did,
|
||||
});
|
||||
(this.$router as Router).push({ name: "account" });
|
||||
this.$router.push({ name: "account" });
|
||||
} catch (err) {
|
||||
console.error("Error saving mnemonic & updating settings:", err);
|
||||
logger.error("Error saving mnemonic & updating settings:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-if="checkingInvite"
|
||||
class="text-lg text-center font-light relative px-7"
|
||||
>
|
||||
<fa icon="spinner" class="fa-spin-pulse" />
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
<div v-else class="text-center mt-4">
|
||||
<p>That invitation did not work.</p>
|
||||
@@ -28,8 +28,8 @@
|
||||
/>
|
||||
<br />
|
||||
<button
|
||||
@click="() => processInvite(inputJwt, true)"
|
||||
class="ml-2 p-2 bg-blue-500 text-white rounded"
|
||||
@click="() => processInvite(inputJwt, true)"
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { Router, RouteLocationNormalized } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||
@@ -52,18 +52,69 @@ import { decodeEndorserJwt } from "../libs/crypto/vc";
|
||||
import { errorStringForLog } from "../libs/endorserServer";
|
||||
import { generateSaveAndActivateIdentity } from "../libs/util";
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
/**
|
||||
* Invite One Accept View Component
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This component handles accepting single-use invitations to join the platform.
|
||||
* It supports multiple invitation formats and provides user feedback during the process.
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Component loads with JWT from route or user input
|
||||
* 2. Validates JWT format and signature
|
||||
* 3. Processes invite data and redirects to contacts page
|
||||
* 4. Handles errors with user feedback
|
||||
*
|
||||
* Supported Invite Formats:
|
||||
* 1. Direct JWT in URL path: /invite-one-accept/{jwt}
|
||||
* 2. JWT in text message URL: https://app.example.com/invite-one-accept/{jwt}
|
||||
* 3. JWT surrounded by other text: "Your invite code is {jwt}"
|
||||
*
|
||||
* Security Features:
|
||||
* - JWT validation
|
||||
* - Identity generation if needed
|
||||
* - Error handling for invalid/expired invites
|
||||
*
|
||||
* @see ContactsView for completion of invite process
|
||||
*/
|
||||
@Component({
|
||||
components: { QuickNav },
|
||||
})
|
||||
export default class InviteOneAcceptView extends Vue {
|
||||
/** Notification function injected by Vue */
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
/** Router instance for navigation */
|
||||
$router!: Router;
|
||||
/** Route instance for current route */
|
||||
$route!: RouteLocationNormalized;
|
||||
|
||||
activeDid: string = "";
|
||||
apiServer: string = "";
|
||||
checkingInvite: boolean = true;
|
||||
inputJwt: string = "";
|
||||
/** Active user's DID */
|
||||
activeDid = "";
|
||||
/** API server endpoint */
|
||||
apiServer = "";
|
||||
/** Loading state for invite processing */
|
||||
checkingInvite = true;
|
||||
/** User input for manual JWT entry */
|
||||
inputJwt = "";
|
||||
|
||||
/**
|
||||
* Component lifecycle hook that initializes invite processing
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Opens database connection
|
||||
* 2. Retrieves account settings
|
||||
* 3. Ensures active DID exists or generates one
|
||||
* 4. Extracts JWT from URL path
|
||||
* 5. Processes invite automatically
|
||||
*
|
||||
* @throws Will not throw but logs errors
|
||||
* @emits Notifications on errors
|
||||
*/
|
||||
async mounted() {
|
||||
this.checkingInvite = true;
|
||||
await db.open();
|
||||
|
||||
// Load or generate identity
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
@@ -72,81 +123,155 @@ export default class InviteOneAcceptView extends Vue {
|
||||
this.activeDid = await generateSaveAndActivateIdentity();
|
||||
}
|
||||
|
||||
const jwt = window.location.pathname.substring(
|
||||
"/invite-one-accept/".length,
|
||||
);
|
||||
// Extract JWT from route path
|
||||
const jwt = (this.$route.params.jwt as string) || "";
|
||||
await this.processInvite(jwt, false);
|
||||
|
||||
this.checkingInvite = false;
|
||||
}
|
||||
|
||||
// process the invite JWT and/or text message containing the URL with the JWT
|
||||
/**
|
||||
* Processes an invite JWT and/or text containing the invite
|
||||
*
|
||||
* Handles multiple input formats:
|
||||
* 1. Direct JWT:
|
||||
* - Raw JWT string starting with "ey"
|
||||
* - Example: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ...
|
||||
*
|
||||
* 2. URL containing JWT:
|
||||
* - Full URL with JWT in path
|
||||
* - Pattern: /invite-one-accept/{jwt}
|
||||
* - Example: https://app.example.com/invite-one-accept/eyJ0eXAiOiJKV1Q...
|
||||
*
|
||||
* 3. Text with embedded JWT:
|
||||
* - JWT surrounded by other text
|
||||
* - Uses regex to extract JWT pattern
|
||||
* - Example: "Your invite code is eyJ0eXAiOiJKV1Q... Click to accept"
|
||||
*
|
||||
* Extraction Process:
|
||||
* 1. First attempts URL pattern match
|
||||
* 2. If no URL found, looks for JWT pattern (ey...)
|
||||
* 3. Validates extracted JWT format
|
||||
* 4. Redirects to contacts page on success
|
||||
*
|
||||
* Error Handling:
|
||||
* - Missing JWT: Shows "Missing Invite" notification
|
||||
* - Invalid JWT: Logs error and shows generic error message
|
||||
* - Network Issues: Captured in try/catch block
|
||||
*
|
||||
* @param jwtInput Raw input that may contain a JWT
|
||||
* @param notifyOnFailure Whether to show error notifications
|
||||
* - true: Shows UI notifications for errors
|
||||
* - false: Silently logs errors (used for auto-processing)
|
||||
* @throws Will not throw but logs errors
|
||||
* @emits Notifications on errors if notifyOnFailure is true
|
||||
* @emits Router navigation on success to /contacts?inviteJwt={jwt}
|
||||
*/
|
||||
async processInvite(jwtInput: string, notifyOnFailure: boolean) {
|
||||
this.checkingInvite = true;
|
||||
|
||||
try {
|
||||
let jwt: string = jwtInput ?? "";
|
||||
|
||||
// parse the string: extract the URL or JWT if surrounded by spaces
|
||||
// and then extract the JWT from the URL
|
||||
// (For another approach used with contacts, see getContactPayloadFromJwtUrl)
|
||||
const urlMatch = jwtInput.match(/(https?:\/\/[^\s]+)/);
|
||||
if (urlMatch && urlMatch[1]) {
|
||||
// extract the JWT from the URL, meaning any character except "?"
|
||||
const internalMatch = urlMatch[1].match(/\/invite-one-accept\/([^?]+)/);
|
||||
if (internalMatch && internalMatch[1]) {
|
||||
jwt = internalMatch[1];
|
||||
}
|
||||
} else {
|
||||
// extract the JWT (which starts with "ey") if it is surrounded by other input
|
||||
const spaceMatch = jwtInput.match(/(ey[\w.-]+)/);
|
||||
if (spaceMatch && spaceMatch[1]) {
|
||||
jwt = spaceMatch[1];
|
||||
}
|
||||
}
|
||||
const jwt = this.extractJwtFromInput(jwtInput);
|
||||
|
||||
if (!jwt) {
|
||||
if (notifyOnFailure) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Missing Invite",
|
||||
text: "There was no invite. Paste the entire text that has the data.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
//const payload: JWTPayload =
|
||||
decodeEndorserJwt(jwt);
|
||||
this.handleMissingJwt(notifyOnFailure);
|
||||
return;
|
||||
}
|
||||
|
||||
// That's good enough for an initial check.
|
||||
// Send them to the contacts page to finish, with inviteJwt in the query string.
|
||||
(this.$router as Router).push({
|
||||
name: "contacts",
|
||||
query: { inviteJwt: jwt },
|
||||
});
|
||||
}
|
||||
await this.validateAndRedirect(jwt);
|
||||
} catch (error) {
|
||||
const fullError = "Error accepting invite: " + errorStringForLog(error);
|
||||
logConsoleAndDb(fullError, true);
|
||||
if (notifyOnFailure) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error processing that invite.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
this.handleError(error, notifyOnFailure);
|
||||
} finally {
|
||||
this.checkingInvite = false;
|
||||
}
|
||||
this.checkingInvite = false;
|
||||
}
|
||||
|
||||
// check the invite JWT
|
||||
/**
|
||||
* Extracts JWT from various input formats
|
||||
* @param input Raw input text
|
||||
* @returns Extracted JWT or empty string
|
||||
*/
|
||||
private extractJwtFromInput(input: string): string {
|
||||
const jwtInput = input ?? "";
|
||||
|
||||
// Try URL format first
|
||||
const urlMatch = jwtInput.match(/(https?:\/\/[^\s]+)/);
|
||||
if (urlMatch?.[1]) {
|
||||
const internalMatch = urlMatch[1].match(/\/invite-one-accept\/([^?]+)/);
|
||||
if (internalMatch?.[1]) return internalMatch[1];
|
||||
}
|
||||
|
||||
// Try direct JWT format
|
||||
const spaceMatch = jwtInput.match(/(ey[\w.-]+)/);
|
||||
if (spaceMatch?.[1]) return spaceMatch[1];
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates JWT and redirects to contacts page
|
||||
* @param jwt JWT to validate
|
||||
*/
|
||||
private async validateAndRedirect(jwt: string) {
|
||||
decodeEndorserJwt(jwt);
|
||||
this.$router.push({
|
||||
name: "contacts",
|
||||
query: { inviteJwt: jwt },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles missing JWT error
|
||||
* @param notify Whether to show notification
|
||||
*/
|
||||
private handleMissingJwt(notify: boolean) {
|
||||
if (notify) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Missing Invite",
|
||||
text: "There was no invite. Paste the entire text that has the data.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles processing errors
|
||||
* @param error Error that occurred
|
||||
* @param notify Whether to show notification
|
||||
*/
|
||||
private handleError(error: unknown, notify: boolean) {
|
||||
const fullError = "Error accepting invite: " + errorStringForLog(error);
|
||||
logConsoleAndDb(fullError, true);
|
||||
|
||||
if (notify) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "There was an error processing that invite.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates invite data format
|
||||
*
|
||||
* Checks for common error cases:
|
||||
* - Truncated URLs
|
||||
* - Missing JWT data
|
||||
* - Invalid URL formats
|
||||
*
|
||||
* @param jwtInput Raw input to validate
|
||||
* @throws Will not throw but shows notifications
|
||||
* @emits Notifications on validation errors
|
||||
*/
|
||||
async checkInvite(jwtInput: string) {
|
||||
if (
|
||||
jwtInput.endsWith(APP_SERVER) ||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
class="fixed right-6 top-12 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
|
||||
@click="createInvite()"
|
||||
>
|
||||
<fa icon="plus" class="fa-fw"></fa>
|
||||
<font-awesome icon="plus" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
|
||||
<InviteDialog ref="inviteDialog" />
|
||||
@@ -72,16 +72,18 @@
|
||||
!invite.redeemedAt &&
|
||||
invite.expiresAt > new Date().toISOString()
|
||||
"
|
||||
class="text-center text-blue-500 cursor-pointer"
|
||||
:title="inviteLink(invite.jwt)"
|
||||
@click="
|
||||
copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)
|
||||
"
|
||||
class="text-center text-blue-500 cursor-pointer"
|
||||
:title="inviteLink(invite.jwt)"
|
||||
>
|
||||
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-center text-slate-500 cursor-pointer"
|
||||
:title="inviteLink(invite.jwt)"
|
||||
@click="
|
||||
showInvite(
|
||||
invite.inviteIdentifier,
|
||||
@@ -89,8 +91,6 @@
|
||||
invite.expiresAt < new Date().toISOString(),
|
||||
)
|
||||
"
|
||||
class="text-center text-slate-500 cursor-pointer"
|
||||
:title="inviteLink(invite.jwt)"
|
||||
>
|
||||
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
|
||||
</span>
|
||||
@@ -106,7 +106,7 @@
|
||||
<br />
|
||||
{{ getTruncatedRedeemedBy(invite.redeemedBy) }}
|
||||
<br />
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="invite.redeemedBy && !contactsRedeemed[invite.redeemedBy]"
|
||||
icon="plus"
|
||||
class="bg-green-600 text-white px-1 py-1 rounded-full cursor-pointer"
|
||||
@@ -114,7 +114,7 @@
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="trash-can"
|
||||
class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
|
||||
@click="deleteInvite(invite.inviteIdentifier, invite.notes)"
|
||||
@@ -132,6 +132,7 @@
|
||||
import axios from "axios";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import ContactNameDialog from "../components/ContactNameDialog.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
@@ -141,7 +142,7 @@ import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { createInviteJwt, getHeaders } from "../libs/endorserServer";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
interface Invite {
|
||||
inviteIdentifier: string;
|
||||
expiresAt: string;
|
||||
@@ -156,6 +157,7 @@ interface Invite {
|
||||
})
|
||||
export default class InviteOneView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
invites: Invite[] = [];
|
||||
activeDid: string = "";
|
||||
@@ -189,7 +191,7 @@ export default class InviteOneView extends Vue {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching invites:", error);
|
||||
logger.error("Error fetching invites:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -256,7 +258,7 @@ export default class InviteOneView extends Vue {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
lookForErrorAndNotify(error: any, title: string, defaultMessage: string) {
|
||||
console.error(title, "-", error);
|
||||
logger.error(title, "-", error);
|
||||
let message = defaultMessage;
|
||||
if (error.response && error.response.data && error.response.data.error) {
|
||||
if (error.response.data.error.message) {
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<div id="ViewBreadcrumb" class="mb-8">
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<!-- Back -->
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="chevron-left"
|
||||
@click="$router.back()"
|
||||
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
/>
|
||||
New Activity For You
|
||||
</h1>
|
||||
@@ -25,7 +25,7 @@
|
||||
<span class="text-lg font-medium ml-4"
|
||||
>New Offer{{ newOffersToUser.length === 1 ? "" : "s" }} To You</span
|
||||
>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="newOffersToUser.length > 0"
|
||||
:icon="showOffersDetails ? 'chevron-down' : 'chevron-right'"
|
||||
class="cursor-pointer ml-4 mr-4 text-lg"
|
||||
@@ -59,12 +59,15 @@
|
||||
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
/>
|
||||
</router-link>
|
||||
<!-- New line that appears on hover or when the offer is clicked -->
|
||||
<div
|
||||
@click="markOffersAsReadStartingWith(offer.jwtId)"
|
||||
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
|
||||
@click="markOffersAsReadStartingWith(offer.jwtId)"
|
||||
>
|
||||
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
|
||||
Click to keep all above as new offers
|
||||
@@ -87,7 +90,7 @@
|
||||
>New Offer{{ newOffersToUserProjects.length === 1 ? "" : "s" }} To
|
||||
Your Projects</span
|
||||
>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="newOffersToUserProjects.length > 0"
|
||||
:icon="
|
||||
showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
|
||||
@@ -125,12 +128,15 @@
|
||||
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
/>
|
||||
</router-link>
|
||||
<!-- New line that appears on hover -->
|
||||
<div
|
||||
@click="markOffersToUserProjectsAsReadStartingWith(offer.jwtId)"
|
||||
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
|
||||
@click="markOffersToUserProjectsAsReadStartingWith(offer.jwtId)"
|
||||
>
|
||||
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
|
||||
Click to keep all above as new offers
|
||||
@@ -154,13 +160,13 @@ import {
|
||||
updateAccountSettings,
|
||||
} from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { Router } from "vue-router";
|
||||
import { OfferSummaryRecord, OfferToPlanSummaryRecord } from "../interfaces";
|
||||
import {
|
||||
didInfo,
|
||||
displayAmount,
|
||||
getNewOffersToUser,
|
||||
getNewOffersToUserProjects,
|
||||
OfferSummaryRecord,
|
||||
OfferToPlanSummaryRecord,
|
||||
} from "../libs/endorserServer";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
|
||||
@@ -169,7 +175,7 @@ import { retrieveAccountDids } from "../libs/util";
|
||||
})
|
||||
export default class NewActivityView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
$router!: Router;
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: string[] = [];
|
||||
@@ -218,7 +224,7 @@ export default class NewActivityView extends Vue {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error retrieving settings & contacts:", err);
|
||||
logger.error("Error retrieving settings & contacts:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<!-- Cancel -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
Edit Identity
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<input
|
||||
v-model="givenName"
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
v-model="givenName"
|
||||
/>
|
||||
|
||||
<div class="mt-8">
|
||||
@@ -54,6 +54,8 @@ import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
components: {},
|
||||
})
|
||||
export default class NewEditAccountView extends Vue {
|
||||
$router!: Router;
|
||||
|
||||
givenName = "";
|
||||
|
||||
// 'created' hook runs when the Vue instance is first created
|
||||
@@ -69,11 +71,11 @@ export default class NewEditAccountView extends Vue {
|
||||
firstName: this.givenName,
|
||||
lastName: "", // deprecated, pre v 0.1.3
|
||||
});
|
||||
(this.$router as Router).back();
|
||||
this.$router.back();
|
||||
}
|
||||
|
||||
onClickCancel() {
|
||||
(this.$router as Router).back();
|
||||
this.$router.back();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<!-- Cancel -->
|
||||
<!-- Back -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
Edit Project Idea
|
||||
</h1>
|
||||
@@ -25,10 +25,10 @@
|
||||
</div>
|
||||
|
||||
<input
|
||||
v-model="fullClaim.name"
|
||||
type="text"
|
||||
placeholder="Idea Name"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
v-model="fullClaim.name"
|
||||
/>
|
||||
|
||||
<div class="flex justify-center mt-4">
|
||||
@@ -36,14 +36,14 @@
|
||||
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
|
||||
<img :src="imageUrl" class="h-24 rounded-xl" />
|
||||
</a>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="trash-can"
|
||||
@click="confirmDeleteImage"
|
||||
class="text-red-500 fa-fw ml-8 mt-10"
|
||||
@click="confirmDeleteImage"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="camera"
|
||||
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
|
||||
@click="openImageDialog"
|
||||
@@ -53,27 +53,27 @@
|
||||
<ImageMethodDialog ref="imageDialog" />
|
||||
|
||||
<input
|
||||
v-model="agentDid"
|
||||
type="text"
|
||||
placeholder="Other Authorized Representative"
|
||||
class="mt-4 block w-full rounded border border-slate-400 px-3 py-2"
|
||||
v-model="agentDid"
|
||||
/>
|
||||
<div class="mb-4">
|
||||
<p v-if="activeDid != projectIssuerDid && agentDid != projectIssuerDid">
|
||||
<span class="text-red-500">Beware!</span>
|
||||
If you save this, the original project owner will no longer be able to
|
||||
edit it.
|
||||
<button @click="agentDid = projectIssuerDid" class="text-blue-500">
|
||||
<button class="text-blue-500" @click="agentDid = projectIssuerDid">
|
||||
Click here to make the original owner an authorized representative.
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
v-model="fullClaim.description"
|
||||
placeholder="Description"
|
||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||
rows="5"
|
||||
v-model="fullClaim.description"
|
||||
maxlength="5000"
|
||||
></textarea>
|
||||
<div class="text-xs text-slate-500 italic">
|
||||
@@ -102,9 +102,9 @@
|
||||
class="rounded border border-slate-400 px-3 py-2"
|
||||
/>
|
||||
<input
|
||||
v-model="startTimeInput"
|
||||
:disabled="!startDateInput"
|
||||
placeholder="Start Time"
|
||||
v-model="startTimeInput"
|
||||
type="time"
|
||||
class="rounded border border-slate-400 ml-2 px-3 py-2"
|
||||
/>
|
||||
@@ -127,9 +127,9 @@
|
||||
class="ml-2 rounded border border-slate-400 px-3 py-2"
|
||||
/>
|
||||
<input
|
||||
v-model="endTimeInput"
|
||||
:disabled="!endDateInput"
|
||||
placeholder="End Time"
|
||||
v-model="endTimeInput"
|
||||
type="time"
|
||||
class="rounded border border-slate-400 ml-2 px-3 py-2"
|
||||
/>
|
||||
@@ -140,7 +140,7 @@
|
||||
class="flex items-center mt-4"
|
||||
@click="includeLocation = !includeLocation"
|
||||
>
|
||||
<input type="checkbox" class="mr-2" v-model="includeLocation" />
|
||||
<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">
|
||||
@@ -179,21 +179,14 @@
|
||||
class="items-center mb-4"
|
||||
>
|
||||
<div class="flex" @click="sendToTrustroots = !sendToTrustroots">
|
||||
<input type="checkbox" class="mr-2" v-model="sendToTrustroots" />
|
||||
<input v-model="sendToTrustroots" type="checkbox" class="mr-2" />
|
||||
<label>Send to Trustroots</label>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-blue-500 ml-2 cursor-pointer"
|
||||
@click.stop="showNostrPartnerInfo"
|
||||
/>
|
||||
</div>
|
||||
<!--
|
||||
<div class="flex" @click="sendToTripHopping = !sendToTripHopping">
|
||||
<input type="checkbox" class="mr-2" v-model="sendToTripHopping" />
|
||||
<label>Send to TripHopping</label>
|
||||
<fa icon="circle-info" class="text-blue-500 ml-2 cursor-pointer" @click.stop="showNostrPartnerInfo" />
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
@@ -229,10 +222,11 @@
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { AxiosError, AxiosRequestHeaders } from "axios";
|
||||
import { DateTime } from "luxon";
|
||||
import { finalizeEvent, serializeEvent } from "nostr-tools";
|
||||
// these core imports could also be included as "import type ..."
|
||||
import { EventTemplate, UnsignedEvent, VerifiedEvent } from "nostr-tools/core";
|
||||
import * as nip06 from "nostr-tools/nip06";
|
||||
import { finalizeEvent } from "nostr-tools/lib/esm/index.js";
|
||||
import {
|
||||
accountFromExtendedKey,
|
||||
extendedKeysFromSeedWords,
|
||||
} from "nostr-tools/lib/esm/nip06.js";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
@@ -245,21 +239,30 @@ import {
|
||||
NotificationIface,
|
||||
} from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { PlanVerifiableCredential } from "../interfaces";
|
||||
import {
|
||||
createEndorserJwtVcFromClaim,
|
||||
getHeaders,
|
||||
PlanVerifiableCredential,
|
||||
} from "../libs/endorserServer";
|
||||
import {
|
||||
retrieveAccountCount,
|
||||
retrieveFullyDecryptedAccount,
|
||||
} from "../libs/util";
|
||||
|
||||
import {
|
||||
EventTemplate,
|
||||
UnsignedEvent,
|
||||
VerifiedEvent,
|
||||
} from "nostr-tools/lib/esm/index.js";
|
||||
import { serializeEvent } from "nostr-tools/lib/esm/index.js";
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
|
||||
})
|
||||
export default class NewEditProjectView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
errNote(message: string) {
|
||||
this.$notify(
|
||||
{ group: "alert", type: "danger", title: "Error", text: message },
|
||||
@@ -305,8 +308,7 @@ export default class NewEditProjectView extends Vue {
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
|
||||
|
||||
this.projectId =
|
||||
(this.$route as RouteLocationNormalizedLoaded).query["projectId"] || "";
|
||||
this.projectId = (this.$route.query["projectId"] as string) || "";
|
||||
|
||||
if (this.projectId) {
|
||||
if (this.numAccounts === 0) {
|
||||
@@ -355,7 +357,7 @@ export default class NewEditProjectView extends Vue {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Got error retrieving that project", error);
|
||||
logger.error("Got error retrieving that project", error);
|
||||
this.errNote("There was an error retrieving that project.");
|
||||
}
|
||||
}
|
||||
@@ -389,7 +391,7 @@ export default class NewEditProjectView extends Vue {
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
) {
|
||||
console.log(
|
||||
logger.log(
|
||||
"Using shared image API server, so only users on that server can play with images.",
|
||||
);
|
||||
}
|
||||
@@ -403,7 +405,7 @@ export default class NewEditProjectView extends Vue {
|
||||
// don't bother with a notification
|
||||
// (either they'll simply continue or they're canceling and going back)
|
||||
} else {
|
||||
console.error("Problem deleting image:", response);
|
||||
logger.error("Problem deleting image:", response);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -418,10 +420,10 @@ export default class NewEditProjectView extends Vue {
|
||||
|
||||
this.imageUrl = "";
|
||||
} catch (error) {
|
||||
console.error("Error deleting image:", error);
|
||||
logger.error("Error deleting image:", error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any).response.status === 404) {
|
||||
console.log("The image was already deleted:", error);
|
||||
logger.log("The image was already deleted:", error);
|
||||
|
||||
this.imageUrl = "";
|
||||
|
||||
@@ -591,12 +593,9 @@ export default class NewEditProjectView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
(this.$router as Router).push({ path: "/project/" + projectPath });
|
||||
this.$router.push({ path: "/project/" + projectPath });
|
||||
} else {
|
||||
console.error(
|
||||
"Got unexpected 'data' inside response from server",
|
||||
resp,
|
||||
);
|
||||
logger.error("Got unexpected 'data' inside response from server", resp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -613,7 +612,7 @@ export default class NewEditProjectView extends Vue {
|
||||
error?: { message?: string };
|
||||
}>;
|
||||
if (serverError) {
|
||||
console.error("Got error from server", serverError);
|
||||
logger.error("Got error from server", serverError);
|
||||
if (Object.prototype.hasOwnProperty.call(serverError, "message")) {
|
||||
userMessage =
|
||||
(serverError.response?.data?.error?.message as string) ||
|
||||
@@ -639,7 +638,7 @@ export default class NewEditProjectView extends Vue {
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.error("Here's the full error trying to save the claim:", error);
|
||||
logger.error("Here's the full error trying to save the claim:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -668,7 +667,7 @@ export default class NewEditProjectView extends Vue {
|
||||
// remove any trailing '
|
||||
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
|
||||
const accountNum = Number(finalDerNumNoApostrophe || 0);
|
||||
const extPubPri = nip06.extendedKeysFromSeedWords(
|
||||
const extPubPri = extendedKeysFromSeedWords(
|
||||
account?.mnemonic as string,
|
||||
"",
|
||||
accountNum,
|
||||
@@ -676,7 +675,7 @@ export default class NewEditProjectView extends Vue {
|
||||
const publicExtendedKey: string = extPubPri?.publicExtendedKey;
|
||||
const privateExtendedKey = extPubPri?.privateExtendedKey;
|
||||
const privateBytes: Uint8Array =
|
||||
nip06.accountFromExtendedKey(privateExtendedKey).privateKey;
|
||||
accountFromExtendedKey(privateExtendedKey).privateKey;
|
||||
// No real content is necessary, we just want something signed,
|
||||
// so we might as well use nostr libs for nostr functions.
|
||||
// Besides: someday we may create real content that we can relay.
|
||||
@@ -710,8 +709,7 @@ export default class NewEditProjectView extends Vue {
|
||||
const endorserPartnerUrl = partnerServer + "/api/partner/link";
|
||||
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
|
||||
const content = this.fullClaim.name + " - see " + timeSafariUrl;
|
||||
const publicKeyHex =
|
||||
nip06.accountFromExtendedKey(publicExtendedKey).publicKey;
|
||||
const publicKeyHex = accountFromExtendedKey(publicExtendedKey).publicKey;
|
||||
const unsignedPayload: UnsignedEvent = {
|
||||
// why doesn't "...signedPayload" work?
|
||||
kind: signedPayload.kind,
|
||||
@@ -760,7 +758,7 @@ export default class NewEditProjectView extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error(`Error sending to ${serviceName}`, error);
|
||||
logger.error(`Error sending to ${serviceName}`, error);
|
||||
let errorMessage = `There was an error sending to ${serviceName}.`;
|
||||
if (error.response?.data?.error?.message) {
|
||||
errorMessage = error.response.data.error.message;
|
||||
@@ -782,7 +780,7 @@ export default class NewEditProjectView extends Vue {
|
||||
this.isHiddenSpinner = false;
|
||||
|
||||
if (this.numAccounts === 0) {
|
||||
console.error("Error: there is no account.");
|
||||
logger.error("Error: there is no account.");
|
||||
} else {
|
||||
this.saveProject();
|
||||
}
|
||||
@@ -810,7 +808,7 @@ export default class NewEditProjectView extends Vue {
|
||||
}
|
||||
|
||||
public onCancelClick() {
|
||||
(this.$router as Router).back();
|
||||
this.$router.back();
|
||||
}
|
||||
|
||||
public showNostrPartnerInfo() {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -25,16 +25,16 @@
|
||||
<div />
|
||||
<div v-if="loading">
|
||||
<span class="text-xl">Creating... </span>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="spinner"
|
||||
class="fa-spin fa-spin-pulse"
|
||||
color="green"
|
||||
size="128"
|
||||
></fa>
|
||||
></font-awesome>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="text-xl">Created!</span>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="burst"
|
||||
class="fa-beat px-12"
|
||||
color="green"
|
||||
@@ -44,7 +44,7 @@
|
||||
--fa-animation-iteration-count: 1;
|
||||
--fa-beat-scale: 6;
|
||||
"
|
||||
></fa>
|
||||
></font-awesome>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
@@ -62,12 +62,13 @@ import QuickNav from "../components/QuickNav.vue";
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class NewIdentifierView extends Vue {
|
||||
loading = true;
|
||||
$router!: Router;
|
||||
|
||||
async mounted() {
|
||||
await generateSaveAndActivateIdentity();
|
||||
this.loading = false;
|
||||
setTimeout(() => {
|
||||
(this.$router as Router).push({ name: "home" });
|
||||
this.$router.push({ name: "home" });
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="cancelBack()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
>
|
||||
</h1>
|
||||
<textarea
|
||||
v-model="descriptionOfItem"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
placeholder="What is offered"
|
||||
v-model="descriptionOfItem"
|
||||
data-testId="itemDescription"
|
||||
/>
|
||||
<div class="flex flex-row justify-center">
|
||||
@@ -49,19 +49,19 @@
|
||||
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="amountInput === '0' ? null : decrement()"
|
||||
>
|
||||
<fa icon="chevron-left" />
|
||||
<font-awesome icon="chevron-left" />
|
||||
</div>
|
||||
<input
|
||||
v-model="amountInput"
|
||||
type="number"
|
||||
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
|
||||
v-model="amountInput"
|
||||
data-testId="inputOfferAmount"
|
||||
/>
|
||||
<div
|
||||
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
|
||||
@click="increment()"
|
||||
>
|
||||
<fa icon="chevron-right" />
|
||||
<font-awesome icon="chevron-right" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,9 +72,9 @@
|
||||
Conditions
|
||||
</span>
|
||||
<textarea
|
||||
v-model="descriptionOfCondition"
|
||||
class="w-full border border-slate-400 px-3 py-2 rounded-r"
|
||||
placeholder="Prerequisites, other people to include, etc."
|
||||
v-model="descriptionOfCondition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -94,11 +94,11 @@
|
||||
<div class="h-7 mt-4 flex">
|
||||
<input
|
||||
v-if="projectId && !offeredToRecipient"
|
||||
v-model="offeredToProject"
|
||||
type="checkbox"
|
||||
class="h-6 w-6 mr-2"
|
||||
v-model="offeredToProject"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="square"
|
||||
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
|
||||
@@ -116,11 +116,11 @@
|
||||
<div class="h-7 mt-4 flex">
|
||||
<input
|
||||
v-if="recipientDid && !offeredToProject"
|
||||
v-model="offeredToRecipient"
|
||||
type="checkbox"
|
||||
class="h-6 w-6 mr-2"
|
||||
v-model="offeredToRecipient"
|
||||
/>
|
||||
<fa
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="square"
|
||||
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
|
||||
@@ -151,7 +151,7 @@
|
||||
|
||||
<p class="text-center mb-2 mt-6 italic">
|
||||
Sign & Send to publish to the world
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
@click="explainData()"
|
||||
@@ -176,24 +176,55 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { GenericCredWrapper, OfferVerifiableCredential } from "../interfaces";
|
||||
import {
|
||||
createAndSubmitOffer,
|
||||
didInfo,
|
||||
editAndSubmitOffer,
|
||||
GenericCredWrapper,
|
||||
getPlanFromCache,
|
||||
hydrateOffer,
|
||||
OfferVerifiableCredential,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
/**
|
||||
* Offer Details View Component
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This component handles the creation and editing of offers within the platform.
|
||||
* It supports both new offers and editing existing ones, with validation and
|
||||
* submission handling.
|
||||
*
|
||||
* Features:
|
||||
* - Offer amount and unit selection
|
||||
* - Item description
|
||||
* - Conditional requirements
|
||||
* - Expiration date setting
|
||||
* - Project or recipient targeting
|
||||
* - Raw claim editing option
|
||||
*
|
||||
* Data Flow:
|
||||
* 1. Component loads with optional previous offer data
|
||||
* 2. Retrieves account settings and contact information
|
||||
* 3. Populates form with existing or default values
|
||||
* 4. Validates and submits offer to server
|
||||
* 5. Redirects on success or shows error
|
||||
*
|
||||
* Security Features:
|
||||
* - DID validation
|
||||
* - JWT handling for edits
|
||||
* - Server-side validation
|
||||
* - Privacy controls for data sharing
|
||||
*
|
||||
* @see GiftedDialog for related gift creation
|
||||
* @see ClaimAddRawView for raw claim editing
|
||||
*/
|
||||
@Component({
|
||||
components: {
|
||||
QuickNav,
|
||||
@@ -201,40 +232,103 @@ import { retrieveAccountDids } from "../libs/util";
|
||||
},
|
||||
})
|
||||
export default class OfferDetailsView extends Vue {
|
||||
/** Notification function injected by Vue */
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
/** Current route instance */
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
/** Router instance for navigation */
|
||||
$router!: Router;
|
||||
|
||||
/** Currently active DID */
|
||||
activeDid = "";
|
||||
/** API server endpoint */
|
||||
apiServer = "";
|
||||
|
||||
/** Offer amount input field */
|
||||
amountInput = "0";
|
||||
/** Conditions for the offer */
|
||||
descriptionOfCondition = "";
|
||||
/** Description of offered item */
|
||||
descriptionOfItem = "";
|
||||
/** Path to redirect after completion */
|
||||
destinationPathAfter = "";
|
||||
/** Controls back button visibility */
|
||||
hideBackButton = false;
|
||||
/** Additional message to display */
|
||||
message = "";
|
||||
/** Flag for project assignment */
|
||||
offeredToProject = false;
|
||||
/** Flag for recipient assignment */
|
||||
offeredToRecipient = false;
|
||||
/** DID of offer creator */
|
||||
offererDid: string | undefined;
|
||||
/** Offer ID for editing */
|
||||
offerId = "";
|
||||
/** Previous offer data for editing */
|
||||
prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>;
|
||||
/** Project ID if offer is for project */
|
||||
projectId = "";
|
||||
/** Project name display */
|
||||
projectName = "a project";
|
||||
/** Recipient DID if offer is for person */
|
||||
recipientDid = "";
|
||||
/** Recipient name display */
|
||||
recipientName = "";
|
||||
/** Advanced features visibility flag */
|
||||
showGeneralAdvanced = false;
|
||||
/** Unit type for offer amount */
|
||||
unitCode = "HUR";
|
||||
/** Expiration date input */
|
||||
validThroughDateInput = "";
|
||||
|
||||
/** Utility library reference */
|
||||
libsUtil = libsUtil;
|
||||
|
||||
/**
|
||||
* Component lifecycle hook that initializes the offer form
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Extracts previous offer data if editing
|
||||
* 2. Sets initial form values from route or previous offer
|
||||
* 3. Loads account settings and contacts
|
||||
* 4. Retrieves project information if needed
|
||||
* 5. Sets offer assignment flags
|
||||
*
|
||||
* @throws Will not throw but shows notifications
|
||||
* @emits Notifications on loading errors
|
||||
*/
|
||||
async mounted() {
|
||||
try {
|
||||
this.prevCredToEdit = (this.$route as Router).query["prevCredToEdit"]
|
||||
await this.loadPreviousOffer();
|
||||
await this.initializeFormValues();
|
||||
await this.loadAccountSettings();
|
||||
await this.loadRecipientInfo();
|
||||
await this.loadProjectInfo();
|
||||
} catch (err: unknown) {
|
||||
logger.error("Error in mounted:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: err.message || "There was an error loading the offer details.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads previous offer data if editing an existing offer
|
||||
* @throws Will not throw but shows notifications
|
||||
*/
|
||||
private async loadPreviousOffer() {
|
||||
try {
|
||||
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
|
||||
? (JSON.parse(
|
||||
(this.$route as Router).query["prevCredToEdit"],
|
||||
this.$route.query["prevCredToEdit"] as string,
|
||||
) as GenericCredWrapper<OfferVerifiableCredential>)
|
||||
: undefined;
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -245,34 +339,38 @@ export default class OfferDetailsView extends Vue {
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes form values from route params or previous offer
|
||||
*/
|
||||
private async initializeFormValues() {
|
||||
const prevAmount =
|
||||
this.prevCredToEdit?.claim?.includesObject?.amountOfThisGood;
|
||||
this.amountInput =
|
||||
(this.$route as Router).query["amountInput"] ||
|
||||
(this.$route.query["amountInput"] as string) ||
|
||||
(prevAmount ? String(prevAmount) : "") ||
|
||||
this.amountInput;
|
||||
this.unitCode = ((this.$route as Router).query["unitCode"] ||
|
||||
|
||||
this.unitCode = ((this.$route.query["unitCode"] as string) ||
|
||||
this.prevCredToEdit?.claim?.includesObject?.unitCode ||
|
||||
this.unitCode) as string;
|
||||
|
||||
this.descriptionOfCondition =
|
||||
this.prevCredToEdit?.claim?.description || this.descriptionOfCondition;
|
||||
|
||||
this.descriptionOfItem =
|
||||
(this.$route as Router).query["description"] ||
|
||||
(this.$route.query["description"] as string) ||
|
||||
this.prevCredToEdit?.claim?.itemOffered?.description ||
|
||||
this.descriptionOfItem;
|
||||
this.destinationPathAfter = (this.$route as Router).query[
|
||||
"destinationPathAfter"
|
||||
];
|
||||
this.offererDid = ((this.$route as Router).query["offererDid"] ||
|
||||
this.prevCredToEdit?.claim?.agent?.identifier ||
|
||||
this.offererDid) as string;
|
||||
this.hideBackButton =
|
||||
(this.$route as Router).query["hideBackButton"] === "true";
|
||||
this.message = ((this.$route as Router).query["message"] as string) || "";
|
||||
|
||||
// find any project ID
|
||||
this.destinationPathAfter =
|
||||
(this.$route.query["destinationPathAfter"] as string) || "";
|
||||
this.hideBackButton =
|
||||
(this.$route.query["hideBackButton"] as string) === "true";
|
||||
this.message = (this.$route.query["message"] as string) || "";
|
||||
|
||||
// Set project info from previous offer or route
|
||||
let project;
|
||||
if (
|
||||
this.prevCredToEdit?.claim?.itemOffered?.isPartOf?.["@type"] ===
|
||||
@@ -280,57 +378,56 @@ export default class OfferDetailsView extends Vue {
|
||||
) {
|
||||
project = this.prevCredToEdit?.claim?.itemOffered?.isPartOf;
|
||||
}
|
||||
this.projectId = ((this.$route as Router).query["projectId"] ||
|
||||
this.projectId = ((this.$route.query["projectId"] as string) ||
|
||||
project?.identifier ||
|
||||
this.projectId) as string;
|
||||
this.projectName = ((this.$route as Router).query["projectName"] ||
|
||||
this.projectName = ((this.$route.query["projectName"] as string) ||
|
||||
project?.name ||
|
||||
this.projectName) as string;
|
||||
|
||||
this.recipientDid = ((this.$route as Router).query["recipientDid"] ||
|
||||
this.recipientDid = ((this.$route.query["recipientDid"] as string) ||
|
||||
this.prevCredToEdit?.claim?.recipient?.identifier) as string;
|
||||
this.recipientName =
|
||||
((this.$route as Router).query["recipientName"] as string) || "";
|
||||
this.recipientName = (this.$route.query["recipientName"] as string) || "";
|
||||
|
||||
this.validThroughDateInput =
|
||||
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer ?? "";
|
||||
this.activeDid = settings.activeDid ?? "";
|
||||
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
|
||||
/**
|
||||
* Loads account settings and updates component state
|
||||
* @throws Will not throw but logs errors
|
||||
*/
|
||||
private async loadAccountSettings() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer ?? "";
|
||||
this.activeDid = settings.activeDid ?? "";
|
||||
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
|
||||
}
|
||||
|
||||
if (this.recipientDid && !this.recipientName) {
|
||||
const allContacts = await db.contacts.toArray();
|
||||
const allMyDids = await retrieveAccountDids();
|
||||
this.recipientName = didInfo(
|
||||
this.recipientDid,
|
||||
this.activeDid,
|
||||
allMyDids,
|
||||
allContacts,
|
||||
);
|
||||
}
|
||||
// these should be functions but something's wrong with the syntax in the <> conditional
|
||||
this.offeredToProject = !!this.projectId;
|
||||
this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: err.message || "There was an error retrieving your settings.",
|
||||
},
|
||||
5000,
|
||||
/**
|
||||
* Loads recipient information if recipient DID exists
|
||||
*/
|
||||
private async loadRecipientInfo() {
|
||||
if (this.recipientDid && !this.recipientName) {
|
||||
const allContacts = await db.contacts.toArray();
|
||||
const allMyDids = await retrieveAccountDids();
|
||||
this.recipientName = didInfo(
|
||||
this.recipientDid,
|
||||
this.activeDid,
|
||||
allMyDids,
|
||||
allContacts,
|
||||
);
|
||||
}
|
||||
// Set assignment flags
|
||||
this.offeredToProject = !!this.projectId;
|
||||
this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads project information if project ID exists
|
||||
*/
|
||||
private async loadProjectInfo() {
|
||||
if (this.projectId && !this.projectName) {
|
||||
// console.log("Getting project name from cache", this.projectId);
|
||||
const project = await getPlanFromCache(
|
||||
this.projectId,
|
||||
this.axios,
|
||||
@@ -343,16 +440,32 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the unit type for the offer amount
|
||||
*
|
||||
* Cycles through available unit types in UNIT_SHORT.
|
||||
* Updates display and internal state.
|
||||
*/
|
||||
changeUnitCode() {
|
||||
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||
const index = units.indexOf(this.unitCode);
|
||||
this.unitCode = units[(index + 1) % units.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the offer amount by 1
|
||||
*
|
||||
* Handles string to number conversion and updates display.
|
||||
*/
|
||||
increment() {
|
||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements the offer amount by 1
|
||||
*
|
||||
* Prevents negative values and handles string to number conversion.
|
||||
*/
|
||||
decrement() {
|
||||
this.amountInput = `${Math.max(
|
||||
0,
|
||||
@@ -360,6 +473,15 @@ export default class OfferDetailsView extends Vue {
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles cancellation of offer creation/editing
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Checks for destination path
|
||||
* 2. Navigates to destination or previous page
|
||||
*
|
||||
* @emits Router navigation
|
||||
*/
|
||||
cancel() {
|
||||
if (this.destinationPathAfter) {
|
||||
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||
@@ -368,10 +490,28 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles back button navigation
|
||||
*
|
||||
* @emits Router navigation to previous page
|
||||
*/
|
||||
cancelBack() {
|
||||
(this.$router as Router).back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and initiates offer submission
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Validates active DID exists
|
||||
* 2. Checks for negative amounts
|
||||
* 3. Ensures description or amount exists
|
||||
* 4. Shows processing notification
|
||||
* 5. Calls recordOffer for submission
|
||||
*
|
||||
* @throws Will not throw but shows notifications
|
||||
* @emits Notifications for validation errors or processing
|
||||
*/
|
||||
async confirm() {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
@@ -426,6 +566,15 @@ export default class OfferDetailsView extends Vue {
|
||||
await this.recordOffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies user about project assignment restrictions
|
||||
*
|
||||
* Shows appropriate error message based on:
|
||||
* - Missing project ID
|
||||
* - Conflict with recipient assignment
|
||||
*
|
||||
* @emits Notification with error message
|
||||
*/
|
||||
notifyUserOfProject() {
|
||||
if (!this.projectId) {
|
||||
this.$notify(
|
||||
@@ -451,6 +600,15 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies user about recipient assignment restrictions
|
||||
*
|
||||
* Shows appropriate error message based on:
|
||||
* - Missing recipient DID
|
||||
* - Conflict with project assignment
|
||||
*
|
||||
* @emits Notification with error message
|
||||
*/
|
||||
notifyUserOfRecipient() {
|
||||
if (!this.recipientDid) {
|
||||
this.$notify(
|
||||
@@ -477,11 +635,18 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the offer to the server
|
||||
*
|
||||
* @param offererDid may be null
|
||||
* @param description may be an empty string
|
||||
* @param amountInput may be 0
|
||||
* @param unitCode may be omitted, defaults to "HUR"
|
||||
* Workflow:
|
||||
* 1. Determines if editing existing or creating new
|
||||
* 2. Prepares offer data with assignments
|
||||
* 3. Submits to server via appropriate method
|
||||
* 4. Handles success/error responses
|
||||
* 5. Navigates on success
|
||||
*
|
||||
* @throws Will not throw but shows notifications
|
||||
* @emits Notifications for success/failure
|
||||
* @emits Router navigation on success
|
||||
*/
|
||||
public async recordOffer() {
|
||||
try {
|
||||
@@ -522,7 +687,7 @@ export default class OfferDetailsView extends Vue {
|
||||
|
||||
if (result.type === "error" || this.isCreationError(result.response)) {
|
||||
const errorMessage = this.getCreationErrorMessage(result);
|
||||
console.error("Error with offer creation result:", result);
|
||||
logger.error("Error with offer creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -551,7 +716,7 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Error with offer recordation caught:", error);
|
||||
logger.error("Error with offer recordation caught:", error);
|
||||
const errorMessage =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
@@ -568,6 +733,17 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs offer parameters for raw editing
|
||||
*
|
||||
* Creates a JSON string containing:
|
||||
* - Offer details
|
||||
* - Assignments
|
||||
* - Conditions
|
||||
* - Expiration
|
||||
*
|
||||
* @returns JSON string of offer parameters
|
||||
*/
|
||||
constructOfferParam() {
|
||||
const recipientDid = this.offeredToRecipient
|
||||
? this.recipientDid
|
||||
@@ -589,11 +765,11 @@ export default class OfferDetailsView extends Vue {
|
||||
return claimStr;
|
||||
}
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
/**
|
||||
* @param result response "data" from the server
|
||||
* @returns true if the result indicates an error
|
||||
* Checks if server response indicates an error
|
||||
*
|
||||
* @param result Response data from server
|
||||
* @returns true if response indicates error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isCreationError(result: any) {
|
||||
@@ -601,8 +777,10 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||
* @returns best guess at an error message
|
||||
* Extracts error message from server response
|
||||
*
|
||||
* @param result Server response object
|
||||
* @returns Best available error message
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getCreationErrorMessage(result: any) {
|
||||
@@ -613,6 +791,13 @@ export default class OfferDetailsView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows privacy information dialog
|
||||
*
|
||||
* Displays standard privacy message about data sharing.
|
||||
*
|
||||
* @emits Notification with privacy message
|
||||
*/
|
||||
explainData() {
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="flex justify-center items-center py-8">
|
||||
<fa icon="spinner" class="fa-spin-pulse" />
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="attendingMeeting">
|
||||
@@ -22,11 +22,11 @@
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-medium">{{ attendingMeeting.name }}</h2>
|
||||
<button
|
||||
@click.stop="leaveMeeting"
|
||||
class="text-red-600 hover:text-red-700 p-2"
|
||||
title="Leave Meeting"
|
||||
@click.stop="leaveMeeting"
|
||||
>
|
||||
<fa icon="right-from-bracket" />
|
||||
<font-awesome icon="right-from-bracket" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,14 +65,14 @@
|
||||
/>
|
||||
<div class="flex justify-end space-x-4">
|
||||
<button
|
||||
@click="cancelPasswordDialog"
|
||||
class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
|
||||
@click="cancelPasswordDialog"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="submitPassword"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||
@click="submitPassword"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
@@ -120,6 +120,7 @@ export default class OnboardMeetingListView extends Vue {
|
||||
},
|
||||
timeout?: number,
|
||||
) => void;
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
@@ -257,7 +258,7 @@ export default class OnboardMeetingListView extends Vue {
|
||||
|
||||
if (postResult.data && postResult.data.success) {
|
||||
// Navigate to members view with password and groupId
|
||||
(this.$router as Router).push({
|
||||
this.$router.push({
|
||||
name: "onboard-meeting-members",
|
||||
params: {
|
||||
groupId: this.selectedMeeting.groupId.toString(),
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
|
||||
<!-- Loading Animation -->
|
||||
<div
|
||||
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
|
||||
v-if="isLoading"
|
||||
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
|
||||
>
|
||||
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocation } from "vue-router";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
@@ -69,17 +69,17 @@ export default class OnboardMeetingMembersView extends Vue {
|
||||
firstName = "";
|
||||
isRegistered = false;
|
||||
isLoading = true;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
$router!: Router;
|
||||
|
||||
$refs!: {
|
||||
userNameDialog: InstanceType<typeof UserNameDialog>;
|
||||
};
|
||||
userNameDialog!: InstanceType<typeof UserNameDialog>;
|
||||
|
||||
get groupId(): string {
|
||||
return (this.$route as RouteLocation).params.groupId as string;
|
||||
return (this.$route.params.groupId as string) || "";
|
||||
}
|
||||
|
||||
get password(): string {
|
||||
return (this.$route as RouteLocation).query.password as string;
|
||||
return (this.$route.query.password as string) || "";
|
||||
}
|
||||
|
||||
async created() {
|
||||
|
||||
@@ -17,24 +17,24 @@
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-2xl">Current Meeting</h2>
|
||||
<button
|
||||
@click="startEditing"
|
||||
class="mb-4 text-blue-600 hover:text-blue-800 transition-colors duration-200 ml-2"
|
||||
title="Edit Meeting"
|
||||
@click="startEditing"
|
||||
>
|
||||
<fa icon="pen" class="fa-fw" />
|
||||
<font-awesome icon="pen" class="fa-fw" />
|
||||
<span class="sr-only">{{
|
||||
isInCreateMode() ? "Create Meeting" : "Edit Meeting"
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="confirmDelete"
|
||||
class="text-red-600 hover:text-red-800 transition-colors duration-200"
|
||||
:disabled="isDeleting"
|
||||
:class="{ 'opacity-50 cursor-not-allowed': isDeleting }"
|
||||
title="Delete Meeting"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
<fa icon="trash-can" class="fa-fw" />
|
||||
<font-awesome icon="trash-can" class="fa-fw" />
|
||||
<span class="sr-only">{{
|
||||
isDeleting ? "Deleting..." : "Delete Meeting"
|
||||
}}</span>
|
||||
@@ -72,14 +72,14 @@
|
||||
</p>
|
||||
<div class="flex justify-between space-x-4">
|
||||
<button
|
||||
@click="showDeleteConfirm = false"
|
||||
class="px-4 py-2 bg-slate-500 text-white rounded hover:bg-slate-700"
|
||||
@click="showDeleteConfirm = false"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@click="deleteMeeting"
|
||||
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
||||
@click="deleteMeeting"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
@@ -99,10 +99,13 @@
|
||||
<h2 class="text-2xl mb-4">
|
||||
{{ isInCreateMode() ? "Create New Meeting" : "Edit Meeting" }}
|
||||
</h2>
|
||||
<!-- This is my first form. Not sure if I like it; will see if the browser benefits extend to the native app. -->
|
||||
<!--
|
||||
This is my first form. Not sure if I like it; will see if the browser benefits extend to
|
||||
the native app.
|
||||
-->
|
||||
<form
|
||||
@submit.prevent="isInCreateMode() ? createMeeting() : updateMeeting()"
|
||||
class="space-y-4"
|
||||
@submit.prevent="isInCreateMode() ? createMeeting() : updateMeeting()"
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
@@ -182,8 +185,8 @@
|
||||
<button
|
||||
v-if="isInEditOrCreateMode()"
|
||||
type="button"
|
||||
@click="cancelEditing"
|
||||
class="w-full bg-slate-500 text-white px-4 py-2 rounded-md hover:bg-slate-600"
|
||||
@click="cancelEditing"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -204,20 +207,21 @@
|
||||
class="inline-block text-blue-600"
|
||||
target="_blank"
|
||||
>
|
||||
• Open shortcut page for members <fa icon="external-link" />
|
||||
• Open shortcut page for members
|
||||
<font-awesome icon="external-link" />
|
||||
</router-link>
|
||||
|
||||
<MembersList
|
||||
:password="currentMeeting.password || ''"
|
||||
:show-organizer-tools="true"
|
||||
@error="handleMembersError"
|
||||
class="mt-4"
|
||||
@error="handleMembersError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isLoading">
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<fa icon="spinner" class="fa-spin-pulse" />
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -235,7 +239,7 @@ import {
|
||||
serverMessageForUser,
|
||||
} from "../libs/endorserServer";
|
||||
import { encryptMessage } from "../libs/crypto";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
interface ServerMeeting {
|
||||
groupId: number; // from the server
|
||||
name: string; // from the server
|
||||
@@ -511,7 +515,7 @@ export default class OnboardMeetingView extends Vue {
|
||||
3000,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error deleting meeting:", error);
|
||||
logger.error("Error deleting meeting:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -537,7 +541,7 @@ export default class OnboardMeetingView extends Vue {
|
||||
password: this.currentMeeting.password || "",
|
||||
};
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"There is no current meeting to edit. We should never get here.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
<h1 class="text-center text-lg font-light relative px-7">
|
||||
<!-- Back -->
|
||||
<button
|
||||
@click="$router.go(-1)"
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
Project Idea
|
||||
</h1>
|
||||
@@ -21,11 +21,11 @@
|
||||
{{ name }}
|
||||
<button
|
||||
v-if="activeDid === issuer || activeDid === agentDid"
|
||||
@click="onEditClick()"
|
||||
title="Edit"
|
||||
data-testId="editClaimButton"
|
||||
@click="onEditClick()"
|
||||
>
|
||||
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
@@ -37,10 +37,10 @@
|
||||
<div class="pb-4 flex gap-4">
|
||||
<div class="pt-1">
|
||||
<ProjectIcon
|
||||
:entityId="projectId"
|
||||
:iconSize="64"
|
||||
:imageUrl="imageUrl"
|
||||
:linkToFull="true"
|
||||
:entity-id="projectId"
|
||||
:icon-size="64"
|
||||
:image-url="imageUrl"
|
||||
:link-to-full="true"
|
||||
class="block border border-slate-300 rounded-md max-h-16 max-w-16"
|
||||
/>
|
||||
</div>
|
||||
@@ -48,7 +48,10 @@
|
||||
<div class="overflow-hidden">
|
||||
<div class="text-sm mb-3">
|
||||
<div class="truncate">
|
||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||
<font-awesome
|
||||
icon="user"
|
||||
class="fa-fw text-slate-400"
|
||||
></font-awesome>
|
||||
{{ issuerInfoObject?.displayName }}
|
||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
|
||||
<a
|
||||
@@ -56,11 +59,14 @@
|
||||
target="_blank"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
<span v-else-if="serverUtil.isHiddenDid(issuer)">
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="info-circle"
|
||||
class="fa-fw text-blue-500 cursor-pointer"
|
||||
@click="openHiddenDidDialog()"
|
||||
@@ -68,35 +74,50 @@
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="startTime">
|
||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||
<font-awesome
|
||||
icon="calendar"
|
||||
class="fa-fw text-slate-400"
|
||||
></font-awesome>
|
||||
Starts {{ startTime }}
|
||||
</div>
|
||||
<div v-if="endTime">
|
||||
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
|
||||
<font-awesome
|
||||
icon="calendar"
|
||||
class="fa-fw text-slate-400"
|
||||
></font-awesome>
|
||||
Ends {{ endTime }}
|
||||
</div>
|
||||
<div v-if="latitude || longitude">
|
||||
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
|
||||
<font-awesome
|
||||
icon="location-dot"
|
||||
class="fa-fw text-slate-400"
|
||||
></font-awesome>
|
||||
<a
|
||||
:href="getOpenStreetMapUrl()"
|
||||
target="_blank"
|
||||
class="underline text-blue-500"
|
||||
>Map View
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw text-blue-500"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="url">
|
||||
<fa icon="globe" class="fa-fw text-slate-400"></fa>
|
||||
<font-awesome
|
||||
icon="globe"
|
||||
class="fa-fw text-slate-400"
|
||||
></font-awesome>
|
||||
<a
|
||||
:href="addScheme(url)"
|
||||
target="_blank"
|
||||
class="underline text-blue-500"
|
||||
>
|
||||
{{ domainForWebsite(this.url) }}
|
||||
<fa icon="arrow-up-right-from-square" class="fa-fw" />
|
||||
{{ domainForWebsite(url) }}
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,23 +129,23 @@
|
||||
{{ truncatedDesc }}
|
||||
<a
|
||||
v-if="description.length >= truncateLength"
|
||||
@click="expandText"
|
||||
class="uppercase text-xs font-semibold text-slate-700"
|
||||
@click="expandText"
|
||||
>... Read More</a
|
||||
>
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ description }}
|
||||
<a
|
||||
@click="collapseText"
|
||||
class="uppercase text-xs font-semibold text-slate-700"
|
||||
@click="collapseText"
|
||||
>- Read Less</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a @click="onClickLoadClaim(projectId)" class="cursor-pointer">
|
||||
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
|
||||
<a class="cursor-pointer" @click="onClickLoadClaim(projectId)">
|
||||
<font-awesome icon="file-lines" class="pl-2 pt-1 text-blue-500" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,12 +159,15 @@
|
||||
<h3 class="text-sm uppercase font-semibold mt-3">
|
||||
Projects That Contribute To This
|
||||
</h3>
|
||||
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
||||
<!--
|
||||
centering because long, wrapped project names didn't left align with blank
|
||||
or "text-left"
|
||||
-->
|
||||
<div class="text-center">
|
||||
<div v-for="plan in fulfillersToThis" :key="plan.handleId">
|
||||
<button
|
||||
@click="onClickLoadProject(plan.handleId)"
|
||||
class="text-blue-500"
|
||||
@click="onClickLoadProject(plan.handleId)"
|
||||
>
|
||||
{{ plan.name }}
|
||||
</button>
|
||||
@@ -160,11 +184,14 @@
|
||||
<h3 class="text-sm uppercase font-semibold mb-3">
|
||||
Projects Getting Contributions From This
|
||||
</h3>
|
||||
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
|
||||
<!--
|
||||
centering because long, wrapped project names didn't left align with blank
|
||||
or "text-left"
|
||||
-->
|
||||
<div class="text-center">
|
||||
<button
|
||||
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
||||
class="text-blue-500"
|
||||
@click="onClickLoadProject(fulfilledByThis.handleId)"
|
||||
>
|
||||
{{ fulfilledByThis.name }}
|
||||
</button>
|
||||
@@ -181,7 +208,10 @@
|
||||
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5 mt-2"
|
||||
>
|
||||
<li @click="openGiftDialogToProject({ name: 'you', did: activeDid })">
|
||||
<fa icon="hand" class="fa-fw text-blue-500 text-5xl cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="hand"
|
||||
class="fa-fw text-blue-500 text-5xl cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
class="mt-5 text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
|
||||
>
|
||||
@@ -206,7 +236,7 @@
|
||||
>
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:iconSize="64"
|
||||
:icon-size="64"
|
||||
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
|
||||
/>
|
||||
<h3
|
||||
@@ -218,14 +248,14 @@
|
||||
<li>
|
||||
<span
|
||||
v-if="allContacts.length >= 5"
|
||||
@click="onClickAllContactsGifting()"
|
||||
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
|
||||
@click="onClickAllContactsGifting()"
|
||||
>
|
||||
... or someone else...
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<GiftedDialog ref="giveDialogToThis" :toProjectId="this.projectId" />
|
||||
<GiftedDialog ref="giveDialogToThis" :to-project-id="projectId" />
|
||||
</div>
|
||||
|
||||
<!-- Offers & Gifts to & from this -->
|
||||
@@ -236,8 +266,8 @@
|
||||
<div class="text-center">
|
||||
<button
|
||||
data-testId="offerButton"
|
||||
@click="openOfferDialog()"
|
||||
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
|
||||
@click="openOfferDialog()"
|
||||
>
|
||||
Offer to this (maybe with conditions)...
|
||||
</button>
|
||||
@@ -245,15 +275,15 @@
|
||||
</div>
|
||||
<OfferDialog
|
||||
ref="customOfferDialog"
|
||||
:projectId="this.projectId"
|
||||
:projectName="this.name"
|
||||
:project-id="projectId"
|
||||
:project-name="name"
|
||||
/>
|
||||
|
||||
<h3 class="text-lg font-bold mb-3 mt-4">Offered To This Idea</h3>
|
||||
|
||||
<div v-if="offersToThis.length === 0">
|
||||
(None yet. Wanna
|
||||
<span @click="openOfferDialog()" class="cursor-pointer text-blue-500"
|
||||
<span class="cursor-pointer text-blue-500" @click="openOfferDialog()"
|
||||
>offer something... especially if others join you</span
|
||||
>?)
|
||||
</div>
|
||||
@@ -261,12 +291,15 @@
|
||||
<ul v-else class="text-sm border-t border-slate-300">
|
||||
<li
|
||||
v-for="offer in offersToThis"
|
||||
:key="offer.id"
|
||||
:key="offer.jwtId"
|
||||
class="py-1.5 border-b border-slate-300"
|
||||
>
|
||||
<div class="flex justify-between gap-4">
|
||||
<span>
|
||||
<fa icon="user" class="fa-fw text-slate-400"></fa>
|
||||
<font-awesome
|
||||
icon="user"
|
||||
class="fa-fw text-slate-400"
|
||||
></font-awesome>
|
||||
{{
|
||||
serverUtil.didInfo(
|
||||
offer.offeredByDid,
|
||||
@@ -277,28 +310,31 @@
|
||||
}}
|
||||
</span>
|
||||
<span v-if="offer.amount" class="whitespace-nowrap">
|
||||
<fa
|
||||
<font-awesome
|
||||
:icon="libsUtil.iconForUnitCode(offer.unit)"
|
||||
class="fa-fw text-slate-400"
|
||||
/>{{ offer.amount }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="offer.objectDescription" class="text-slate-500">
|
||||
<fa icon="comment" class="fa-fw text-slate-400" />
|
||||
<font-awesome icon="comment" class="fa-fw text-slate-400" />
|
||||
{{ offer.objectDescription }}
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<a
|
||||
@click="onClickLoadClaim(offer.jwtId as string)"
|
||||
class="cursor-pointer"
|
||||
@click="onClickLoadClaim(offer.jwtId as string)"
|
||||
>
|
||||
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="pl-2 pt-1 text-blue-500"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-if="checkIsFulfillable(offer)"
|
||||
@click="onClickFulfillGiveToOffer(offer)"
|
||||
>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="hand-holding-heart"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
/>
|
||||
@@ -317,8 +353,8 @@
|
||||
<div v-if="activeDid && isRegistered">
|
||||
<div class="text-center">
|
||||
<button
|
||||
@click="openGiftDialogToProject()"
|
||||
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1rounded-md"
|
||||
@click="openGiftDialogToProject()"
|
||||
>
|
||||
Given To This...
|
||||
</button>
|
||||
@@ -342,8 +378,8 @@
|
||||
<span class="font-semibold mr-2 shrink-0">Totals</span>
|
||||
<span class="whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
<a
|
||||
@click="totalsExpanded = !totalsExpanded"
|
||||
class="cursor-pointer text-blue-500"
|
||||
@click="totalsExpanded = !totalsExpanded"
|
||||
>
|
||||
<!-- just show the hours, or alternatively whatever is first -->
|
||||
<span v-if="givenTotalHours() > 0">
|
||||
@@ -469,17 +505,14 @@
|
||||
<div v-if="activeDid && isRegistered">
|
||||
<div class="text-center">
|
||||
<button
|
||||
@click="openGiftDialogFromProject()"
|
||||
class="block w-full bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
|
||||
@click="openGiftDialogFromProject()"
|
||||
>
|
||||
Given By This...
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<GiftedDialog
|
||||
ref="giveDialogFromThis"
|
||||
:fromProjectId="this.projectId"
|
||||
/>
|
||||
<GiftedDialog ref="giveDialogFromThis" :from-project-id="projectId" />
|
||||
|
||||
<h3 class="text-lg font-bold mb-3 mt-4">
|
||||
Benefitted From This Project
|
||||
@@ -490,7 +523,7 @@
|
||||
<ul v-else class="text-sm border-t border-slate-300">
|
||||
<li
|
||||
v-for="give in givesProvidedByThis"
|
||||
:key="give.id"
|
||||
:key="give.jwtId"
|
||||
class="py-1.5 border-b border-slate-300"
|
||||
>
|
||||
<div class="flex justify-between gap-4">
|
||||
@@ -505,23 +538,26 @@
|
||||
}}
|
||||
</span>
|
||||
<span v-if="give.amount" class="whitespace-nowrap">
|
||||
<fa
|
||||
<font-awesome
|
||||
:icon="libsUtil.iconForUnitCode(give.unit)"
|
||||
class="fa-fw text-slate-400"
|
||||
/>{{ give.amount }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-slate-500">
|
||||
<fa icon="calendar" class="fa-fw text-slate-400" />
|
||||
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
|
||||
{{ give.issuedAt?.substring(0, 10) }}
|
||||
</div>
|
||||
<div v-if="give.description" class="text-slate-500">
|
||||
<fa icon="comment" class="fa-fw text-slate-400" />
|
||||
<font-awesome icon="comment" class="fa-fw text-slate-400" />
|
||||
{{ give.description }}
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<a @click="onClickLoadClaim(give.jwtId)">
|
||||
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a
|
||||
@@ -531,13 +567,19 @@
|
||||
"
|
||||
@click="deepCheckConfirmable(give)"
|
||||
>
|
||||
<fa icon="circle-check" class="text-blue-500 cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="circle-check"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
|
||||
<fa icon="spinner" class="fa-spin-pulse" />
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
</a>
|
||||
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
|
||||
<fa icon="circle-check" class="text-slate-500 cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="circle-check"
|
||||
class="text-slate-500 cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="give.fullClaim.image" class="flex justify-center">
|
||||
@@ -561,7 +603,15 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import {
|
||||
GenericVerifiableCredential,
|
||||
GenericCredWrapper,
|
||||
GiveSummaryRecord,
|
||||
GiveVerifiableCredential,
|
||||
OfferSummaryRecord,
|
||||
OfferVerifiableCredential,
|
||||
PlanSummaryRecord,
|
||||
} from "../interfaces";
|
||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||
import OfferDialog from "../components/OfferDialog.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
@@ -576,18 +626,42 @@ import {
|
||||
} from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import {
|
||||
GenericCredWrapper,
|
||||
GiveSummaryRecord,
|
||||
GiveVerifiableCredential,
|
||||
OfferSummaryRecord,
|
||||
OfferVerifiableCredential,
|
||||
PlanSummaryRecord,
|
||||
} from "../libs/endorserServer";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
/**
|
||||
* Project View Component
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This component displays and manages detailed project information. It handles:
|
||||
* - Project loading and display from URL-encoded project handles
|
||||
* - Project metadata (name, description, dates, location)
|
||||
* - Issuer information and verification
|
||||
* - Project contributions and fulfillments
|
||||
* - Offers and gifts tracking
|
||||
* - Contact interactions
|
||||
*
|
||||
* Data Flow:
|
||||
* 1. Component loads with project ID from route
|
||||
* 2. Fetches project data, contacts, and account settings
|
||||
* 3. Loads related data (offers, gifts, fulfillments)
|
||||
* 4. Updates UI with paginated results
|
||||
*
|
||||
* Security Features:
|
||||
* - DID visibility controls
|
||||
* - JWT validation for imports
|
||||
* - Permission checks for actions
|
||||
*
|
||||
* State Management:
|
||||
* - Maintains separate loading states for different data types
|
||||
* - Handles pagination limits
|
||||
* - Tracks confirmation states
|
||||
*
|
||||
* @see GiftedDialog for gift creation
|
||||
* @see OfferDialog for offer creation
|
||||
* @see HiddenDidDialog for DID privacy explanations
|
||||
*/
|
||||
@Component({
|
||||
components: {
|
||||
EntityIcon,
|
||||
@@ -600,52 +674,103 @@ import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
||||
},
|
||||
})
|
||||
export default class ProjectViewView extends Vue {
|
||||
/** Notification function injected by Vue */
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
/** Router instance for navigation */
|
||||
$router!: Router;
|
||||
|
||||
// Account and Settings State
|
||||
/** Currently active DID */
|
||||
activeDid = "";
|
||||
/** Project agent DID */
|
||||
agentDid = "";
|
||||
/** DIDs that can see the agent DID */
|
||||
agentDidVisibleToDids: Array<string> = [];
|
||||
/** All DIDs associated with current account */
|
||||
allMyDids: Array<string> = [];
|
||||
/** All known contacts */
|
||||
allContacts: Array<Contact> = [];
|
||||
/** API server endpoint */
|
||||
apiServer = "";
|
||||
checkingConfirmationForJwtId = "";
|
||||
/** Registration status of current user */
|
||||
isRegistered = false;
|
||||
|
||||
// Project Data
|
||||
/** Project description */
|
||||
description = "";
|
||||
/** Project end time */
|
||||
endTime = "";
|
||||
/** Text expansion state */
|
||||
expanded = false;
|
||||
/** Project fulfilled by this project */
|
||||
fulfilledByThis: PlanSummaryRecord | null = null;
|
||||
/** Projects fulfilling this project */
|
||||
fulfillersToThis: Array<PlanSummaryRecord> = [];
|
||||
/** Flag for fulfiller pagination */
|
||||
fulfillersToHitLimit = false;
|
||||
/** Gifts to this project */
|
||||
givesToThis: Array<GiveSummaryRecord> = [];
|
||||
givesHitLimit = false;
|
||||
givesProvidedByThis: Array<GiveSummaryRecord> = [];
|
||||
givesProvidedByHitLimit = false;
|
||||
givesTotalsByUnit: Array<{ unit: string; amount: number }> = [];
|
||||
imageUrl = "";
|
||||
isRegistered = false;
|
||||
/** Project issuer DID */
|
||||
issuer = "";
|
||||
/** Cached issuer information */
|
||||
issuerInfoObject: {
|
||||
known: boolean;
|
||||
displayName: string;
|
||||
profileImageUrl?: string;
|
||||
} | null = null;
|
||||
/** DIDs that can see issuer information */
|
||||
issuerVisibleToDids: Array<string> = [];
|
||||
/** Project location data */
|
||||
latitude = 0;
|
||||
loadingTotals = false;
|
||||
longitude = 0;
|
||||
/** Project name */
|
||||
name = "";
|
||||
offersToThis: Array<OfferSummaryRecord> = [];
|
||||
offersHitLimit = false;
|
||||
projectId = ""; // handle ID
|
||||
recentlyCheckedAndUnconfirmableJwts: string[] = [];
|
||||
/** Project ID (handle) */
|
||||
projectId = "";
|
||||
/** Project start time */
|
||||
startTime = "";
|
||||
totalsExpanded = false;
|
||||
truncatedDesc = "";
|
||||
truncateLength = 40;
|
||||
/** Project URL */
|
||||
url = "";
|
||||
|
||||
// Interaction Data
|
||||
/** Gifts to this project */
|
||||
offersToThis: Array<OfferSummaryRecord> = [];
|
||||
/** Flag for offers pagination */
|
||||
offersHitLimit = false;
|
||||
|
||||
// UI State
|
||||
/** JWT being checked for confirmation */
|
||||
checkingConfirmationForJwtId = "";
|
||||
/** Recently checked unconfirmable JWTs */
|
||||
recentlyCheckedAndUnconfirmableJwts: string[] = [];
|
||||
|
||||
totalsExpanded = false;
|
||||
truncatedDesc = "";
|
||||
/** Truncation length */
|
||||
truncateLength = 40;
|
||||
|
||||
// Utility References
|
||||
libsUtil = libsUtil;
|
||||
serverUtil = serverUtil;
|
||||
|
||||
/**
|
||||
* Component lifecycle hook that initializes the project view
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Loads account settings and contacts
|
||||
* 2. Retrieves all account DIDs
|
||||
* 3. Extracts project ID from URL
|
||||
* 4. Initializes project data loading
|
||||
*
|
||||
* @throws Logs errors but continues loading
|
||||
* @emits Notification on profile loading errors
|
||||
*/
|
||||
async created() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
@@ -680,12 +805,14 @@ export default class ProjectViewView extends Vue {
|
||||
this.loadTotals();
|
||||
}
|
||||
|
||||
onEditClick() {
|
||||
const route = {
|
||||
/**
|
||||
* Navigates to project edit view with current project ID
|
||||
*/
|
||||
onEditClick(): void {
|
||||
this.$router.push({
|
||||
name: "new-edit-project",
|
||||
query: { projectId: this.projectId },
|
||||
};
|
||||
(this.$router as Router).push(route);
|
||||
});
|
||||
}
|
||||
|
||||
// Isn't there a better way to make this available to the template?
|
||||
@@ -743,7 +870,7 @@ export default class ProjectViewView extends Vue {
|
||||
this.url = resp.data.claim?.url || "";
|
||||
} else {
|
||||
// actually, axios throws an error on 404 so we probably never get here
|
||||
console.error("Error getting project:", resp);
|
||||
logger.error("Error getting project:", resp);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -755,7 +882,7 @@ export default class ProjectViewView extends Vue {
|
||||
);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error("Error retrieving project:", error);
|
||||
logger.error("Error retrieving project:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -783,6 +910,15 @@ export default class ProjectViewView extends Vue {
|
||||
this.loadPlanFulfilledBy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads gifts made to this project
|
||||
*
|
||||
* Handles pagination and updates component state with results.
|
||||
* Uses beforeId for pagination based on last loaded gift.
|
||||
*
|
||||
* @throws Logs errors and notifies user
|
||||
* @emits Notification on loading errors
|
||||
*/
|
||||
async loadGives() {
|
||||
const givesUrl =
|
||||
this.apiServer +
|
||||
@@ -823,13 +959,22 @@ export default class ProjectViewView extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
console.error(
|
||||
logger.error(
|
||||
"Something went wrong retrieving more gives to this project:",
|
||||
serverError.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads gifts provided by this project
|
||||
*
|
||||
* Similar to loadGives but for outgoing gifts.
|
||||
* Maintains separate pagination state.
|
||||
*
|
||||
* @throws Logs errors and notifies user
|
||||
* @emits Notification on loading errors
|
||||
*/
|
||||
async loadGivesProvidedBy() {
|
||||
const providedByUrl =
|
||||
this.apiServer +
|
||||
@@ -873,13 +1018,22 @@ export default class ProjectViewView extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
console.error(
|
||||
logger.error(
|
||||
"Something went wrong retrieving gives that were provided by this project:",
|
||||
serverError.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads offers made to this project
|
||||
*
|
||||
* Handles pagination and filtering of valid offers.
|
||||
* Updates component state with results.
|
||||
*
|
||||
* @throws Logs errors and notifies user
|
||||
* @emits Notification on loading errors
|
||||
*/
|
||||
async loadOffers() {
|
||||
const offersUrl =
|
||||
this.apiServer +
|
||||
@@ -920,13 +1074,21 @@ export default class ProjectViewView extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
console.error(
|
||||
logger.error(
|
||||
"Something went wrong retrieving more offers to this project:",
|
||||
serverError.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads projects that fulfill this project
|
||||
*
|
||||
* Manages pagination state and updates component with results.
|
||||
*
|
||||
* @throws Logs errors and notifies user
|
||||
* @emits Notification on loading errors
|
||||
*/
|
||||
async loadPlanFulfillersTo() {
|
||||
const fulfillsUrl =
|
||||
this.apiServer +
|
||||
@@ -968,13 +1130,21 @@ export default class ProjectViewView extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
console.error(
|
||||
logger.error(
|
||||
"Something went wrong retrieving more plans that fulfill this project:",
|
||||
serverError.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads project that this project fulfills
|
||||
*
|
||||
* Updates fulfilledByThis state with result.
|
||||
*
|
||||
* @throws Logs errors and notifies user
|
||||
* @emits Notification on loading errors
|
||||
*/
|
||||
async loadPlanFulfilledBy() {
|
||||
const fulfilledByUrl =
|
||||
this.apiServer +
|
||||
@@ -1007,7 +1177,7 @@ export default class ProjectViewView extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
console.error(
|
||||
logger.error(
|
||||
"Error retrieving plans fulfilled by this project:",
|
||||
serverError.message,
|
||||
);
|
||||
@@ -1022,7 +1192,7 @@ export default class ProjectViewView extends Vue {
|
||||
const route = {
|
||||
path: "/project/" + encodeURIComponent(projectId),
|
||||
};
|
||||
(this.$router as Router).push(route);
|
||||
this.$router.push(route);
|
||||
this.loadProject(projectId, this.activeDid);
|
||||
}
|
||||
|
||||
@@ -1069,14 +1239,14 @@ export default class ProjectViewView extends Vue {
|
||||
projectId: this.projectId,
|
||||
},
|
||||
};
|
||||
(this.$router as Router).push(route);
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
onClickLoadClaim(jwtId: string) {
|
||||
const route = {
|
||||
path: "/claim/" + encodeURIComponent(jwtId),
|
||||
};
|
||||
(this.$router as Router).push(route);
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
checkIsFulfillable(offer: OfferSummaryRecord) {
|
||||
@@ -1225,7 +1395,7 @@ export default class ProjectViewView extends Vue {
|
||||
),
|
||||
),
|
||||
);
|
||||
const confirmationClaim: serverUtil.GenericVerifiableCredential = {
|
||||
const confirmationClaim: GenericVerifiableCredential = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "AgreeAction",
|
||||
object: goodClaim,
|
||||
@@ -1251,7 +1421,7 @@ export default class ProjectViewView extends Vue {
|
||||
give.jwtId,
|
||||
];
|
||||
} else {
|
||||
console.error("Got error submitting the confirmation:", result);
|
||||
logger.error("Got error submitting the confirmation:", result);
|
||||
const message =
|
||||
(result.error?.error as string) ||
|
||||
"There was a problem submitting the confirmation.";
|
||||
@@ -1307,7 +1477,7 @@ export default class ProjectViewView extends Vue {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading totals:", error);
|
||||
logger.error("Error loading totals:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
:class="computedOfferTabClassNames()"
|
||||
@click="
|
||||
offers = [];
|
||||
projects = [];
|
||||
@@ -23,7 +24,6 @@
|
||||
showProjects = false;
|
||||
loadOffers();
|
||||
"
|
||||
v-bind:class="computedOfferTabClassNames()"
|
||||
>
|
||||
Offers
|
||||
</a>
|
||||
@@ -31,6 +31,7 @@
|
||||
<li>
|
||||
<a
|
||||
href="#"
|
||||
:class="computedProjectTabClassNames()"
|
||||
@click="
|
||||
offers = [];
|
||||
projects = [];
|
||||
@@ -38,7 +39,6 @@
|
||||
showProjects = true;
|
||||
loadProjects();
|
||||
"
|
||||
v-bind:class="computedProjectTabClassNames()"
|
||||
>
|
||||
Projects
|
||||
</a>
|
||||
@@ -57,7 +57,7 @@
|
||||
<button
|
||||
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
|
||||
>
|
||||
<fa icon="magnifying-glass" class="fa-fw"></fa>
|
||||
<font-awesome icon="magnifying-glass" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
</div>
|
||||
-->
|
||||
@@ -68,15 +68,15 @@
|
||||
class="fixed right-6 top-24 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
|
||||
@click="onClickNewProject()"
|
||||
>
|
||||
<fa icon="plus" class="fa-fw"></fa>
|
||||
<font-awesome icon="plus" class="fa-fw"></font-awesome>
|
||||
</button>
|
||||
|
||||
<!-- Loading Animation -->
|
||||
<div
|
||||
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||
v-if="isLoading"
|
||||
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
|
||||
>
|
||||
<fa icon="spinner" class="fa-spin-pulse"></fa>
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
|
||||
</div>
|
||||
|
||||
<!-- Offer Results List -->
|
||||
@@ -90,22 +90,22 @@
|
||||
</div>
|
||||
<ul id="listOffers" class="border-t border-slate-300">
|
||||
<li
|
||||
class="border-b border-slate-300"
|
||||
v-for="offer in offers"
|
||||
:key="offer.handleId"
|
||||
class="border-b border-slate-300"
|
||||
>
|
||||
<div class="block py-4 flex gap-4">
|
||||
<div v-if="offer.fulfillsPlanHandleId" class="flex-none">
|
||||
<ProjectIcon
|
||||
:entityId="offer.fulfillsPlanHandleId"
|
||||
:iconSize="48"
|
||||
:entity-id="offer.fulfillsPlanHandleId"
|
||||
:icon-size="48"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="offer.recipientDid" class="flex-none w-12">
|
||||
<EntityIcon
|
||||
:entityId="offer.recipientDid"
|
||||
:iconSize="48"
|
||||
:entity-id="offer.recipientDid"
|
||||
:icon-size="48"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md"
|
||||
/>
|
||||
</div>
|
||||
@@ -130,17 +130,20 @@
|
||||
|
||||
<span class="text-sm">
|
||||
<span v-if="offer.amount">
|
||||
<fa
|
||||
<font-awesome
|
||||
:icon="libsUtil.iconForUnitCode(offer.unit)"
|
||||
class="fa-fw text-slate-400"
|
||||
/>
|
||||
|
||||
<span v-if="offer.amountGiven >= offer.amount">
|
||||
<fa icon="check-circle" class="fa-fw text-green-500" />
|
||||
<font-awesome
|
||||
icon="check-circle"
|
||||
class="fa-fw text-green-500"
|
||||
/>
|
||||
All {{ offer.amount }} given
|
||||
</span>
|
||||
<span v-else>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="triangle-exclamation"
|
||||
class="fa-fw text-yellow-500"
|
||||
/>
|
||||
@@ -156,13 +159,14 @@
|
||||
>
|
||||
<!--
|
||||
There's no need for a green icon:
|
||||
it's unnecessary if there's already a green, and confusing if there's a yellow.
|
||||
it's unnecessary if there's already a green, and confusing if there's a
|
||||
yellow.
|
||||
-->
|
||||
all
|
||||
</span>
|
||||
<span v-else>
|
||||
<!-- only show icon if there's not already a warning -->
|
||||
<fa
|
||||
<font-awesome
|
||||
v-if="offer.amountGiven >= offer.amount"
|
||||
icon="triangle-exclamation"
|
||||
class="fa-fw text-yellow-300"
|
||||
@@ -176,13 +180,16 @@
|
||||
<span v-else>
|
||||
<!-- Non-amount offer -->
|
||||
<span v-if="offer.nonAmountGivenConfirmed">
|
||||
<fa icon="check-circle" class="fa-fw text-green-500" />
|
||||
<font-awesome
|
||||
icon="check-circle"
|
||||
class="fa-fw text-green-500"
|
||||
/>
|
||||
{{ offer.nonAmountGivenConfirmed }}
|
||||
{{ offer.nonAmountGivenConfirmed == 1 ? "give" : "gives" }}
|
||||
are confirmed.
|
||||
</span>
|
||||
<span v-else>
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="triangle-exclamation"
|
||||
class="fa-fw text-yellow-500"
|
||||
/>
|
||||
@@ -191,10 +198,10 @@
|
||||
</span>
|
||||
|
||||
<a @click="onClickLoadClaim(offer.jwtId)">
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
></fa>
|
||||
></font-awesome>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -209,7 +216,7 @@
|
||||
You have not announced any projects.
|
||||
<div v-if="isRegistered">
|
||||
Hit the big
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="plus"
|
||||
class="bg-green-600 text-white px-1.5 py-1 rounded-full"
|
||||
/>
|
||||
@@ -217,8 +224,8 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<button
|
||||
@click="showNameThenIdDialog()"
|
||||
class="text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
||||
@click="showNameThenIdDialog()"
|
||||
>
|
||||
Get someone to onboard you.
|
||||
</button>
|
||||
@@ -227,19 +234,19 @@
|
||||
</div>
|
||||
<ul id="listProjects" class="border-t border-slate-300">
|
||||
<li
|
||||
class="border-b border-slate-300"
|
||||
v-for="project in projects"
|
||||
:key="project.handleId"
|
||||
class="border-b border-slate-300"
|
||||
>
|
||||
<a
|
||||
@click="onClickLoadProject(project.handleId)"
|
||||
class="block py-4 flex gap-4"
|
||||
@click="onClickLoadProject(project.handleId)"
|
||||
>
|
||||
<div class="flex-none">
|
||||
<ProjectIcon
|
||||
:entityId="project.handleId"
|
||||
:iconSize="48"
|
||||
:imageUrl="project.image"
|
||||
:entity-id="project.handleId"
|
||||
:icon-size="48"
|
||||
:image-url="project.image"
|
||||
class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12"
|
||||
/>
|
||||
</div>
|
||||
@@ -281,7 +288,7 @@ import {
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { OnboardPage } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: {
|
||||
EntityIcon,
|
||||
@@ -295,7 +302,9 @@ import { OnboardPage } from "../libs/util";
|
||||
})
|
||||
export default class ProjectsView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
errNote(message) {
|
||||
$router!: Router;
|
||||
|
||||
errNote(message: string) {
|
||||
this.$notify(
|
||||
{ group: "alert", type: "danger", title: "Error", text: message },
|
||||
5000,
|
||||
@@ -337,13 +346,13 @@ export default class ProjectsView extends Vue {
|
||||
}
|
||||
|
||||
if (this.allMyDids.length === 0) {
|
||||
console.error("No accounts found.");
|
||||
logger.error("No accounts found.");
|
||||
this.errNote("You need an identifier to load your projects.");
|
||||
} else {
|
||||
await this.loadProjects();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error initializing:", err);
|
||||
logger.error("Error initializing:", err);
|
||||
this.errNote("Something went wrong loading your projects.");
|
||||
}
|
||||
}
|
||||
@@ -372,7 +381,7 @@ export default class ProjectsView extends Vue {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Bad server response & data for plans:",
|
||||
resp.status,
|
||||
resp.data,
|
||||
@@ -381,7 +390,7 @@ export default class ProjectsView extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Got error loading plans:", error.message || error);
|
||||
logger.error("Got error loading plans:", error.message || error);
|
||||
this.errNote("Got an error loading projects.");
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
@@ -417,7 +426,7 @@ export default class ProjectsView extends Vue {
|
||||
const route = {
|
||||
path: "/project/" + encodeURIComponent(id),
|
||||
};
|
||||
(this.$router as Router).push(route);
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -427,14 +436,14 @@ export default class ProjectsView extends Vue {
|
||||
const route = {
|
||||
name: "new-edit-project",
|
||||
};
|
||||
(this.$router as Router).push(route);
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
onClickLoadClaim(jwtId: string) {
|
||||
const route = {
|
||||
path: "/claim/" + encodeURIComponent(jwtId),
|
||||
};
|
||||
(this.$router as Router).push(route);
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -465,7 +474,7 @@ export default class ProjectsView extends Vue {
|
||||
this.offers = this.offers.concat([offer]);
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
logger.error(
|
||||
"Bad server response & data for offers:",
|
||||
resp.status,
|
||||
resp.data,
|
||||
@@ -482,7 +491,7 @@ export default class ProjectsView extends Vue {
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Got error loading offers:", error.message || error);
|
||||
logger.error("Got error loading offers:", error.message || error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -537,10 +546,10 @@ export default class ProjectsView extends Vue {
|
||||
text: "If so, we'll use those with QR codes to share.",
|
||||
onCancel: async () => {},
|
||||
onNo: async () => {
|
||||
(this.$router as Router).push({ name: "share-my-contact-info" });
|
||||
this.$router.push({ name: "share-my-contact-info" });
|
||||
},
|
||||
onYes: async () => {
|
||||
(this.$router as Router).push({ name: "contact-qr" });
|
||||
this.$router.push({ name: "contact-qr" });
|
||||
},
|
||||
noText: "we will share another way",
|
||||
yesText: "we are nearby with cameras",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -22,17 +22,17 @@
|
||||
<div>
|
||||
<h2 class="text-2xl m-2">You're Here</h2>
|
||||
<div class="m-2 flex">
|
||||
<input type="checkbox" v-model="attended" class="h-6 w-6" />
|
||||
<input v-model="attended" type="checkbox" class="h-6 w-6" />
|
||||
<span class="pb-2 pl-2 pr-2">Attended</span>
|
||||
</div>
|
||||
<div class="m-2 flex">
|
||||
<input type="checkbox" v-model="gaveTime" class="h-6 w-6" />
|
||||
<input v-model="gaveTime" type="checkbox" class="h-6 w-6" />
|
||||
<span class="pb-2 pl-2 pr-2">Spent Time</span>
|
||||
<span v-if="gaveTime">
|
||||
<input
|
||||
v-model="hoursStr"
|
||||
type="text"
|
||||
placeholder="How much time"
|
||||
v-model="hoursStr"
|
||||
size="1"
|
||||
class="border border-slate-400 h-6 px-2"
|
||||
/>
|
||||
@@ -48,8 +48,8 @@
|
||||
class="flex justify-center mt-4"
|
||||
>
|
||||
<button
|
||||
@click="record()"
|
||||
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md w-56"
|
||||
@click="record()"
|
||||
>
|
||||
Sign & Send
|
||||
</button>
|
||||
@@ -81,7 +81,7 @@ import {
|
||||
createAndSubmitGive,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: {
|
||||
QuickNav,
|
||||
@@ -90,7 +90,7 @@ import * as libsUtil from "../libs/util";
|
||||
})
|
||||
export default class QuickActionBvcBeginView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
$router!: Router;
|
||||
attended = true;
|
||||
gaveTime = true;
|
||||
hoursStr = "1";
|
||||
@@ -143,7 +143,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
if (timeResult.type === "success") {
|
||||
timeSuccess = true;
|
||||
} else {
|
||||
console.error("Error sending time:", timeResult);
|
||||
logger.error("Error sending time:", timeResult);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -170,7 +170,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
if (attendResult.type === "success") {
|
||||
attendedSuccess = true;
|
||||
} else {
|
||||
console.error("Error sending attendance:", attendResult);
|
||||
logger.error("Error sending attendance:", attendResult);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -201,12 +201,12 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
},
|
||||
3000,
|
||||
);
|
||||
(this.$router as Router).push({ path: "/quick-action-bvc" });
|
||||
this.$router.push({ path: "/quick-action-bvc" });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Error sending claims.", error);
|
||||
logger.error("Error sending claims.", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -22,16 +22,16 @@
|
||||
<div>
|
||||
<h2 class="text-2xl m-2">Confirm</h2>
|
||||
<div v-if="loadingConfirms" class="flex justify-center">
|
||||
<fa icon="spinner" class="fa-spin-pulse" />
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
<div v-else-if="claimsToConfirm.length === 0">
|
||||
There are no claims yet today for you to confirm.
|
||||
</div>
|
||||
<ul class="border-t border-slate-300 m-2">
|
||||
<li
|
||||
class="border-b border-slate-300 py-2"
|
||||
v-for="record in claimsToConfirm"
|
||||
:key="record.id"
|
||||
class="border-b border-slate-300 py-2"
|
||||
>
|
||||
<div class="grid grid-cols-12">
|
||||
<span class="col-span-11 justify-self-start">
|
||||
@@ -39,6 +39,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="claimsToConfirmSelected.includes(record.id)"
|
||||
class="mr-2 h-6 w-6"
|
||||
@click="
|
||||
claimsToConfirmSelected.includes(record.id)
|
||||
? claimsToConfirmSelected.splice(
|
||||
@@ -47,7 +48,6 @@
|
||||
)
|
||||
: claimsToConfirmSelected.push(record.id)
|
||||
"
|
||||
class="mr-2 h-6 w-6"
|
||||
/>
|
||||
</span>
|
||||
{{
|
||||
@@ -59,7 +59,7 @@
|
||||
)
|
||||
}}
|
||||
<a @click="onClickLoadClaim(record.id)">
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
/>
|
||||
@@ -78,7 +78,7 @@
|
||||
}}
|
||||
so if you expected but do not see details from someone then ask them to
|
||||
check that their activity is visible to you on their Contacts
|
||||
<fa icon="users" class="text-slate-500" />
|
||||
<font-awesome icon="users" class="text-slate-500" />
|
||||
page.
|
||||
</span>
|
||||
</div>
|
||||
@@ -96,18 +96,18 @@
|
||||
<div>
|
||||
<h2 class="text-2xl m-2">Anything else?</h2>
|
||||
<div class="m-2 flex">
|
||||
<input type="checkbox" v-model="someoneGave" class="h-6 w-6" />
|
||||
<input v-model="someoneGave" type="checkbox" class="h-6 w-6" />
|
||||
<span class="pb-2 pl-2 pr-2">The group provided</span>
|
||||
<span v-if="someoneGave">
|
||||
<input
|
||||
type="text"
|
||||
v-model="description"
|
||||
type="text"
|
||||
size="20"
|
||||
class="border border-slate-400 h-6 px-2"
|
||||
/>
|
||||
<br />
|
||||
(Everyone likes personalized messages! 😁 ... and for a pic:
|
||||
<input type="checkbox" v-model="supplyGiftDetails" />)
|
||||
<input v-model="supplyGiftDetails" type="checkbox" />)
|
||||
</span>
|
||||
<!-- This is to match input height to avoid shifting when hiding & showing. -->
|
||||
<span v-else class="h-6">...</span>
|
||||
@@ -119,8 +119,8 @@
|
||||
class="flex justify-center mt-4"
|
||||
>
|
||||
<button
|
||||
@click="record()"
|
||||
class="block text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md w-56"
|
||||
@click="record()"
|
||||
>
|
||||
Sign & Send
|
||||
</button>
|
||||
@@ -151,18 +151,20 @@ import {
|
||||
retrieveSettingsForActiveAccount,
|
||||
} from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import {
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
ErrorResult,
|
||||
} from "../interfaces";
|
||||
import {
|
||||
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
||||
claimSpecialDescription,
|
||||
containsHiddenDid,
|
||||
createAndSubmitConfirmation,
|
||||
createAndSubmitGive,
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
getHeaders,
|
||||
ErrorResult,
|
||||
} from "../libs/endorserServer";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
methods: { claimSpecialDescription },
|
||||
components: {
|
||||
@@ -227,7 +229,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error("Bad response", response);
|
||||
logger.error("Bad response", response);
|
||||
throw new Error("Bad response when retrieving claims.");
|
||||
}
|
||||
await response.json().then((data) => {
|
||||
@@ -246,7 +248,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
dataByOthers.length - dataByOthersWithoutHidden.length;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
logger.error("Error:", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -298,7 +300,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
result.status === "fulfilled" && result.value.type === "success",
|
||||
);
|
||||
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
|
||||
console.error("Error sending confirmations:", confirmResults);
|
||||
logger.error("Error sending confirmations:", confirmResults);
|
||||
const howMany = confirmsSucceeded.length === 0 ? "all" : "some";
|
||||
this.$notify(
|
||||
{
|
||||
@@ -331,7 +333,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
);
|
||||
giveSucceeded = giveResult.type === "success";
|
||||
if (!giveSucceeded) {
|
||||
console.error("Error sending give:", giveResult);
|
||||
logger.error("Error sending give:", giveResult);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -403,7 +405,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.error("Error sending claims.", error);
|
||||
logger.error("Error sending claims.", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -41,12 +41,14 @@ import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
|
||||
import { Router } from "vue-router";
|
||||
@Component({
|
||||
components: {
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
},
|
||||
})
|
||||
export default class QuickActionBvcView extends Vue {}
|
||||
export default class QuickActionBvcView extends Vue {
|
||||
$router!: Router;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<div id="ViewBreadcrumb" class="mb-8">
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<!-- Back -->
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="chevron-left"
|
||||
@click="$router.back()"
|
||||
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
/>
|
||||
Offers to Your Projects
|
||||
</h1>
|
||||
@@ -20,13 +20,13 @@
|
||||
<p class="mt-2">
|
||||
Maybe there are already some projects you can help on the
|
||||
<router-link to="/discover" class="text-blue-500">
|
||||
Discover page <fa icon="search" />
|
||||
Discover page <font-awesome icon="search" />
|
||||
</router-link>
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
You can announce more of your own on
|
||||
<router-link to="/contacts" class="text-blue-500">
|
||||
Your Ideas page <fa icon="hand" />
|
||||
Your Ideas page <font-awesome icon="hand" />
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
@@ -42,8 +42,8 @@
|
||||
class="mt-4 relative group"
|
||||
>
|
||||
<div
|
||||
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
|
||||
v-if="offer.jwtId == lastAckedOfferToUserProjectsJwtId"
|
||||
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
|
||||
>
|
||||
You've already seen all the following
|
||||
</div>
|
||||
@@ -65,7 +65,10 @@
|
||||
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
/>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -83,20 +86,21 @@ import QuickNav from "../components/QuickNav.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { Router } from "vue-router";
|
||||
import { OfferToPlanSummaryRecord } from "../interfaces";
|
||||
import {
|
||||
didInfo,
|
||||
displayAmount,
|
||||
getNewOffersToUserProjects,
|
||||
OfferToPlanSummaryRecord,
|
||||
} from "../libs/endorserServer";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: { EntityIcon, GiftedDialog, InfiniteScroll, QuickNav },
|
||||
})
|
||||
export default class RecentOffersToUserView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
$router!: Router;
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: string[] = [];
|
||||
@@ -134,7 +138,7 @@ export default class RecentOffersToUserView extends Vue {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error retrieving settings & contacts:", err);
|
||||
logger.error("Error retrieving settings & contacts:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<div id="ViewBreadcrumb" class="mb-8">
|
||||
<h1 class="text-lg text-center font-light relative px-7">
|
||||
<!-- Back -->
|
||||
<fa
|
||||
<font-awesome
|
||||
icon="chevron-left"
|
||||
@click="$router.back()"
|
||||
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
/>
|
||||
Offers to You
|
||||
</h1>
|
||||
@@ -20,7 +20,7 @@
|
||||
<p class="mt-2">
|
||||
You can start the cycle on the
|
||||
<router-link to="/contacts" class="text-blue-500">
|
||||
Contacts page <fa icon="users" />
|
||||
Contacts page <font-awesome icon="users" />
|
||||
</router-link>
|
||||
with an "Offer" directly to someone. Hopefully you'll find a common
|
||||
interest!
|
||||
@@ -37,8 +37,8 @@
|
||||
class="mt-4 relative group"
|
||||
>
|
||||
<div
|
||||
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
|
||||
v-if="offer.jwtId == lastAckedOfferToUserJwtId"
|
||||
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
|
||||
>
|
||||
You've already seen all the following
|
||||
</div>
|
||||
@@ -58,7 +58,10 @@
|
||||
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
|
||||
<font-awesome
|
||||
icon="file-lines"
|
||||
class="pl-2 text-blue-500 cursor-pointer"
|
||||
/>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -68,7 +71,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import { Router } from "vue-router";
|
||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
import InfiniteScroll from "../components/InfiniteScroll.vue";
|
||||
@@ -76,20 +79,20 @@ import QuickNav from "../components/QuickNav.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { OfferSummaryRecord } from "../interfaces";
|
||||
import {
|
||||
didInfo,
|
||||
displayAmount,
|
||||
getNewOffersToUser,
|
||||
OfferSummaryRecord,
|
||||
} from "../libs/endorserServer";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: { EntityIcon, GiftedDialog, InfiniteScroll, QuickNav },
|
||||
})
|
||||
export default class RecentOffersToUserView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
$router!: Router;
|
||||
activeDid = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
allMyDids: string[] = [];
|
||||
@@ -126,7 +129,7 @@ export default class RecentOffersToUserView extends Vue {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error retrieving settings & contacts:", err);
|
||||
logger.error("Error retrieving settings & contacts:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="storeSearchBox"
|
||||
>
|
||||
<fa icon="save" class="fa-fw" />
|
||||
<font-awesome icon="save" class="fa-fw" />
|
||||
Store This Location for Nearby Search
|
||||
</button>
|
||||
<button
|
||||
@@ -43,7 +43,7 @@
|
||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="forgetSearchBox"
|
||||
>
|
||||
<fa icon="trash-can" class="fa-fw" />
|
||||
<font-awesome icon="trash-can" class="fa-fw" />
|
||||
Delete Stored Location
|
||||
</button>
|
||||
<button
|
||||
@@ -51,7 +51,7 @@
|
||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="resetLatLong"
|
||||
>
|
||||
<fa icon="rotate" class="fa-fw" />
|
||||
<font-awesome icon="rotate" class="fa-fw" />
|
||||
Reset To Original
|
||||
</button>
|
||||
<button
|
||||
@@ -59,7 +59,7 @@
|
||||
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
|
||||
@click="isNewMarkerSet = false"
|
||||
>
|
||||
<fa icon="eraser" class="fa-fw" />
|
||||
<font-awesome icon="eraser" class="fa-fw" />
|
||||
Erase Marker
|
||||
</button>
|
||||
<div v-if="isNewMarkerSet">
|
||||
@@ -71,9 +71,9 @@
|
||||
<div class="aspect-video">
|
||||
<l-map
|
||||
ref="map"
|
||||
v-model:zoom="localZoom"
|
||||
:center="[localCenterLat, localCenterLong]"
|
||||
class="!z-40 rounded-md"
|
||||
v-model:zoom="localZoom"
|
||||
@click="setMapPoint"
|
||||
>
|
||||
<l-tile-layer
|
||||
@@ -115,7 +115,7 @@ import QuickNav from "../components/QuickNav.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { BoundingBox, MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
const DEFAULT_LAT_LONG_DIFF = 0.01;
|
||||
const WORLD_ZOOM = 2;
|
||||
const DEFAULT_ZOOM = 2;
|
||||
@@ -131,6 +131,7 @@ const DEFAULT_ZOOM = 2;
|
||||
})
|
||||
export default class SearchAreaView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
isChoosingSearchBox = false;
|
||||
isNewMarkerSet = false;
|
||||
@@ -219,7 +220,7 @@ export default class SearchAreaView extends Vue {
|
||||
},
|
||||
7000,
|
||||
);
|
||||
(this.$router as Router).back();
|
||||
this.$router.back();
|
||||
} catch (err) {
|
||||
this.$notify(
|
||||
{
|
||||
@@ -230,7 +231,7 @@ export default class SearchAreaView extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
console.error(
|
||||
logger.error(
|
||||
"Telling user to retry the location search setting because:",
|
||||
err,
|
||||
);
|
||||
@@ -273,7 +274,7 @@ export default class SearchAreaView extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
console.error(
|
||||
logger.error(
|
||||
"Telling user to retry the location search setting because:",
|
||||
err,
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,10 @@
|
||||
)
|
||||
"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
<font-awesome
|
||||
icon="copy"
|
||||
class="text-slate-400 fa-fw"
|
||||
></font-awesome>
|
||||
</button>
|
||||
<span v-show="showCopiedSeed" class="text-sm text-green-500">
|
||||
Copied
|
||||
@@ -79,7 +82,10 @@
|
||||
)
|
||||
"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
<font-awesome
|
||||
icon="copy"
|
||||
class="text-slate-400 fa-fw"
|
||||
></font-awesome>
|
||||
</button>
|
||||
<span v-show="showCopiedDeri" class="text-sm text-green-500"
|
||||
>Copied</span
|
||||
@@ -110,11 +116,12 @@ import {
|
||||
retrieveAccountCount,
|
||||
retrieveFullyDecryptedAccount,
|
||||
} from "../libs/util";
|
||||
|
||||
import { Router } from "vue-router";
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class SeedBackupView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
$router!: Router;
|
||||
activeAccount: Account | null | undefined = null;
|
||||
numAccounts = 0;
|
||||
showCopiedDeri = false;
|
||||
@@ -130,7 +137,7 @@ export default class SeedBackupView extends Vue {
|
||||
this.numAccounts = await retrieveAccountCount();
|
||||
this.activeAccount = await retrieveFullyDecryptedAccount(activeDid);
|
||||
} catch (err: unknown) {
|
||||
console.error("Got an error loading an identifier:", err);
|
||||
logger.error("Got an error loading an identifier:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw" />
|
||||
<font-awesome icon="chevron-left" class="fa-fw" />
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="mt-8">Click to copy your info, then send it to them.</div>
|
||||
<div>
|
||||
They will paste it in the input box on the Contacts
|
||||
<fa icon="users" /> screen.
|
||||
<font-awesome icon="users" /> screen.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -50,15 +50,16 @@ import { NotificationIface, APP_SERVER } from "../constants/app";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { retrieveAccountMetadata } from "../libs/util";
|
||||
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({
|
||||
components: { QuickNav, TopMessage },
|
||||
})
|
||||
export default class ShareMyContactInfoView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
mounted() {
|
||||
console.log("APP_SERVER in mounted:", APP_SERVER);
|
||||
logger.log("APP_SERVER in mounted:", APP_SERVER);
|
||||
}
|
||||
|
||||
async onClickShare() {
|
||||
@@ -106,7 +107,7 @@ export default class ShareMyContactInfoView extends Vue {
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
(this.$router as Router).push({ name: "contacts" });
|
||||
this.$router.push({ name: "contacts" });
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -8,30 +8,30 @@
|
||||
</h1>
|
||||
<div v-if="imageBlob">
|
||||
<div v-if="uploading" class="text-center mb-4">
|
||||
<fa icon="spinner" class="fa-spin-pulse" />
|
||||
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="text-center mb-4">Choose how to use this image</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<button
|
||||
@click="recordGift"
|
||||
class="text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
||||
@click="recordGift"
|
||||
>
|
||||
<fa icon="gift" class="fa-fw" />
|
||||
<font-awesome icon="gift" class="fa-fw" />
|
||||
Record a Gift
|
||||
</button>
|
||||
<button
|
||||
@click="recordProfile"
|
||||
class="text-center text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
||||
@click="recordProfile"
|
||||
>
|
||||
<fa icon="circle-user" class="fa-fw" />
|
||||
<font-awesome icon="circle-user" class="fa-fw" />
|
||||
Save as Profile Image
|
||||
</button>
|
||||
<button
|
||||
@click="cancel"
|
||||
class="text-center text-md font-bold bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
|
||||
@click="cancel"
|
||||
>
|
||||
<fa icon="ban" class="fa-fw" />
|
||||
<font-awesome icon="ban" class="fa-fw" />
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
@@ -66,7 +66,11 @@
|
||||
<script lang="ts">
|
||||
import axios from "axios";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocationRaw, Router } from "vue-router";
|
||||
import {
|
||||
RouteLocationNormalizedLoaded,
|
||||
RouteLocationRaw,
|
||||
Router,
|
||||
} from "vue-router";
|
||||
|
||||
import PhotoDialog from "../components/PhotoDialog.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
@@ -79,10 +83,12 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { base64ToBlob, SHARED_PHOTO_BASE64_KEY } from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@Component({ components: { PhotoDialog, QuickNav } })
|
||||
export default class SharedPhotoView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
|
||||
activeDid: string | undefined = undefined;
|
||||
imageBlob: Blob | undefined = undefined;
|
||||
@@ -105,14 +111,12 @@ export default class SharedPhotoView extends Vue {
|
||||
// clear the temp image
|
||||
db.temp.delete(SHARED_PHOTO_BASE64_KEY);
|
||||
|
||||
this.imageFileName = (this.$route as Router).query[
|
||||
"fileName"
|
||||
] as string;
|
||||
this.imageFileName = this.$route.query["fileName"] as string;
|
||||
} else {
|
||||
console.error("No appropriate image found in temp storage.", temp);
|
||||
logger.error("No appropriate image found in temp storage.", temp);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error("Got an error loading an identifier:", err);
|
||||
logger.error("Got an error loading an identifier:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -138,7 +142,7 @@ export default class SharedPhotoView extends Vue {
|
||||
recipientDid: this.activeDid,
|
||||
},
|
||||
} as RouteLocationRaw;
|
||||
(this.$router as Router).push(route);
|
||||
this.$router.push(route);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -149,7 +153,7 @@ export default class SharedPhotoView extends Vue {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
profileImageUrl: imgUrl,
|
||||
});
|
||||
(this.$router as Router).push({ name: "account" });
|
||||
this.$router.push({ name: "account" });
|
||||
},
|
||||
IMAGE_TYPE_PROFILE,
|
||||
true,
|
||||
@@ -161,7 +165,7 @@ export default class SharedPhotoView extends Vue {
|
||||
async cancel() {
|
||||
this.imageBlob = undefined;
|
||||
this.imageFileName = undefined;
|
||||
(this.$router as Router).push({ name: "home" });
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
|
||||
async sendToImageServer(imageType: string) {
|
||||
@@ -187,7 +191,7 @@ export default class SharedPhotoView extends Vue {
|
||||
window.location.hostname === "localhost" &&
|
||||
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
|
||||
) {
|
||||
console.log(
|
||||
logger.log(
|
||||
"Using shared image API server, so only users on that server can play with images.",
|
||||
);
|
||||
}
|
||||
@@ -201,7 +205,7 @@ export default class SharedPhotoView extends Vue {
|
||||
this.imageFileName = undefined;
|
||||
result = response.data.url as string;
|
||||
} else {
|
||||
console.error("Problem uploading the image", response.data);
|
||||
logger.error("Problem uploading the image", response.data);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -217,7 +221,7 @@ export default class SharedPhotoView extends Vue {
|
||||
|
||||
this.uploading = false;
|
||||
} catch (error) {
|
||||
console.error("Error uploading the image", error);
|
||||
logger.error("Error uploading the image", error);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
href="https://www.perplexity.ai/search/what-are-passkeys-v2SHV3yLQlyA2CYH6.Nvhg"
|
||||
target="_blank"
|
||||
>
|
||||
<fa icon="info-circle" class="fa-fw text-blue-500" />
|
||||
<font-awesome icon="info-circle" class="fa-fw text-blue-500" />
|
||||
</a>
|
||||
</p>
|
||||
<p class="text-center font-light mt-4">
|
||||
@@ -44,21 +44,21 @@
|
||||
href="https://www.perplexity.ai/search/what-is-a-seed-phrase-OqiP9foVRXidr_2le5OFKA"
|
||||
target="_blank"
|
||||
>
|
||||
<fa icon="info-circle" class="fa-fw text-blue-500" />
|
||||
<font-awesome icon="info-circle" class="fa-fw text-blue-500" />
|
||||
</a>
|
||||
</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-4">
|
||||
<a
|
||||
v-if="PASSKEYS_ENABLED"
|
||||
@click="onClickNewPasskey()"
|
||||
class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2 cursor-pointer"
|
||||
@click="onClickNewPasskey()"
|
||||
>
|
||||
Generate one with a passkey
|
||||
</a>
|
||||
<a
|
||||
@click="onClickNewSeed()"
|
||||
class="block w-full text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md mb-2 cursor-pointer"
|
||||
data-testId="newSeed"
|
||||
@click="onClickNewSeed()"
|
||||
>
|
||||
Generate one with a new seed
|
||||
</a>
|
||||
@@ -69,15 +69,15 @@
|
||||
</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2">
|
||||
<a
|
||||
@click="onClickNo()"
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md cursor-pointer"
|
||||
@click="onClickNo()"
|
||||
>
|
||||
You have a seed
|
||||
</a>
|
||||
<a
|
||||
v-if="numAccounts > 0"
|
||||
@click="onClickDerive()"
|
||||
class="block w-full text-center text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md cursor-pointer"
|
||||
@click="onClickDerive()"
|
||||
>
|
||||
Derive new address from existing seed
|
||||
</a>
|
||||
@@ -102,6 +102,7 @@ import {
|
||||
components: {},
|
||||
})
|
||||
export default class StartView extends Vue {
|
||||
$router!: Router;
|
||||
PASSKEYS_ENABLED = PASSKEYS_ENABLED;
|
||||
|
||||
givenName = "";
|
||||
@@ -115,22 +116,22 @@ export default class StartView extends Vue {
|
||||
}
|
||||
|
||||
public onClickNewSeed() {
|
||||
(this.$router as Router).push({ name: "new-identifier" });
|
||||
this.$router.push({ name: "new-identifier" });
|
||||
}
|
||||
|
||||
public async onClickNewPasskey() {
|
||||
const keyName =
|
||||
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : "");
|
||||
await registerSaveAndActivatePasskey(keyName);
|
||||
(this.$router as Router).push({ name: "account" });
|
||||
this.$router.push({ name: "account" });
|
||||
}
|
||||
|
||||
public onClickNo() {
|
||||
(this.$router as Router).push({ name: "import-account" });
|
||||
this.$router.push({ name: "import-account" });
|
||||
}
|
||||
|
||||
public onClickDerive() {
|
||||
(this.$router as Router).push({ name: "import-derive" });
|
||||
this.$router.push({ name: "import-derive" });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
<script lang="ts">
|
||||
import { SVGRenderer } from "three/examples/jsm/renderers/SVGRenderer.js";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
|
||||
import { Router } from "vue-router";
|
||||
import { World } from "../components/World/World.js";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
@@ -72,6 +72,7 @@ interface Dictionary<T> {
|
||||
@Component({ components: { World, QuickNav } })
|
||||
export default class StatisticsView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
$router!: Router;
|
||||
|
||||
world: World;
|
||||
worldProperties: Dictionary<number> = {};
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
|
||||
@click="$router.back()"
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,9 @@
|
||||
<h2 class="text-xl font-bold mb-4">Notiwind Alerts</h2>
|
||||
|
||||
<button
|
||||
class="font-bold capitalize bg-slate-900 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="
|
||||
this.$notify(
|
||||
$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'toast',
|
||||
@@ -36,14 +37,14 @@
|
||||
5000,
|
||||
)
|
||||
"
|
||||
class="font-bold capitalize bg-slate-900 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Toast
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="
|
||||
this.$notify(
|
||||
$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'info',
|
||||
@@ -53,14 +54,14 @@
|
||||
5000,
|
||||
)
|
||||
"
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Info
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="font-bold capitalize bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="
|
||||
this.$notify(
|
||||
$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'success',
|
||||
@@ -70,14 +71,14 @@
|
||||
5000,
|
||||
)
|
||||
"
|
||||
class="font-bold capitalize bg-emerald-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Success
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="font-bold capitalize bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="
|
||||
this.$notify(
|
||||
$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'warning',
|
||||
@@ -87,14 +88,14 @@
|
||||
5000,
|
||||
)
|
||||
"
|
||||
class="font-bold capitalize bg-amber-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Warning
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="font-bold capitalize bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="
|
||||
this.$notify(
|
||||
$notify(
|
||||
{
|
||||
group: 'alert',
|
||||
type: 'danger',
|
||||
@@ -104,14 +105,14 @@
|
||||
5000,
|
||||
)
|
||||
"
|
||||
class="font-bold capitalize bg-rose-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Danger
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="
|
||||
this.$notify(
|
||||
$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-permission',
|
||||
@@ -121,14 +122,14 @@
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Notif ON
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="
|
||||
this.$notify(
|
||||
$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-mute',
|
||||
@@ -138,14 +139,14 @@
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Notif MUTE
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="
|
||||
this.$notify(
|
||||
$notify(
|
||||
{
|
||||
group: 'modal',
|
||||
type: 'notification-off',
|
||||
@@ -155,7 +156,6 @@
|
||||
-1,
|
||||
)
|
||||
"
|
||||
class="font-bold capitalize bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
|
||||
>
|
||||
Notif OFF
|
||||
</button>
|
||||
@@ -190,8 +190,8 @@
|
||||
<div>
|
||||
Register Passkey
|
||||
<button
|
||||
@click="register()"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="register()"
|
||||
>
|
||||
Simplewebauthn
|
||||
</button>
|
||||
@@ -200,14 +200,14 @@
|
||||
<div>
|
||||
Create JWT
|
||||
<button
|
||||
@click="createJwtSimplewebauthn()"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="createJwtSimplewebauthn()"
|
||||
>
|
||||
Simplewebauthn
|
||||
</button>
|
||||
<button
|
||||
@click="createJwtNavigator()"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="createJwtNavigator()"
|
||||
>
|
||||
Navigator
|
||||
</button>
|
||||
@@ -216,28 +216,28 @@
|
||||
<div v-if="jwt">
|
||||
Verify New JWT
|
||||
<button
|
||||
@click="verifySimplewebauthn()"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="verifySimplewebauthn()"
|
||||
>
|
||||
Simplewebauthn
|
||||
</button>
|
||||
<button
|
||||
@click="verifyWebCrypto()"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="verifyWebCrypto()"
|
||||
>
|
||||
WebCrypto
|
||||
</button>
|
||||
<button
|
||||
@click="verifyP256()"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="verifyP256()"
|
||||
>
|
||||
p256 - broken
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>Verify New JWT -- requires creation first</div>
|
||||
<button
|
||||
@click="verifyMyJwt()"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="verifyMyJwt()"
|
||||
>
|
||||
Verify Hard-Coded JWT
|
||||
</button>
|
||||
@@ -248,8 +248,8 @@
|
||||
See console for more output.
|
||||
<div>
|
||||
<button
|
||||
@click="testEncryptionDecryption()"
|
||||
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
|
||||
@click="testEncryptionDecryption()"
|
||||
>
|
||||
Run Test
|
||||
</button>
|
||||
@@ -285,7 +285,7 @@ import {
|
||||
registerAndSavePasskey,
|
||||
SHARED_PHOTO_BASE64_KEY,
|
||||
} from "../libs/util";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
const inputFileNameRef = ref<Blob>();
|
||||
|
||||
const TEST_PAYLOAD = {
|
||||
@@ -378,7 +378,7 @@ export default class Help extends Vue {
|
||||
this.userName = DEFAULT_USERNAME;
|
||||
},
|
||||
onYes: async () => {
|
||||
(this.$router as Router).push({ name: "new-edit-account" });
|
||||
this.$router.push({ name: "new-edit-account" });
|
||||
},
|
||||
noText: "try again and use " + DEFAULT_USERNAME,
|
||||
},
|
||||
@@ -411,7 +411,7 @@ export default class Help extends Vue {
|
||||
TEST_PAYLOAD,
|
||||
this.credIdHex as string,
|
||||
);
|
||||
console.log("simple jwt4url", this.jwt);
|
||||
logger.log("simple jwt4url", this.jwt);
|
||||
}
|
||||
|
||||
public async createJwtNavigator() {
|
||||
@@ -428,7 +428,7 @@ export default class Help extends Vue {
|
||||
TEST_PAYLOAD,
|
||||
this.credIdHex as string,
|
||||
);
|
||||
console.log("lower jwt4url", this.jwt);
|
||||
logger.log("lower jwt4url", this.jwt);
|
||||
}
|
||||
|
||||
public async verifyP256() {
|
||||
@@ -440,7 +440,7 @@ export default class Help extends Vue {
|
||||
this.peerSetup?.clientDataJsonBase64Url as Base64URLString,
|
||||
this.peerSetup?.signature as Base64URLString,
|
||||
);
|
||||
console.log("decoded", decoded);
|
||||
logger.log("decoded", decoded);
|
||||
}
|
||||
|
||||
public async verifySimplewebauthn() {
|
||||
@@ -452,7 +452,7 @@ export default class Help extends Vue {
|
||||
this.peerSetup?.clientDataJsonBase64Url as Base64URLString,
|
||||
this.peerSetup?.signature as Base64URLString,
|
||||
);
|
||||
console.log("decoded", decoded);
|
||||
logger.log("decoded", decoded);
|
||||
}
|
||||
|
||||
public async verifyWebCrypto() {
|
||||
@@ -464,7 +464,7 @@ export default class Help extends Vue {
|
||||
this.peerSetup?.clientDataJsonBase64Url as Base64URLString,
|
||||
this.peerSetup?.signature as Base64URLString,
|
||||
);
|
||||
console.log("decoded", decoded);
|
||||
logger.log("decoded", decoded);
|
||||
}
|
||||
|
||||
public async verifyMyJwt() {
|
||||
@@ -490,7 +490,7 @@ export default class Help extends Vue {
|
||||
payload["ClientDataJSONB64URL"],
|
||||
signatureB64URL,
|
||||
);
|
||||
console.log("decoded", decoded);
|
||||
logger.log("decoded", decoded);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user