forked from trent_larson/crowd-funder-for-time-pwa
Compare commits
14 Commits
misc2
...
more-small
| Author | SHA1 | Date | |
|---|---|---|---|
| 85bd807bcc | |||
| eeece8a1b4 | |||
| bbfc1e1007 | |||
| 433d0c023e | |||
| ac6376243b | |||
| a12f033b72 | |||
| 42cd7d00de | |||
| c388cc8cfe | |||
| 6d4d4e40c3 | |||
| 3b39faf173 | |||
| f43ecc98aa | |||
| 5b7ccf9ef0 | |||
| 9bacd4da87 | |||
| 7450d8d1c3 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kickstart-for-time-pwa",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
|
||||
@@ -6,14 +6,10 @@ tasks:
|
||||
- 40 notifications :
|
||||
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew
|
||||
|
||||
- 01 bug - we get a 404 when reloading the page anyplace except "/", and it's hard to get back
|
||||
- .1 test - make sure that a registration failure (including network failure) doesn't give a success message (which may have happened during board meeting)
|
||||
|
||||
- .1 don't allow to even see the claim actions if they're not registered
|
||||
|
||||
- 01 Replace Gifted/Give in ContactsView with GiftedDialog assignee:matthew
|
||||
|
||||
- 01 fix the Discovery map display to not show on top of bottom icons (and any other UI tweaks on the map flow) assignee-group:ui
|
||||
- .1 add instructions for map location selection
|
||||
|
||||
- 01 Show pop-up or some message confirming that settings & contacts download has been initiated/finished assignee:matthew assignee-group:ui
|
||||
|
||||
@@ -27,7 +23,7 @@ tasks:
|
||||
|
||||
- 24 Move to Vite assignee:matthew
|
||||
|
||||
- .2 fit more icons on home screen, with a "more" button to contacts page if there is more than 2 rows
|
||||
- .2 fit as many icons as possible on home & project view screens but only going halfway down the page
|
||||
- .1 Remove notification alert visuals on home page
|
||||
- .5 Add infinite scroll to gifts on the home page
|
||||
- .5 bug - search for "Safari" does not find the project, but if already on the "Anywhere" tab it shows all
|
||||
@@ -57,7 +53,8 @@ tasks:
|
||||
- 01 Would it look better to shrink the buttons on many pages so they don't expand to the width of the screen? assignee-group:ui
|
||||
- .5 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui
|
||||
- .5 make a VC details page
|
||||
- .1 Add units or different icon to the coins (to distinguish $, BTC, etc)
|
||||
- .1 Add units or different icon to the coins (to distinguish $, BTC, hours, etc)
|
||||
- .1 remove firstName (& lastName) from localStorage
|
||||
|
||||
- contacts v+ :
|
||||
- 01 Import all the non-sensitive data (ie. contacts & settings).
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<h1 class="text-xl font-bold text-center mb-4">
|
||||
{{ message }} {{ giver?.name || "somebody not specified" }}
|
||||
{{ message }} {{ giver?.name || "somebody not named" }}
|
||||
</h1>
|
||||
<input
|
||||
type="text"
|
||||
@@ -51,18 +51,57 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop, Emit } from "vue-facing-decorator";
|
||||
import { GiverInputInfo, GiverOutputInfo } from "@/libs/endorserServer";
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
import { createAndSubmitGive, GiverInputInfo } from "@/libs/endorserServer";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
type: string;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
@Component
|
||||
export default class GiftedDialog extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
|
||||
@Prop message = "";
|
||||
@Prop projectId = "";
|
||||
|
||||
activeDid = "";
|
||||
apiServer = "";
|
||||
|
||||
giver?: GiverInputInfo;
|
||||
description = "";
|
||||
hours = "0";
|
||||
visible = false;
|
||||
|
||||
async created() {
|
||||
try {
|
||||
await db.open();
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.log("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
err.message ||
|
||||
"There was an error retrieving the latest sweet, sweet action.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
open(giver: GiverInputInfo) {
|
||||
this.giver = giver;
|
||||
this.visible = true;
|
||||
@@ -80,27 +119,169 @@ export default class GiftedDialog extends Vue {
|
||||
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
|
||||
}
|
||||
|
||||
@Emit("dialog-result")
|
||||
confirm(): GiverOutputInfo {
|
||||
const result = {
|
||||
action: "confirm",
|
||||
giver: this.giver,
|
||||
hours: parseFloat(this.hours),
|
||||
description: this.description,
|
||||
};
|
||||
cancel() {
|
||||
this.close();
|
||||
this.description = "";
|
||||
this.giver = undefined;
|
||||
this.hours = "0";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Emit("dialog-result")
|
||||
cancel(): GiverOutputInfo {
|
||||
const result = { action: "cancel" };
|
||||
async confirm() {
|
||||
this.close();
|
||||
return result;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
text: "Recording the give...",
|
||||
title: "",
|
||||
},
|
||||
1000,
|
||||
);
|
||||
// this is asynchronous, but we don't need to wait for it to complete
|
||||
this.recordGive(
|
||||
this.giver?.did as string | undefined,
|
||||
this.description,
|
||||
parseFloat(this.hours),
|
||||
).then(() => {
|
||||
this.description = "";
|
||||
this.giver = undefined;
|
||||
this.hours = "0";
|
||||
});
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
throw new Error(
|
||||
"Attempted to load Give records for DID ${activeDid} but no identity was found",
|
||||
);
|
||||
}
|
||||
return identity;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param giverDid may be null
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
*/
|
||||
public async recordGive(
|
||||
giverDid?: string,
|
||||
description?: string,
|
||||
hours?: number,
|
||||
) {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must select an identity before you can record a give.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description && !hours) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must enter a description or some number of hours.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
const result = await createAndSubmitGive(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
identity,
|
||||
giverDid,
|
||||
this.activeDid,
|
||||
description,
|
||||
hours,
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
if (
|
||||
result.type === "error" ||
|
||||
this.isGiveCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
console.log("Error with give creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: errorMessage || "There was an error creating the give.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
text: "That gift was recorded.",
|
||||
},
|
||||
10000,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.log("Error with give recordation caught:", error);
|
||||
const message =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
"There was an error recording the give.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
/**
|
||||
* @param result response "data" from the server
|
||||
* @returns true if the result indicates an error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isGiveCreationError(result: any) {
|
||||
return result.status !== 201 || result.data?.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||
* @returns best guess at an error message
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getGiveCreationErrorMessage(result: any) {
|
||||
return (
|
||||
result.error?.userMessage ||
|
||||
result.error?.error ||
|
||||
result.response?.data?.error?.message
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,8 @@ export type Settings = {
|
||||
activeDid?: string;
|
||||
apiServer?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
isRegistered?: boolean;
|
||||
lastName?: string; // deprecated, pre v 0.1.3
|
||||
lastViewedClaimId?: string;
|
||||
searchBoxes?: Array<{
|
||||
name: string;
|
||||
|
||||
@@ -33,7 +33,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "home",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
|
||||
beforeEnter: enterOrStart,
|
||||
},
|
||||
{
|
||||
path: "/account",
|
||||
@@ -79,7 +78,6 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: "contacts",
|
||||
component: () =>
|
||||
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
|
||||
beforeEnter: enterOrStart,
|
||||
},
|
||||
{
|
||||
path: "/scan-contact",
|
||||
@@ -196,9 +194,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
path: "/test",
|
||||
name: "test",
|
||||
component: () =>
|
||||
import(
|
||||
/* webpackChunkName: "test" */ "../views/TestView.vue"
|
||||
),
|
||||
import(/* webpackChunkName: "test" */ "../views/TestView.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -52,7 +52,17 @@
|
||||
|
||||
<!-- Identity Details -->
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||
<h2 class="text-xl font-semibold mb-2">{{ firstName }} {{ lastName }}</h2>
|
||||
<h2 v-if="givenName" class="text-xl font-semibold mb-2">
|
||||
{{ givenName }}
|
||||
</h2>
|
||||
<span v-else>
|
||||
<router-link
|
||||
:to="{ name: 'new-edit-account' }"
|
||||
class="text-xs bg-blue-500 text-white px-1.5 py-1 rounded-md"
|
||||
>
|
||||
(set name)
|
||||
</router-link>
|
||||
</span>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">ID</div>
|
||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||
@@ -67,53 +77,11 @@
|
||||
</button>
|
||||
<span v-show="showDidCopy">Copied!</span>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||
<code class="truncate">{{ publicBase64 }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showB64Copy">Copied!</span>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
|
||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||
<code class="truncate">{{ publicHex }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showPubCopy">Copied!</span>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
|
||||
<div class="text-sm text-slate-500 flex justify-start items-center mb-1">
|
||||
<code class="truncate">{{ derivationPath }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(derivationPath, () => (showDerCopy = !showDerCopy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showDerCopy">Copied!</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-link
|
||||
:to="{ name: 'new-edit-account' }"
|
||||
class="block text-center text-lg font-bold uppercase bg-blue-600 text-white px-2 py-3 rounded-md mb-2"
|
||||
class="block text-center text-lg font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md mb-2"
|
||||
>
|
||||
Edit Identity
|
||||
</router-link>
|
||||
@@ -132,8 +100,10 @@
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- label -->
|
||||
<div>App Notifications</div>
|
||||
<!-- toggle -->
|
||||
<div class="relative">
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input type="checkbox" name="toggleNotifications" class="sr-only" />
|
||||
<!-- line -->
|
||||
@@ -143,8 +113,6 @@
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
<!-- label -->
|
||||
<div class="ml-2">App Notifications</div>
|
||||
</label>
|
||||
<label
|
||||
for="toggleMuteNotifications"
|
||||
@@ -159,8 +127,10 @@
|
||||
)
|
||||
"
|
||||
>
|
||||
<!-- label -->
|
||||
<div>Mute Notifications</div>
|
||||
<!-- toggle -->
|
||||
<div class="relative">
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -174,8 +144,6 @@
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
<!-- label -->
|
||||
<div class="ml-2">Mute Notifications</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -192,32 +160,13 @@
|
||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-6"
|
||||
@click="exportDatabase()"
|
||||
>
|
||||
Download Settings & Contacts (excluding Identifier Data)
|
||||
Download Settings & Contacts
|
||||
<br />
|
||||
(excluding Identifier Data)
|
||||
</a>
|
||||
<a ref="downloadLink" />
|
||||
|
||||
<!-- QR code popup -->
|
||||
<dialog id="dlgQR" class="backdrop:bg-black/75 rounded-md">
|
||||
<div class="text-slate-500 text-center">
|
||||
<b>ID:</b> <code>did:peer:kl45kj41lk451kl3</code>
|
||||
</div>
|
||||
<img src="/img/sample-qr-code.png" class="w-full mb-3" />
|
||||
|
||||
<button
|
||||
value="cancel"
|
||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||
>
|
||||
Copy to Clipboard
|
||||
</button>
|
||||
<button
|
||||
value="cancel"
|
||||
class="block w-full text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</dialog>
|
||||
|
||||
<div class="flex py-2">
|
||||
<div v-if="activeDid" class="flex py-2">
|
||||
<button class="text-center text-md text-blue-500" @click="checkLimits()">
|
||||
Check Limits
|
||||
</button>
|
||||
@@ -252,14 +201,76 @@
|
||||
>
|
||||
Advanced
|
||||
</h3>
|
||||
|
||||
<div v-if="showAdvanced">
|
||||
<!-- Deep Identity Details -->
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2 py-2">
|
||||
Deep Identity Details
|
||||
</h2>
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||
<div class="text-slate-500 text-sm font-bold">Public Key (base 64)</div>
|
||||
<div
|
||||
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
||||
>
|
||||
<code class="truncate">{{ publicBase64 }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showB64Copy">Copied!</span>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">Public Key (hex)</div>
|
||||
<div
|
||||
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
||||
>
|
||||
<code class="truncate">{{ publicHex }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showPubCopy">Copied!</span>
|
||||
</div>
|
||||
|
||||
<div class="text-slate-500 text-sm font-bold">Derivation Path</div>
|
||||
<div
|
||||
class="text-sm text-slate-500 flex justify-start items-center mb-1"
|
||||
>
|
||||
<code class="truncate">{{ derivationPath }}</code>
|
||||
<button
|
||||
@click="
|
||||
doCopyTwoSecRedo(
|
||||
derivationPath,
|
||||
() => (showDerCopy = !showDerCopy),
|
||||
)
|
||||
"
|
||||
class="ml-2"
|
||||
>
|
||||
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
|
||||
</button>
|
||||
<span v-show="showDerCopy">Copied!</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label
|
||||
for="toggleShowAmounts"
|
||||
class="flex items-center cursor-pointer mb-6"
|
||||
class="flex items-center cursor-pointer py-2"
|
||||
@click="handleChange"
|
||||
>
|
||||
<!-- label -->
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">
|
||||
Show amounts given with contacts
|
||||
</h2>
|
||||
<!-- toggle -->
|
||||
<div class="relative">
|
||||
<div class="relative ml-2">
|
||||
<!-- input -->
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -274,23 +285,31 @@
|
||||
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
||||
></div>
|
||||
</div>
|
||||
<!-- label -->
|
||||
<div class="ml-2">Show amounts given with contacts</div>
|
||||
</label>
|
||||
|
||||
<div class="flex py-2">
|
||||
<!-- id used by puppeteer test script -->
|
||||
<router-link
|
||||
id="switch-identity-link"
|
||||
:to="{ name: 'identity-switcher' }"
|
||||
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2"
|
||||
>
|
||||
Switch Identity / No Identity
|
||||
</router-link>
|
||||
<button class="text-blue-500">
|
||||
<!-- id used by puppeteer test script -->
|
||||
<router-link
|
||||
id="switch-identity-link"
|
||||
:to="{ name: 'identity-switcher' }"
|
||||
class="block text-center"
|
||||
>
|
||||
Switch Identity / No Identity
|
||||
</router-link>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex py-2">
|
||||
Claim Server
|
||||
<button class="text-blue-500">
|
||||
<router-link :to="{ name: 'statistics' }" class="block text-center">
|
||||
See Achievements & Statistics
|
||||
</router-link>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex py-4">
|
||||
<h2 class="text-slate-500 text-sm font-bold mb-2">Claim Server</h2>
|
||||
<input
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 px-3 py-2"
|
||||
@@ -322,17 +341,6 @@
|
||||
Use Local
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="text-blue-500">
|
||||
<router-link
|
||||
:to="{ name: 'statistics' }"
|
||||
class="block text-center py-3"
|
||||
>
|
||||
See Achievements & Statistics
|
||||
</router-link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -346,7 +354,7 @@ import { useClipboard } from "@vueuse/core";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import { AppString } from "@/constants/app";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
|
||||
@@ -368,14 +376,6 @@ interface IAccount {
|
||||
derivationPath: string;
|
||||
}
|
||||
|
||||
interface SettingsType {
|
||||
activeDid?: string;
|
||||
apiServer?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
showContactGivesInline?: boolean;
|
||||
}
|
||||
|
||||
@Component({ components: { QuickNav } })
|
||||
export default class AccountViewView extends Vue {
|
||||
$notify!: (notification: Notification, timeout?: number) => void;
|
||||
@@ -386,8 +386,8 @@ export default class AccountViewView extends Vue {
|
||||
apiServer = "";
|
||||
apiServerInput = "";
|
||||
derivationPath = "";
|
||||
firstName = "";
|
||||
lastName = "";
|
||||
givenName = "";
|
||||
isRegistered = false;
|
||||
numAccounts = 0;
|
||||
publicHex = "";
|
||||
publicBase64 = "";
|
||||
@@ -402,8 +402,6 @@ export default class AccountViewView extends Vue {
|
||||
showPubCopy = false;
|
||||
|
||||
showAdvanced = false;
|
||||
alertMessage = "";
|
||||
alertTitle = "";
|
||||
|
||||
public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
|
||||
try {
|
||||
@@ -428,7 +426,7 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
// Return parsed identity or null if not found
|
||||
return JSON.parse(account?.identity || "null");
|
||||
return JSON.parse((account?.identity as string) || "null");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,12 +507,14 @@ export default class AccountViewView extends Vue {
|
||||
* Initializes component state with values from the database or defaults.
|
||||
* @param {SettingsType} settings - Object containing settings from the database.
|
||||
*/
|
||||
initializeState(settings: SettingsType | undefined) {
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.apiServerInput = settings?.apiServer || "";
|
||||
this.firstName = settings?.firstName || "";
|
||||
this.lastName = settings?.lastName || "";
|
||||
initializeState(settings: Settings | undefined) {
|
||||
this.activeDid = (settings?.activeDid as string) || "";
|
||||
this.apiServer = (settings?.apiServer as string) || "";
|
||||
this.apiServerInput = (settings?.apiServer as string) || "";
|
||||
this.givenName =
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
this.showContactGives = !!settings?.showContactGivesInline;
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ export default class AccountViewView extends Vue {
|
||||
) {
|
||||
this.publicHex = identity.keys[0].publicKeyHex;
|
||||
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64");
|
||||
this.derivationPath = identity.keys[0].meta.derivationPath;
|
||||
this.derivationPath = identity.keys[0].meta.derivationPath as string;
|
||||
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: identity.did,
|
||||
@@ -701,6 +701,27 @@ export default class AccountViewView extends Vue {
|
||||
const resp = await this.fetchRateLimits(identity);
|
||||
if (resp.status === 200) {
|
||||
this.limits = resp.data;
|
||||
if (!this.isRegistered) {
|
||||
// the user is not known to be registered, but they are so let's record it
|
||||
try {
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
isRegistered: true,
|
||||
});
|
||||
this.isRegistered = true;
|
||||
} catch (err) {
|
||||
console.log("Got an error updating settings:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Update Error",
|
||||
text: "Unable to update your settings. Check claim limits again.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.handleRateLimitsError(error);
|
||||
@@ -729,8 +750,13 @@ export default class AccountViewView extends Vue {
|
||||
private handleRateLimitsError(error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
const data = error.response?.data as ErrorResponse;
|
||||
this.limitsMessage = data?.error?.message || "Bad server response.";
|
||||
console.error("Bad response retrieving limits:", error);
|
||||
this.limitsMessage =
|
||||
(data?.error?.message as string) || "Bad server response.";
|
||||
console.log(
|
||||
"Got bad response retrieving limits, which usually means user isn't registered. Server says:",
|
||||
this.limitsMessage,
|
||||
//error,
|
||||
);
|
||||
} else if (
|
||||
error instanceof Error &&
|
||||
error.message ===
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- Quick Search -->
|
||||
|
||||
<!-- Initial Loading Animation -->
|
||||
|
||||
<!-- Results List -->
|
||||
<ul class="border-t border-slate-300">
|
||||
<li class="border-b border-slate-300 py-3">
|
||||
@@ -70,12 +66,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<GiftedDialog
|
||||
ref="customDialog"
|
||||
@dialog-result="handleDialogResult"
|
||||
message="Received from"
|
||||
>
|
||||
</GiftedDialog>
|
||||
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -83,16 +74,10 @@
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { AccountsSchema } from "@/db/tables/accounts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { Account, AccountsSchema } from "@/db/tables/accounts";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
CreateAndSubmitGiveResult,
|
||||
ErrorResult,
|
||||
GiverInputInfo,
|
||||
GiverOutputInfo,
|
||||
} from "@/libs/endorserServer";
|
||||
import { GiverInputInfo } from "@/libs/endorserServer";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
@@ -124,10 +109,10 @@ export default class ContactGiftingView extends Vue {
|
||||
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first();
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
@@ -150,7 +135,7 @@ export default class ContactGiftingView extends Vue {
|
||||
async created() {
|
||||
try {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
@@ -173,123 +158,5 @@ export default class ContactGiftingView extends Vue {
|
||||
openDialog(giver: GiverInputInfo) {
|
||||
(this.$refs.customDialog as GiftedDialog).open(giver);
|
||||
}
|
||||
|
||||
handleDialogResult(result: GiverOutputInfo) {
|
||||
if (result.action === "confirm") {
|
||||
return new Promise((resolve) => {
|
||||
this.recordGive(
|
||||
result.giver?.did,
|
||||
result.description,
|
||||
result.hours,
|
||||
).then(() => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// action was "cancel" so do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param giverDid may be null
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
*/
|
||||
public async recordGive(
|
||||
giverDid?: string,
|
||||
description?: string,
|
||||
hours?: number,
|
||||
) {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must select an identity before you can record a give.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description && !hours) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must enter a description or some number of hours.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
const result = await createAndSubmitGive(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
identity,
|
||||
giverDid,
|
||||
this.activeDid,
|
||||
description,
|
||||
hours,
|
||||
);
|
||||
|
||||
if (this.isGiveCreationError(result)) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
console.log("Error with give result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: errorMessage || "There was an error recording the give.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
text: "That gift was recorded.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.log("Error with give caught:", error);
|
||||
|
||||
const message =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
"There was an error recording the Give.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
isGiveCreationError(result: CreateAndSubmitGiveResult) {
|
||||
return result.type == "error";
|
||||
}
|
||||
|
||||
getGiveCreationErrorMessage(result: CreateAndSubmitGiveResult) {
|
||||
return (result as ErrorResult).error?.userMessage;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -108,7 +108,9 @@ export default class ContactQRScanShow extends Vue {
|
||||
iat: Date.now(),
|
||||
iss: this.activeDid,
|
||||
own: {
|
||||
name: (settings?.firstName || "") + " " + (settings?.lastName || ""),
|
||||
name:
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
|
||||
publicEncKey,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -256,7 +256,7 @@ import { NotificationIface } from "@/constants/app";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import {
|
||||
accessToken,
|
||||
getContactPayloadFromJwtUrl,
|
||||
@@ -271,6 +271,7 @@ import {
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const Buffer = require("buffer/").Buffer;
|
||||
@@ -308,7 +309,7 @@ export default class ContactsView extends Vue {
|
||||
|
||||
async created() {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
|
||||
@@ -332,7 +333,7 @@ export default class ContactsView extends Vue {
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const accounts = await accountsDB.accounts.toArray();
|
||||
const account = R.find((acc) => acc.did === activeDid, accounts);
|
||||
const account = R.find((acc) => acc.did === activeDid, accounts) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
@@ -394,7 +395,7 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
title: "Server Error",
|
||||
text:
|
||||
"Got an error retrieving your " +
|
||||
(useRecipient ? "given" : "received") +
|
||||
@@ -453,7 +454,7 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
title: "Server Error",
|
||||
text: error as string,
|
||||
},
|
||||
-1,
|
||||
@@ -589,6 +590,16 @@ export default class ContactsView extends Vue {
|
||||
"?",
|
||||
)
|
||||
) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
text: "",
|
||||
title: "Registration submitted...",
|
||||
},
|
||||
1000,
|
||||
);
|
||||
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
|
||||
const vcClaim: RegisterVerifiableCredential = {
|
||||
@@ -671,7 +682,7 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
title: "Server Error",
|
||||
text: userMessage,
|
||||
},
|
||||
-1,
|
||||
@@ -696,36 +707,30 @@ export default class ContactsView extends Vue {
|
||||
contact.seesMe = visibility;
|
||||
db.contacts.update(contact.did, { seesMe: visibility });
|
||||
} else {
|
||||
console.error("Bad response setting visibility: ", resp.data);
|
||||
if (resp.data.error?.message) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: resp.data.error?.message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: "Bad server response of " + resp.status,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
console.error(
|
||||
"Got some bad server response when setting visibility: ",
|
||||
resp,
|
||||
);
|
||||
const message =
|
||||
resp.data.error?.message || "Bad server response of " + resp.status;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Got some server error when setting visibility:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: err as string,
|
||||
title: "Server Error",
|
||||
text: "Check connectivity and try again.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -750,7 +755,7 @@ export default class ContactsView extends Vue {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "toast",
|
||||
type: "info",
|
||||
title: "Refreshed",
|
||||
text:
|
||||
this.nameForContact(contact, true) +
|
||||
@@ -758,38 +763,29 @@ export default class ContactsView extends Vue {
|
||||
(visibility ? "" : "not ") +
|
||||
"see your activity.",
|
||||
},
|
||||
5000,
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
if (resp.data.error?.message) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: resp.data.error?.message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: "Bad server response of " + resp.status,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
console.log("Got bad server response when checking visibility: ", resp);
|
||||
const message = resp.data.error?.message || "Got bad server response.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Server Error",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Caught error from server request to check visibility:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
text: err as string,
|
||||
title: "Server Error",
|
||||
text: "Check connectivity and try again.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
@@ -989,7 +985,7 @@ export default class ContactsView extends Vue {
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error With Server",
|
||||
title: "Server Error",
|
||||
text: userMessage,
|
||||
},
|
||||
-1,
|
||||
|
||||
@@ -6,64 +6,82 @@
|
||||
Time Safari
|
||||
</h1>
|
||||
|
||||
<!-- show the actions for recognizing a give -->
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-bold">Record a Gift</h2>
|
||||
|
||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||
<li @click="openDialog()">
|
||||
<EntityIcon
|
||||
:entityId="null"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
<h3
|
||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
Anonymous
|
||||
</h3>
|
||||
</li>
|
||||
<li
|
||||
v-for="contact in allContacts"
|
||||
:key="contact.did"
|
||||
@click="openDialog(contact)"
|
||||
<div v-if="!activeDid">
|
||||
To record others' giving,
|
||||
<router-link :to="{ name: 'start' }" class="text-blue-500">
|
||||
create your identifier.</router-link
|
||||
>
|
||||
<EntityIcon
|
||||
:entityId="contact.did"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
<h3
|
||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isRegistered">
|
||||
To record others' giving, someone must register your account, so show
|
||||
them
|
||||
<router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
|
||||
your identity info</router-link
|
||||
>
|
||||
and then
|
||||
<router-link :to="{ name: 'account' }" class="text-blue-500">
|
||||
check your limits.</router-link
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- activeDid && isRegistered -->
|
||||
<h2 class="text-xl font-bold">Record a Gift</h2>
|
||||
|
||||
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5">
|
||||
<li @click="openDialog()">
|
||||
<EntityIcon
|
||||
:entityId="null"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
<h3
|
||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
Anonymous/Unnamed
|
||||
</h3>
|
||||
</li>
|
||||
<li
|
||||
v-for="contact in allContacts"
|
||||
:key="contact.did"
|
||||
@click="openDialog(contact)"
|
||||
>
|
||||
{{ contact.name || contact.did }}
|
||||
</h3>
|
||||
</li>
|
||||
</ul>
|
||||
<EntityIcon
|
||||
:entityId="contact.did"
|
||||
:iconSize="64"
|
||||
class="mx-auto border border-slate-300 rounded-md mb-1"
|
||||
></EntityIcon>
|
||||
<h3
|
||||
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
{{ contact.name || contact.did }}
|
||||
</h3>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
||||
<router-link
|
||||
v-if="allContacts.length > 7"
|
||||
:to="{ name: 'contact-gives' }"
|
||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||
>
|
||||
Show More Contacts…
|
||||
</router-link>
|
||||
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
||||
<router-link
|
||||
v-if="allContacts.length >= 7"
|
||||
:to="{ name: 'contact-gives' }"
|
||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||
>
|
||||
Show More Contacts…
|
||||
</router-link>
|
||||
|
||||
<!-- If there are no contacts, show this instead: -->
|
||||
<div
|
||||
class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
|
||||
v-if="allContacts.length === 0"
|
||||
>
|
||||
(No contacts to show.)
|
||||
<!-- If there are no contacts, show this instead: -->
|
||||
<div
|
||||
class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
|
||||
v-if="allContacts.length === 0"
|
||||
>
|
||||
(No contacts to show.)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GiftedDialog
|
||||
ref="customDialog"
|
||||
@dialog-result="handleDialogResult"
|
||||
message="Received from"
|
||||
>
|
||||
</GiftedDialog>
|
||||
<GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
|
||||
|
||||
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4">
|
||||
<h2 class="text-xl font-bold mb-4">Latest Activity</h2>
|
||||
@@ -99,19 +117,18 @@
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
GiverInputInfo,
|
||||
GiverOutputInfo,
|
||||
GiveServerRecord,
|
||||
} from "@/libs/endorserServer";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
@@ -135,6 +152,7 @@ export default class HomeView extends Vue {
|
||||
feedPreviousOldestId?: string;
|
||||
feedLastViewedId?: string;
|
||||
isHiddenSpinner = true;
|
||||
isRegistered = false;
|
||||
numAccounts = 0;
|
||||
|
||||
async beforeCreate() {
|
||||
@@ -144,10 +162,10 @@ export default class HomeView extends Vue {
|
||||
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first();
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
@@ -174,11 +192,12 @@ export default class HomeView extends Vue {
|
||||
this.allMyDids = allAccounts.map((acc) => acc.did);
|
||||
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
this.feedLastViewedId = settings?.lastViewedClaimId;
|
||||
this.isRegistered = !!settings?.isRegistered;
|
||||
this.updateAllFeed();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
@@ -204,7 +223,9 @@ export default class HomeView extends Vue {
|
||||
if (this.activeDid) {
|
||||
await accountsDB.open();
|
||||
const allAccounts = await accountsDB.accounts.toArray();
|
||||
const account = allAccounts.find((acc) => acc.did === this.activeDid);
|
||||
const account = allAccounts.find(
|
||||
(acc) => acc.did === this.activeDid,
|
||||
) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
@@ -333,139 +354,5 @@ export default class HomeView extends Vue {
|
||||
openDialog(giver: GiverInputInfo) {
|
||||
(this.$refs.customDialog as GiftedDialog).open(giver);
|
||||
}
|
||||
|
||||
handleDialogResult(result: GiverOutputInfo) {
|
||||
if (result.action === "confirm") {
|
||||
return new Promise((resolve) => {
|
||||
this.recordGive(
|
||||
result.giver?.did,
|
||||
result.description,
|
||||
result.hours,
|
||||
).then(() => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// action was "cancel" so do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param giverDid may be null
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
*/
|
||||
public async recordGive(
|
||||
giverDid?: string,
|
||||
description?: string,
|
||||
hours?: number,
|
||||
) {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must select an identity before you can record a give.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description && !hours) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must enter a description or some number of hours.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
const result = await createAndSubmitGive(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
identity,
|
||||
giverDid,
|
||||
this.activeDid,
|
||||
description,
|
||||
hours,
|
||||
);
|
||||
|
||||
if (
|
||||
result.type === "error" ||
|
||||
this.isGiveCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
console.log("Error with give creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: errorMessage || "There was an error creating the give.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
text: "That gift was recorded.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
console.log("Error with give recordation caught:", error);
|
||||
const message =
|
||||
error.userMessage ||
|
||||
error.response?.data?.error?.message ||
|
||||
"There was an error recording the give.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
/**
|
||||
* @param result response "data" from the server
|
||||
* @returns true if the result indicates an error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isGiveCreationError(result: any) {
|
||||
return result.status !== 201 || result.data?.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||
* @returns best guess at an error message
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getGiveCreationErrorMessage(result: any) {
|
||||
return (
|
||||
result.error?.userMessage ||
|
||||
result.error?.error ||
|
||||
result.response?.data?.error?.message
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
|
||||
<span class="overflow-hidden">
|
||||
<h2 class="text-xl font-semibold mb-0">
|
||||
{{ firstName }} {{ lastName }}
|
||||
{{ givenName }}
|
||||
</h2>
|
||||
<div class="text-sm text-slate-500 truncate">
|
||||
<b>ID:</b> <code>{{ activeDid }}</code>
|
||||
@@ -71,7 +71,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
||||
import { AppString } from "@/constants/app";
|
||||
import { db, accountsDB } from "@/db/index";
|
||||
import { AccountsSchema } from "@/db/tables/accounts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
|
||||
interface Notification {
|
||||
@@ -90,8 +90,7 @@ export default class IdentitySwitcherView extends Vue {
|
||||
public activeDid = "";
|
||||
public apiServer = "";
|
||||
public apiServerInput = "";
|
||||
public firstName = "";
|
||||
public lastName = "";
|
||||
public givenName = "";
|
||||
public otherIdentities: Array<{ did: string }> = [];
|
||||
public showContactGives = false;
|
||||
|
||||
@@ -101,19 +100,20 @@ export default class IdentitySwitcherView extends Vue {
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first();
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
const identity = JSON.parse((account?.identity as string) || "null");
|
||||
return identity;
|
||||
}
|
||||
|
||||
async created() {
|
||||
try {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.apiServerInput = settings?.apiServer || "";
|
||||
this.firstName = settings?.firstName || "No";
|
||||
this.lastName = settings?.lastName || "Name";
|
||||
this.givenName =
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
|
||||
this.showContactGives = !!settings?.showContactGivesInline;
|
||||
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
@@ -151,7 +151,7 @@ export default class IdentitySwitcherView extends Vue {
|
||||
did = undefined;
|
||||
}
|
||||
await db.open();
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: did,
|
||||
});
|
||||
this.activeDid = did || "";
|
||||
|
||||
@@ -10,21 +10,15 @@
|
||||
>
|
||||
<fa icon="chevron-left" class="fa-fw"></fa>
|
||||
</button>
|
||||
[New/Edit] Identity
|
||||
Edit Identity
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="First Name"
|
||||
placeholder="Name"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
v-model="firstName"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Last Name"
|
||||
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
|
||||
v-model="lastName"
|
||||
v-model="givenName"
|
||||
/>
|
||||
|
||||
<div class="mt-8">
|
||||
@@ -50,36 +44,30 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { db } from "@/db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
|
||||
@Component({
|
||||
components: {},
|
||||
})
|
||||
export default class NewEditAccountView extends Vue {
|
||||
firstName =
|
||||
localStorage.getItem("firstName") === null
|
||||
? "--"
|
||||
: localStorage.getItem("firstName");
|
||||
lastName =
|
||||
localStorage.getItem("lastName") === null
|
||||
? "--"
|
||||
: localStorage.getItem("lastName");
|
||||
givenName = "";
|
||||
|
||||
// 'created' hook runs when the Vue instance is first created
|
||||
async created() {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
this.firstName = settings?.firstName || "";
|
||||
this.lastName = settings?.lastName || "";
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.givenName =
|
||||
(settings?.firstName || "") +
|
||||
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
|
||||
}
|
||||
|
||||
onClickSaveChanges() {
|
||||
db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
firstName: this.firstName,
|
||||
lastName: this.lastName,
|
||||
firstName: this.givenName,
|
||||
lastName: "", // deprecated, pre v 0.1.3
|
||||
});
|
||||
localStorage.setItem("firstName", this.firstName as string);
|
||||
localStorage.setItem("lastName", this.lastName as string);
|
||||
localStorage.setItem("firstName", this.givenName as string);
|
||||
localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
|
||||
this.$router.push({ name: "account" });
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<h3
|
||||
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
>
|
||||
Anonymous
|
||||
Anonymous/Unnamed
|
||||
</h3>
|
||||
</li>
|
||||
<li
|
||||
@@ -125,7 +125,7 @@
|
||||
|
||||
<!-- Ideally, this button should only be visible when the active account has more than 7 or 11 contacts in their list (we want to limit the grid count above to 8 or 12 accounts to keep it compact) -->
|
||||
<router-link
|
||||
v-if="allContacts.length > 7"
|
||||
v-if="allContacts.length >= 7"
|
||||
:to="{ name: 'contact-gives' }"
|
||||
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md"
|
||||
>
|
||||
@@ -196,8 +196,8 @@
|
||||
|
||||
<GiftedDialog
|
||||
ref="customDialog"
|
||||
@dialog-result="handleDialogResult"
|
||||
message="Received from"
|
||||
:projectId="this.projectId"
|
||||
>
|
||||
</GiftedDialog>
|
||||
</section>
|
||||
@@ -212,18 +212,16 @@ import { Component, Vue } from "vue-facing-decorator";
|
||||
import GiftedDialog from "@/components/GiftedDialog.vue";
|
||||
import { accountsDB, db } from "@/db/index";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
|
||||
import { accessToken } from "@/libs/crypto";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
GiverInputInfo,
|
||||
GiverOutputInfo,
|
||||
GiveServerRecord,
|
||||
ResultWithType,
|
||||
} from "@/libs/endorserServer";
|
||||
import QuickNav from "@/components/QuickNav.vue";
|
||||
import EntityIcon from "@/components/EntityIcon.vue";
|
||||
import { Account } from "@/db/tables/accounts";
|
||||
|
||||
interface Notification {
|
||||
group: string;
|
||||
@@ -257,7 +255,7 @@ export default class ProjectViewView extends Vue {
|
||||
|
||||
async created() {
|
||||
await db.open();
|
||||
const settings = await db.settings.get(MASTER_SETTINGS_KEY);
|
||||
const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
|
||||
this.activeDid = settings?.activeDid || "";
|
||||
this.apiServer = settings?.apiServer || "";
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
@@ -266,17 +264,17 @@ export default class ProjectViewView extends Vue {
|
||||
const accounts = accountsDB.accounts;
|
||||
const accountsArr = await accounts?.toArray();
|
||||
this.allMyDids = accountsArr.map((acc) => acc.did);
|
||||
const account = accountsArr?.find((acc) => acc.did === this.activeDid);
|
||||
const account = accountsArr.find((acc) => acc.did === this.activeDid);
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
this.LoadProject(identity);
|
||||
}
|
||||
|
||||
public async getIdentity(activeDid: string) {
|
||||
await accountsDB.open();
|
||||
const account = await accountsDB.accounts
|
||||
const account = (await accountsDB.accounts
|
||||
.where("did")
|
||||
.equals(activeDid)
|
||||
.first();
|
||||
.first()) as Account;
|
||||
const identity = JSON.parse(account?.identity || "null");
|
||||
|
||||
if (!identity) {
|
||||
@@ -461,11 +459,6 @@ export default class ProjectViewView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
openDialog(contact: GiverInputInfo) {
|
||||
const dialog: GiftedDialog = this.$refs.customDialog as GiftedDialog;
|
||||
dialog.open(contact);
|
||||
}
|
||||
|
||||
getOpenStreetMapUrl() {
|
||||
// Google URL is https://maps.google.com/?q=LAT,LONG
|
||||
return (
|
||||
@@ -480,96 +473,8 @@ export default class ProjectViewView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
handleDialogResult(result: GiverOutputInfo) {
|
||||
if (result.action === "confirm") {
|
||||
return new Promise((resolve) => {
|
||||
this.recordGive(
|
||||
result.giver?.did,
|
||||
result.description,
|
||||
result.hours,
|
||||
).then(() => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// action was not "confirm" so do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param giverDid may be null
|
||||
* @param description may be an empty string
|
||||
* @param hours may be 0
|
||||
*/
|
||||
async recordGive(giverDid?: string, description?: string, hours?: number) {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must select an identity before you can record a give.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!description && !hours) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "You must enter a description or some number of hours.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
const identity = await this.getIdentity(this.activeDid);
|
||||
const result = await createAndSubmitGive(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
identity,
|
||||
giverDid,
|
||||
this.activeDid,
|
||||
description,
|
||||
hours,
|
||||
this.projectId,
|
||||
);
|
||||
if (result.type == "success") {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "success",
|
||||
title: "Success",
|
||||
text: "That gift was recorded.",
|
||||
},
|
||||
-1,
|
||||
);
|
||||
} else {
|
||||
console.log("Error with give creation:", result);
|
||||
if (result.type != "error") {
|
||||
console.log(
|
||||
"... and it has an unexpected result type of",
|
||||
(result as ResultWithType).type,
|
||||
);
|
||||
}
|
||||
const message =
|
||||
result?.error?.userMessage ||
|
||||
"There was an error recording the Give.";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: message,
|
||||
},
|
||||
-1,
|
||||
);
|
||||
}
|
||||
}
|
||||
openDialog(contact: GiverInputInfo) {
|
||||
(this.$refs.customDialog as GiftedDialog).open(contact);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user