consolidate into GiftedDialog because the result was always the same #76

Merged
trentlarson merged 12 commits from more-smalls into master 1 year ago
  1. 2
      package.json
  2. 11
      project.task.yaml
  3. 215
      src/components/GiftedDialog.vue
  4. 3
      src/db/tables/settings.ts
  5. 2
      src/router/index.ts
  6. 266
      src/views/AccountViewView.vue
  7. 147
      src/views/ContactGiftingView.vue
  8. 4
      src/views/ContactQRScanShowView.vue
  9. 108
      src/views/ContactsView.vue
  10. 271
      src/views/HomeView.vue
  11. 18
      src/views/IdentitySwitcherView.vue
  12. 38
      src/views/NewEditAccountView.vue
  13. 117
      src/views/ProjectViewView.vue

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "kickstart-for-time-pwa", "name": "kickstart-for-time-pwa",
"version": "0.1.2", "version": "0.1.3",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",

11
project.task.yaml

@ -6,14 +6,10 @@ tasks:
- 40 notifications : - 40 notifications :
- push, where we trigger a ServiceWorker(?) in the app to reach out and check for new data assignee:matthew - 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 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 - 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 - 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 - 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 - .1 Remove notification alert visuals on home page
- .5 Add infinite scroll to gifts on the 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 - .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 - 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 Display a more appealing confirmation on the map when erasing the marker assignee-group:ui
- .5 make a VC details page - .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+ : - contacts v+ :
- 01 Import all the non-sensitive data (ie. contacts & settings). - 01 Import all the non-sensitive data (ie. contacts & settings).

215
src/components/GiftedDialog.vue

@ -2,7 +2,7 @@
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4"> <h1 class="text-xl font-bold text-center mb-4">
{{ message }} {{ giver?.name || "somebody not specified" }} {{ message }} {{ giver?.name || "somebody not named" }}
</h1> </h1>
<input <input
type="text" type="text"
@ -51,18 +51,57 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop, Emit } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { GiverInputInfo, GiverOutputInfo } from "@/libs/endorserServer"; 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 @Component
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
$notify!: (notification: Notification, timeout?: number) => void;
@Prop message = ""; @Prop message = "";
@Prop projectId = "";
activeDid = "";
apiServer = "";
giver?: GiverInputInfo; giver?: GiverInputInfo;
description = ""; description = "";
hours = "0"; hours = "0";
visible = false; 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) { open(giver: GiverInputInfo) {
this.giver = giver; this.giver = giver;
this.visible = true; this.visible = true;
@ -80,27 +119,169 @@ export default class GiftedDialog extends Vue {
this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`; this.hours = `${Math.max(0, (parseFloat(this.hours) || 1) - 1)}`;
} }
@Emit("dialog-result") cancel() {
confirm(): GiverOutputInfo {
const result = {
action: "confirm",
giver: this.giver,
hours: parseFloat(this.hours),
description: this.description,
};
this.close(); this.close();
this.description = ""; this.description = "";
this.giver = undefined; this.giver = undefined;
this.hours = "0"; this.hours = "0";
return result;
} }
@Emit("dialog-result") async confirm() {
cancel(): GiverOutputInfo {
const result = { action: "cancel" };
this.close(); 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> </script>

3
src/db/tables/settings.ts

@ -12,7 +12,8 @@ export type Settings = {
activeDid?: string; activeDid?: string;
apiServer?: string; apiServer?: string;
firstName?: string; firstName?: string;
lastName?: string; isRegistered?: boolean;
lastName?: string; // deprecated, pre v 0.1.3
lastViewedClaimId?: string; lastViewedClaimId?: string;
searchBoxes?: Array<{ searchBoxes?: Array<{
name: string; name: string;

2
src/router/index.ts

@ -33,7 +33,6 @@ const routes: Array<RouteRecordRaw> = [
name: "home", name: "home",
component: () => component: () =>
import(/* webpackChunkName: "home" */ "../views/HomeView.vue"), import(/* webpackChunkName: "home" */ "../views/HomeView.vue"),
beforeEnter: enterOrStart,
}, },
{ {
path: "/account", path: "/account",
@ -79,7 +78,6 @@ const routes: Array<RouteRecordRaw> = [
name: "contacts", name: "contacts",
component: () => component: () =>
import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"), import(/* webpackChunkName: "contacts" */ "../views/ContactsView.vue"),
beforeEnter: enterOrStart,
}, },
{ {
path: "/scan-contact", path: "/scan-contact",

266
src/views/AccountViewView.vue

@ -52,7 +52,17 @@
<!-- Identity Details --> <!-- Identity Details -->
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <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-slate-500 text-sm font-bold">ID</div>
<div class="text-sm text-slate-500 flex justify-start items-center mb-1"> <div class="text-sm text-slate-500 flex justify-start items-center mb-1">
@ -67,53 +77,11 @@
</button> </button>
<span v-show="showDidCopy">Copied!</span> <span v-show="showDidCopy">Copied!</span>
</div> </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> </div>
<router-link <router-link
:to="{ name: 'new-edit-account' }" :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 Edit Identity
</router-link> </router-link>
@ -132,8 +100,10 @@
) )
" "
> >
<!-- label -->
<div>App Notifications</div>
<!-- toggle --> <!-- toggle -->
<div class="relative"> <div class="relative ml-2">
<!-- input --> <!-- input -->
<input type="checkbox" name="toggleNotifications" class="sr-only" /> <input type="checkbox" name="toggleNotifications" class="sr-only" />
<!-- line --> <!-- line -->
@ -143,8 +113,6 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition" class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div> ></div>
</div> </div>
<!-- label -->
<div class="ml-2">App Notifications</div>
</label> </label>
<label <label
for="toggleMuteNotifications" for="toggleMuteNotifications"
@ -159,8 +127,10 @@
) )
" "
> >
<!-- label -->
<div>Mute Notifications</div>
<!-- toggle --> <!-- toggle -->
<div class="relative"> <div class="relative ml-2">
<!-- input --> <!-- input -->
<input <input
type="checkbox" type="checkbox"
@ -174,8 +144,6 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition" class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div> ></div>
</div> </div>
<!-- label -->
<div class="ml-2">Mute Notifications</div>
</label> </label>
</div> </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" 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()" @click="exportDatabase()"
> >
Download Settings & Contacts (excluding Identifier Data) Download Settings & Contacts
<br />
(excluding Identifier Data)
</a> </a>
<a ref="downloadLink" /> <a ref="downloadLink" />
<!-- QR code popup --> <div v-if="activeDid" class="flex py-2">
<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">
<button class="text-center text-md text-blue-500" @click="checkLimits()"> <button class="text-center text-md text-blue-500" @click="checkLimits()">
Check Limits Check Limits
</button> </button>
@ -252,14 +201,76 @@
> >
Advanced Advanced
</h3> </h3>
<div v-if="showAdvanced"> <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 <label
for="toggleShowAmounts" for="toggleShowAmounts"
class="flex items-center cursor-pointer mb-6" class="flex items-center cursor-pointer py-2"
@click="handleChange" @click="handleChange"
> >
<!-- label -->
<h2 class="text-slate-500 text-sm font-bold mb-2">
Show amounts given with contacts
</h2>
<!-- toggle --> <!-- toggle -->
<div class="relative"> <div class="relative ml-2">
<!-- input --> <!-- input -->
<input <input
type="checkbox" type="checkbox"
@ -274,23 +285,31 @@
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition" class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
></div> ></div>
</div> </div>
<!-- label -->
<div class="ml-2">Show amounts given with contacts</div>
</label> </label>
<div class="flex py-2"> <div class="flex py-2">
<!-- id used by puppeteer test script --> <button class="text-blue-500">
<router-link <!-- id used by puppeteer test script -->
id="switch-identity-link" <router-link
:to="{ name: 'identity-switcher' }" id="switch-identity-link"
class="block text-center text-md uppercase bg-slate-500 text-white px-1.5 py-2 rounded-md mb-2" :to="{ name: 'identity-switcher' }"
> class="block text-center"
Switch Identity / No Identity >
</router-link> Switch Identity / No Identity
</router-link>
</button>
</div> </div>
<div class="flex py-2"> <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 <input
type="text" type="text"
class="block w-full rounded border border-slate-400 px-3 py-2" class="block w-full rounded border border-slate-400 px-3 py-2"
@ -322,17 +341,6 @@
Use Local Use Local
</button> </button>
</div> </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> </div>
</section> </section>
</template> </template>
@ -346,7 +354,7 @@ import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; 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 { accessToken } from "@/libs/crypto";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { ErrorResponse, RateLimits } from "@/libs/endorserServer"; import { ErrorResponse, RateLimits } from "@/libs/endorserServer";
@ -368,14 +376,6 @@ interface IAccount {
derivationPath: string; derivationPath: string;
} }
interface SettingsType {
activeDid?: string;
apiServer?: string;
firstName?: string;
lastName?: string;
showContactGivesInline?: boolean;
}
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class AccountViewView extends Vue { export default class AccountViewView extends Vue {
$notify!: (notification: Notification, timeout?: number) => void; $notify!: (notification: Notification, timeout?: number) => void;
@ -386,8 +386,8 @@ export default class AccountViewView extends Vue {
apiServer = ""; apiServer = "";
apiServerInput = ""; apiServerInput = "";
derivationPath = ""; derivationPath = "";
firstName = ""; givenName = "";
lastName = ""; isRegistered = false;
numAccounts = 0; numAccounts = 0;
publicHex = ""; publicHex = "";
publicBase64 = ""; publicBase64 = "";
@ -402,8 +402,6 @@ export default class AccountViewView extends Vue {
showPubCopy = false; showPubCopy = false;
showAdvanced = false; showAdvanced = false;
alertMessage = "";
alertTitle = "";
public async getIdentity(activeDid: string): Promise<IIdentifier | null> { public async getIdentity(activeDid: string): Promise<IIdentifier | null> {
try { try {
@ -428,7 +426,7 @@ export default class AccountViewView extends Vue {
} }
// Return parsed identity or null if not found // 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. * Initializes component state with values from the database or defaults.
* @param {SettingsType} settings - Object containing settings from the database. * @param {SettingsType} settings - Object containing settings from the database.
*/ */
initializeState(settings: SettingsType | undefined) { initializeState(settings: Settings | undefined) {
this.activeDid = settings?.activeDid || ""; this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = (settings?.apiServer as string) || "";
this.apiServerInput = settings?.apiServer || ""; this.apiServerInput = (settings?.apiServer as string) || "";
this.firstName = settings?.firstName || ""; this.givenName =
this.lastName = settings?.lastName || ""; (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // pre v 0.1.3
this.isRegistered = !!settings?.isRegistered;
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
} }
@ -531,7 +531,7 @@ export default class AccountViewView extends Vue {
) { ) {
this.publicHex = identity.keys[0].publicKeyHex; this.publicHex = identity.keys[0].publicKeyHex;
this.publicBase64 = Buffer.from(this.publicHex, "hex").toString("base64"); 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, { db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: identity.did, activeDid: identity.did,
@ -701,6 +701,27 @@ export default class AccountViewView extends Vue {
const resp = await this.fetchRateLimits(identity); const resp = await this.fetchRateLimits(identity);
if (resp.status === 200) { if (resp.status === 200) {
this.limits = resp.data; 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) { } catch (error) {
this.handleRateLimitsError(error); this.handleRateLimitsError(error);
@ -729,8 +750,13 @@ export default class AccountViewView extends Vue {
private handleRateLimitsError(error: unknown) { private handleRateLimitsError(error: unknown) {
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
const data = error.response?.data as ErrorResponse; const data = error.response?.data as ErrorResponse;
this.limitsMessage = data?.error?.message || "Bad server response."; this.limitsMessage =
console.error("Bad response retrieving limits:", error); (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 ( } else if (
error instanceof Error && error instanceof Error &&
error.message === error.message ===

147
src/views/ContactGiftingView.vue

@ -16,10 +16,6 @@
</h1> </h1>
</div> </div>
<!-- Quick Search -->
<!-- Initial Loading Animation -->
<!-- Results List --> <!-- Results List -->
<ul class="border-t border-slate-300"> <ul class="border-t border-slate-300">
<li class="border-b border-slate-300 py-3"> <li class="border-b border-slate-300 py-3">
@ -70,12 +66,7 @@
</li> </li>
</ul> </ul>
<GiftedDialog <GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
</section> </section>
</template> </template>
@ -83,16 +74,10 @@
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts"; import { Account, AccountsSchema } from "@/db/tables/accounts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { accessToken } from "@/libs/crypto"; import { accessToken } from "@/libs/crypto";
import { import { GiverInputInfo } from "@/libs/endorserServer";
createAndSubmitGive,
CreateAndSubmitGiveResult,
ErrorResult,
GiverInputInfo,
GiverOutputInfo,
} from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
@ -124,10 +109,10 @@ export default class ContactGiftingView extends Vue {
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = (await accountsDB.accounts
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first(); .first()) as Account;
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
@ -150,7 +135,7 @@ export default class ContactGiftingView extends Vue {
async created() { async created() {
try { try {
await db.open(); 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.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
@ -173,123 +158,5 @@ export default class ContactGiftingView extends Vue {
openDialog(giver: GiverInputInfo) { openDialog(giver: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver); (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> </script>

4
src/views/ContactQRScanShowView.vue

@ -108,7 +108,9 @@ export default class ContactQRScanShow extends Vue {
iat: Date.now(), iat: Date.now(),
iss: this.activeDid, iss: this.activeDid,
own: { own: {
name: (settings?.firstName || "") + " " + (settings?.lastName || ""), name:
(settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""), // deprecated, pre v 0.1.3
publicEncKey, publicEncKey,
}, },
}; };

108
src/views/ContactsView.vue

@ -256,7 +256,7 @@ import { NotificationIface } from "@/constants/app";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
import { import {
accessToken, accessToken,
getContactPayloadFromJwtUrl, getContactPayloadFromJwtUrl,
@ -271,6 +271,7 @@ import {
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts";
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const Buffer = require("buffer/").Buffer; const Buffer = require("buffer/").Buffer;
@ -308,7 +309,7 @@ export default class ContactsView extends Vue {
async created() { async created() {
await db.open(); 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.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
@ -332,7 +333,7 @@ export default class ContactsView extends Vue {
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); 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"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
@ -394,7 +395,7 @@ export default class ContactsView extends Vue {
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: text:
"Got an error retrieving your " + "Got an error retrieving your " +
(useRecipient ? "given" : "received") + (useRecipient ? "given" : "received") +
@ -453,7 +454,7 @@ export default class ContactsView extends Vue {
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: error as string, text: error as string,
}, },
-1, -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 identity = await this.getIdentity(this.activeDid);
const vcClaim: RegisterVerifiableCredential = { const vcClaim: RegisterVerifiableCredential = {
@ -671,7 +682,7 @@ export default class ContactsView extends Vue {
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: userMessage, text: userMessage,
}, },
-1, -1,
@ -696,36 +707,30 @@ export default class ContactsView extends Vue {
contact.seesMe = visibility; contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility }); db.contacts.update(contact.did, { seesMe: visibility });
} else { } else {
console.error("Bad response setting visibility: ", resp.data); console.error(
if (resp.data.error?.message) { "Got some bad server response when setting visibility: ",
this.$notify( resp,
{ );
group: "alert", const message =
type: "danger", resp.data.error?.message || "Bad server response of " + resp.status;
title: "Error With Server", this.$notify(
text: resp.data.error?.message, {
}, group: "alert",
-1, type: "danger",
); title: "Server Error",
} else { text: message,
this.$notify( },
{ -1,
group: "alert", );
type: "danger",
title: "Error With Server",
text: "Bad server response of " + resp.status,
},
-1,
);
}
} }
} catch (err) { } catch (err) {
console.error("Got some server error when setting visibility:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: err as string, text: "Check connectivity and try again.",
}, },
-1, -1,
); );
@ -750,7 +755,7 @@ export default class ContactsView extends Vue {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "toast", type: "info",
title: "Refreshed", title: "Refreshed",
text: text:
this.nameForContact(contact, true) + this.nameForContact(contact, true) +
@ -758,38 +763,29 @@ export default class ContactsView extends Vue {
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity.", "see your activity.",
}, },
5000, -1,
); );
} else { } else {
if (resp.data.error?.message) { console.log("Got bad server response when checking visibility: ", resp);
this.$notify( const message = resp.data.error?.message || "Got bad server response.";
{ this.$notify(
group: "alert", {
type: "danger", group: "alert",
title: "Error With Server", type: "danger",
text: resp.data.error?.message, title: "Server Error",
}, text: message,
-1, },
); -1,
} else { );
this.$notify(
{
group: "alert",
type: "danger",
title: "Error With Server",
text: "Bad server response of " + resp.status,
},
-1,
);
}
} }
} catch (err) { } catch (err) {
console.log("Caught error from server request to check visibility:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: err as string, text: "Check connectivity and try again.",
}, },
-1, -1,
); );
@ -989,7 +985,7 @@ export default class ContactsView extends Vue {
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error With Server", title: "Server Error",
text: userMessage, text: userMessage,
}, },
-1, -1,

271
src/views/HomeView.vue

@ -6,64 +6,82 @@
Time Safari Time Safari
</h1> </h1>
<!-- show the actions for recognizing a give -->
<div class="mb-8"> <div class="mb-8">
<h2 class="text-xl font-bold">Record a Gift</h2> <div v-if="!activeDid">
To record others' giving,
<ul class="grid grid-cols-4 gap-x-3 gap-y-5 text-center mb-5"> <router-link :to="{ name: 'start' }" class="text-blue-500">
<li @click="openDialog()"> create your identifier.</router-link
<EntityIcon >
:entityId="null" </div>
:iconSize="64"
class="mx-auto border border-slate-300 rounded-md mb-1" <div v-else-if="!isRegistered">
></EntityIcon> To record others' giving, someone must register your account, so show
<h3 them
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden" <router-link :to="{ name: 'contact-qr' }" class="text-blue-500">
> your identity info</router-link
Anonymous >
</h3> and then
</li> <router-link :to="{ name: 'account' }" class="text-blue-500">
<li check your limits.</router-link
v-for="contact in allContacts"
:key="contact.did"
@click="openDialog(contact)"
> >
<EntityIcon </div>
:entityId="contact.did"
:iconSize="64" <div v-else>
class="mx-auto border border-slate-300 rounded-md mb-1" <!-- activeDid && isRegistered -->
></EntityIcon> <h2 class="text-xl font-bold">Record a Gift</h2>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden" <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 }} <EntityIcon
</h3> :entityId="contact.did"
</li> :iconSize="64"
</ul> 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&hellip;
</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) --> <!-- If there are no contacts, show this instead: -->
<router-link <div
v-if="allContacts.length > 7" class="rounded border border-dashed border-slate-300 bg-slate-100 px-4 py-3 text-center italic text-slate-500"
:to="{ name: 'contact-gives' }" v-if="allContacts.length === 0"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md" >
> (No contacts to show.)
Show More Contacts&hellip; </div>
</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.)
</div> </div>
</div> </div>
<GiftedDialog <GiftedDialog ref="customDialog" message="Received from"> </GiftedDialog>
ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from"
>
</GiftedDialog>
<div class="bg-slate-100 rounded-md overflow-hidden px-4 py-3 mb-4"> <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> <h2 class="text-xl font-bold mb-4">Latest Activity</h2>
@ -99,19 +117,18 @@
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { db, accountsDB } from "@/db/index"; 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 { accessToken } from "@/libs/crypto";
import { import {
createAndSubmitGive,
didInfo, didInfo,
GiverInputInfo, GiverInputInfo,
GiverOutputInfo,
GiveServerRecord, GiveServerRecord,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { Account } from "@/db/tables/accounts";
interface Notification { interface Notification {
group: string; group: string;
@ -135,6 +152,7 @@ export default class HomeView extends Vue {
feedPreviousOldestId?: string; feedPreviousOldestId?: string;
feedLastViewedId?: string; feedLastViewedId?: string;
isHiddenSpinner = true; isHiddenSpinner = true;
isRegistered = false;
numAccounts = 0; numAccounts = 0;
async beforeCreate() { async beforeCreate() {
@ -144,10 +162,10 @@ export default class HomeView extends Vue {
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = (await accountsDB.accounts
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first(); .first()) as Account;
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
@ -174,11 +192,12 @@ export default class HomeView extends Vue {
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
await db.open(); 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.apiServer = settings?.apiServer || "";
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
this.feedLastViewedId = settings?.lastViewedClaimId; this.feedLastViewedId = settings?.lastViewedClaimId;
this.isRegistered = !!settings?.isRegistered;
this.updateAllFeed(); this.updateAllFeed();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
@ -204,7 +223,9 @@ export default class HomeView extends Vue {
if (this.activeDid) { if (this.activeDid) {
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); 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"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { if (!identity) {
@ -333,139 +354,5 @@ export default class HomeView extends Vue {
openDialog(giver: GiverInputInfo) { openDialog(giver: GiverInputInfo) {
(this.$refs.customDialog as GiftedDialog).open(giver); (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> </script>

18
src/views/IdentitySwitcherView.vue

@ -22,7 +22,7 @@
<fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa> <fa icon="circle-check" class="fa-fw text-blue-600 text-xl mr-3"></fa>
<span class="overflow-hidden"> <span class="overflow-hidden">
<h2 class="text-xl font-semibold mb-0"> <h2 class="text-xl font-semibold mb-0">
{{ firstName }} {{ lastName }} {{ givenName }}
</h2> </h2>
<div class="text-sm text-slate-500 truncate"> <div class="text-sm text-slate-500 truncate">
<b>ID:</b> <code>{{ activeDid }}</code> <b>ID:</b> <code>{{ activeDid }}</code>
@ -71,7 +71,7 @@ import { Component, Vue } from "vue-facing-decorator";
import { AppString } from "@/constants/app"; import { AppString } from "@/constants/app";
import { db, accountsDB } from "@/db/index"; import { db, accountsDB } from "@/db/index";
import { AccountsSchema } from "@/db/tables/accounts"; 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"; import QuickNav from "@/components/QuickNav.vue";
interface Notification { interface Notification {
@ -90,8 +90,7 @@ export default class IdentitySwitcherView extends Vue {
public activeDid = ""; public activeDid = "";
public apiServer = ""; public apiServer = "";
public apiServerInput = ""; public apiServerInput = "";
public firstName = ""; public givenName = "";
public lastName = "";
public otherIdentities: Array<{ did: string }> = []; public otherIdentities: Array<{ did: string }> = [];
public showContactGives = false; public showContactGives = false;
@ -101,19 +100,20 @@ export default class IdentitySwitcherView extends Vue {
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first(); .first();
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse((account?.identity as string) || "null");
return identity; return identity;
} }
async created() { async created() {
try { try {
await db.open(); 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.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.apiServerInput = settings?.apiServer || ""; this.apiServerInput = settings?.apiServer || "";
this.firstName = settings?.firstName || "No"; this.givenName =
this.lastName = settings?.lastName || "Name"; (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
this.showContactGives = !!settings?.showContactGivesInline; this.showContactGives = !!settings?.showContactGivesInline;
const identity = await this.getIdentity(this.activeDid); const identity = await this.getIdentity(this.activeDid);
@ -151,7 +151,7 @@ export default class IdentitySwitcherView extends Vue {
did = undefined; did = undefined;
} }
await db.open(); await db.open();
db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did, activeDid: did,
}); });
this.activeDid = did || ""; this.activeDid = did || "";

38
src/views/NewEditAccountView.vue

@ -10,21 +10,15 @@
> >
<fa icon="chevron-left" class="fa-fw"></fa> <fa icon="chevron-left" class="fa-fw"></fa>
</button> </button>
[New/Edit] Identity Edit Identity
</h1> </h1>
</div> </div>
<input <input
type="text" type="text"
placeholder="First Name" placeholder="Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="firstName" v-model="givenName"
/>
<input
type="text"
placeholder="Last Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="lastName"
/> />
<div class="mt-8"> <div class="mt-8">
@ -50,36 +44,30 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { db } from "@/db/index"; import { db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; import { MASTER_SETTINGS_KEY, Settings } from "@/db/tables/settings";
@Component({ @Component({
components: {}, components: {},
}) })
export default class NewEditAccountView extends Vue { export default class NewEditAccountView extends Vue {
firstName = givenName = "";
localStorage.getItem("firstName") === null
? "--"
: localStorage.getItem("firstName");
lastName =
localStorage.getItem("lastName") === null
? "--"
: localStorage.getItem("lastName");
// 'created' hook runs when the Vue instance is first created // 'created' hook runs when the Vue instance is first created
async created() { async created() {
await db.open(); await db.open();
const settings = await db.settings.get(MASTER_SETTINGS_KEY); const settings = (await db.settings.get(MASTER_SETTINGS_KEY)) as Settings;
this.firstName = settings?.firstName || ""; this.givenName =
this.lastName = settings?.lastName || ""; (settings?.firstName || "") +
(settings?.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
} }
onClickSaveChanges() { onClickSaveChanges() {
db.settings.update(MASTER_SETTINGS_KEY, { db.settings.update(MASTER_SETTINGS_KEY, {
firstName: this.firstName, firstName: this.givenName,
lastName: this.lastName, lastName: "", // deprecated, pre v 0.1.3
}); });
localStorage.setItem("firstName", this.firstName as string); localStorage.setItem("firstName", this.givenName as string);
localStorage.setItem("lastName", this.lastName as string); localStorage.setItem("lastName", ""); // deprecated, pre v 0.1.3
this.$router.push({ name: "account" }); this.$router.push({ name: "account" });
} }

117
src/views/ProjectViewView.vue

@ -102,7 +102,7 @@
<h3 <h3
class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden" class="text-xs italic font-medium text-ellipsis whitespace-nowrap overflow-hidden"
> >
Anonymous Anonymous/Unnamed
</h3> </h3>
</li> </li>
<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) --> <!-- 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 <router-link
v-if="allContacts.length > 7" v-if="allContacts.length >= 7"
:to="{ name: 'contact-gives' }" :to="{ name: 'contact-gives' }"
class="block text-center text-md font-bold uppercase bg-slate-500 text-white px-2 py-3 rounded-md" 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 <GiftedDialog
ref="customDialog" ref="customDialog"
@dialog-result="handleDialogResult"
message="Received from" message="Received from"
:projectId="this.projectId"
> >
</GiftedDialog> </GiftedDialog>
</section> </section>
@ -212,18 +212,16 @@ import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "@/components/GiftedDialog.vue"; import GiftedDialog from "@/components/GiftedDialog.vue";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Contact } from "@/db/tables/contacts"; 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 { accessToken } from "@/libs/crypto";
import { import {
createAndSubmitGive,
didInfo, didInfo,
GiverInputInfo, GiverInputInfo,
GiverOutputInfo,
GiveServerRecord, GiveServerRecord,
ResultWithType,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
import { Account } from "@/db/tables/accounts";
interface Notification { interface Notification {
group: string; group: string;
@ -257,7 +255,7 @@ export default class ProjectViewView extends Vue {
async created() { async created() {
await db.open(); 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.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
@ -266,17 +264,17 @@ export default class ProjectViewView extends Vue {
const accounts = accountsDB.accounts; const accounts = accountsDB.accounts;
const accountsArr = await accounts?.toArray(); const accountsArr = await accounts?.toArray();
this.allMyDids = accountsArr.map((acc) => acc.did); 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"); const identity = JSON.parse(account?.identity || "null");
this.LoadProject(identity); this.LoadProject(identity);
} }
public async getIdentity(activeDid: string) { public async getIdentity(activeDid: string) {
await accountsDB.open(); await accountsDB.open();
const account = await accountsDB.accounts const account = (await accountsDB.accounts
.where("did") .where("did")
.equals(activeDid) .equals(activeDid)
.first(); .first()) as Account;
const identity = JSON.parse(account?.identity || "null"); const identity = JSON.parse(account?.identity || "null");
if (!identity) { 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() { getOpenStreetMapUrl() {
// Google URL is https://maps.google.com/?q=LAT,LONG // Google URL is https://maps.google.com/?q=LAT,LONG
return ( return (
@ -480,96 +473,8 @@ export default class ProjectViewView extends Vue {
); );
} }
handleDialogResult(result: GiverOutputInfo) { openDialog(contact: GiverInputInfo) {
if (result.action === "confirm") { (this.$refs.customDialog as GiftedDialog).open(contact);
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,
);
}
}
} }
} }
</script> </script>

Loading…
Cancel
Save