Browse Source

move contact actions into the details page (prepping for checkboxes)

Trent Larson 3 months ago
parent
commit
511be5f9a2
  1. 2
      playwright.config-local.ts
  2. 29
      src/libs/util.ts
  3. 329
      src/views/ContactsView.vue
  4. 439
      src/views/DIDView.vue
  5. 4
      test-playwright/30-record-gift.spec.ts
  6. 9
      test-playwright/40-add-contact.spec.ts

2
playwright.config-local.ts

@ -74,7 +74,7 @@ export default defineConfig({
/* Configure global timeout; default is 30000 milliseconds */ /* Configure global timeout; default is 30000 milliseconds */
// the image upload will often not succeed at 5 seconds // the image upload will often not succeed at 5 seconds
//timeout: 10000, timeout: 15000,
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
/** /**

29
src/libs/util.ts

@ -1,11 +1,14 @@
// many of these are also found in endorser-mobile utility.ts // many of these are also found in endorser-mobile utility.ts
import axios, { AxiosResponse } from "axios"; import axios, { AxiosResponse } from "axios";
import { Buffer } from "buffer";
import * as R from "ramda";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { DEFAULT_PUSH_SERVER } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES, DEFAULT_PASSKEY_EXPIRATION_MINUTES,
MASTER_SETTINGS_KEY, MASTER_SETTINGS_KEY,
@ -18,11 +21,9 @@ import {
OfferVerifiableCredential, OfferVerifiableCredential,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as serverUtil from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer";
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
import { Buffer } from "buffer";
import { KeyMeta } from "@/libs/crypto/vc"; import { KeyMeta } from "@/libs/crypto/vc";
import { createPeerDid } from "@/libs/crypto/vc/didPeer"; import { createPeerDid } from "@/libs/crypto/vc/didPeer";
import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer";
export const PRIVACY_MESSAGE = export const PRIVACY_MESSAGE =
"The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow."; "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow.";
@ -91,6 +92,28 @@ export const isGiveAction = (
return veriClaim.claimType === "GiveAction"; return veriClaim.claimType === "GiveAction";
}; };
export const nameForDid = (
activeDid: string,
contacts: Array<Contact>,
did: string,
): string => {
if (did === activeDid) {
return "you";
}
const contact = R.find((con) => con.did == did, contacts);
return nameForContact(contact);
};
export const nameForContact = (
contact?: Contact,
capitalize?: boolean,
): string => {
return (
(contact?.name as string) ||
(capitalize ? "This" : "this") + " unnamed user"
);
};
export const doCopyTwoSecRedo = (text: string, fn: () => void) => { export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
fn(); fn();
useClipboard() useClipboard()

329
src/views/ContactsView.vue

@ -37,7 +37,7 @@
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400" class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
@click="onClickNewContact()" @click="onClickNewContact()"
> >
<fa icon="plus" class="fa-fw"></fa> <fa icon="plus" class="fa-fw" />
</button> </button>
</div> </div>
@ -82,10 +82,10 @@
<ul <ul
id="listContacts" id="listContacts"
v-if="contacts.length > 0" v-if="contacts.length > 0"
class="border-t border-slate-300" class="border-t border-slate-300 mt-1"
> >
<li <li
class="border-b border-slate-300 pt-2.5 pb-4" class="border-b border-slate-300 pt-1 pb-1"
v-for="contact in contacts" v-for="contact in contacts"
:key="contact.did" :key="contact.did"
> >
@ -98,131 +98,16 @@
@click="showLargeIdenticon = contact" @click="showLargeIdenticon = contact"
/> />
{{ contact.name || AppString.NO_CONTACT_NAME }} {{ contact.name || AppString.NO_CONTACT_NAME }}
<button
@click="
contactEdit = contact;
contactNewName = contact.name || '';
"
title="Edit"
>
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1"></fa>
</button>
<router-link <router-link
:to="{ :to="{
path: '/did/' + encodeURIComponent(contact.did), path: '/did/' + encodeURIComponent(contact.did),
}" }"
title="See more about this DID" title="See more about this person"
> >
<fa icon="circle-info" class="text-blue-500 ml-4" /> <fa icon="circle-info" class="text-blue-500 ml-4" />
</router-link> </router-link>
</h2> </h2>
<div class="text-sm truncate">
Identifier:
<button
@click="
libsUtil.doCopyTwoSecRedo(
contact.did,
() => (showDidCopy = !showDidCopy),
)
"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDidCopy" class="text-green-500">Copied DID</span>
{{ contact.did }}
</div>
<div class="text-sm truncate" v-if="contact.publicKeyBase64">
Public Key (base 64):
<button
@click="
libsUtil.doCopyTwoSecRedo(
contact.publicKeyBase64,
() => (showPubKeyCopy = !showPubKeyCopy),
)
"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showPubKeyCopy" class="text-green-500"
>Copied Key</span
>
{{ contact.publicKeyBase64 }}
</div>
<div class="text-sm truncate" v-if="contact.nextPubKeyHashB64">
Next Public Key Hash (base 64):
<button
@click="
libsUtil.doCopyTwoSecRedo(
contact.nextPubKeyHashB64,
() => (showPubKeyHashCopy = !showPubKeyHashCopy),
)
"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showPubKeyHashCopy" class="text-green-500"
>Copied Hash</span
>
{{ contact.nextPubKeyHashB64 }}
</div>
<div id="ContactActions" class="flex gap-1.5 mt-2"> <div id="ContactActions" class="flex gap-1.5 mt-2">
<div v-if="activeDid">
<button
v-if="contact.seesMe && contact.did !== activeDid"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="confirmSetVisibility(contact, false)"
title="They can see you"
>
<fa icon="eye" class="fa-fw" />
</button>
<button
v-else-if="!contact.seesMe && contact.did !== activeDid"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="confirmSetVisibility(contact, true)"
title="They cannot see you"
>
<fa icon="eye-slash" class="fa-fw" />
</button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="eye" class="text-white mx-2.5" />
<button
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)"
title="Check Visibility"
v-if="contact.did !== activeDid"
>
<fa icon="rotate" class="fa-fw" />
</button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="rotate" class="text-white mx-2.5" />
<button
@click="confirmRegister(contact)"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 px-2 py-1.5 rounded-md"
v-if="contact.did !== activeDid"
title="Registration"
>
<fa
v-if="contact.registered"
icon="person-circle-check"
class="fa-fw"
/>
<fa v-else icon="person-circle-question" class="fa-fw" />
</button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="rotate" class="text-white ml-6 px-2.5" />
</div>
<button
@click="confirmDeleteContact(contact)"
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 px-2 py-1.5 rounded-md"
title="Delete"
>
<fa icon="trash-can" class="fa-fw" />
</button>
<div <div
v-if="showGiveNumbers && contact.did != activeDid" v-if="showGiveNumbers && contact.did != activeDid"
class="ml-auto flex gap-1.5" class="ml-auto flex gap-1.5"
@ -308,33 +193,6 @@
/> />
</div> </div>
</div> </div>
<div v-if="contactEdit !== null" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
<input
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="Name"
v-model="contactNewName"
/>
<div class="flex justify-between">
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
@click="onClickSaveName(contactEdit, contactNewName)"
>
<fa icon="save" />
</button>
<span class="inline-block w-2" />
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
@click="onClickCancelName()"
>
<fa icon="ban" />
</button>
</div>
</div>
</div>
</section> </section>
</template> </template>
@ -782,55 +640,29 @@ export default class ContactsView extends Vue {
}); });
} }
// prompt with confirmation if they want to delete a contact // note that this is also in DIDView.vue
confirmDeleteContact(contact: Contact) { async confirmSetVisibility(contact: Contact, visibility: boolean) {
this.$notify( const visibilityPrompt = visibility
{ ? "Are you sure you want to make your activity visible to them?"
group: "modal", : "Are you sure you want to hide all your activity from them?";
type: "confirm",
title: "Delete",
text:
"Are you sure you want to remove " +
this.nameForDid(this.contacts, contact.did) +
" with DID " +
contact.did +
" from your contact list?",
onYes: async () => {
await this.deleteContact(contact);
},
},
-1,
);
}
async deleteContact(contact: Contact) {
await db.open();
await db.contacts.delete(contact.did);
this.contacts = R.without([contact], this.contacts);
}
// confirm to register a new contact
async confirmRegister(contact: Contact) {
this.$notify( this.$notify(
{ {
group: "modal", group: "modal",
type: "confirm", type: "confirm",
title: "Register", title: "Set Visibility",
text: text: visibilityPrompt,
"Are you sure you want to register " +
this.nameForDid(this.contacts, contact.did) +
(contact.registered
? " -- especially since they are already marked as registered"
: "") +
"?",
onYes: async () => { onYes: async () => {
await this.register(contact); const success = await this.setVisibility(contact, visibility, true);
if (success) {
contact.seesMe = visibility; // didn't work inside setVisibility
}
}, },
}, },
-1, -1,
); );
} }
// note that this is also in DIDView.vue
async register(contact: Contact) { async register(contact: Contact) {
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000); this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
@ -896,27 +728,7 @@ export default class ContactsView extends Vue {
} }
} }
async confirmSetVisibility(contact: Contact, visibility: boolean) { // note that this is also in DIDView.vue
const visibilityPrompt = visibility
? "Are you sure you want to make your activity visible to them?"
: "Are you sure you want to hide all your activity from them?";
this.$notify(
{
group: "modal",
type: "confirm",
title: "Set Visibility",
text: visibilityPrompt,
onYes: async () => {
const success = await this.setVisibility(contact, visibility, true);
if (success) {
contact.seesMe = visibility; // didn't work inside setVisibility
}
},
},
-1,
);
}
async setVisibility( async setVisibility(
contact: Contact, contact: Contact,
visibility: boolean, visibility: boolean,
@ -966,6 +778,7 @@ export default class ContactsView extends Vue {
} }
} }
// note that this is also in DIDView.vue
async checkVisibility(contact: Contact) { async checkVisibility(contact: Contact) {
const url = const url =
this.apiServer + this.apiServer +
@ -999,7 +812,7 @@ export default class ContactsView extends Vue {
type: "info", type: "info",
title: "Visibility Refreshed", title: "Visibility Refreshed",
text: text:
this.nameForContact(contact, true) + libsUtil.nameForContact(contact, true) +
" can " + " can " +
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity.", "see your activity.",
@ -1033,21 +846,6 @@ export default class ContactsView extends Vue {
} }
} }
private nameForDid(contacts: Array<Contact>, did: string): string {
if (did === this.activeDid) {
return "you";
}
const contact = R.find((con) => con.did == did, contacts);
return this.nameForContact(contact);
}
private nameForContact(contact?: Contact, capitalize?: boolean): string {
return (
(contact?.name as string) ||
(capitalize ? "This" : "this") + " unnamed user"
);
}
confirmShowGiftedDialog(giverDid: string, recipientDid: string) { confirmShowGiftedDialog(giverDid: string, recipientDid: string) {
// if they have unconfirmed amounts, ask to confirm those // if they have unconfirmed amounts, ask to confirm those
if ( if (
@ -1093,13 +891,13 @@ export default class ContactsView extends Vue {
if (giverDid) { if (giverDid) {
giver = { giver = {
did: giverDid, did: giverDid,
name: this.nameForDid(this.contacts, giverDid), name: libsUtil.nameForDid(this.activeDid, this.contacts, giverDid),
}; };
} }
if (recipientDid) { if (recipientDid) {
receiver = { receiver = {
did: recipientDid, did: recipientDid,
name: this.nameForDid(this.contacts, recipientDid), name: libsUtil.nameForDid(this.activeDid, this.contacts, recipientDid),
}; };
} }
@ -1131,25 +929,13 @@ export default class ContactsView extends Vue {
); );
} }
openOfferDialog(recipientDid: string, recipientName: string) { openOfferDialog(recipientDid: string, recipientName?: string) {
(this.$refs.customOfferDialog as OfferDialog).open( (this.$refs.customOfferDialog as OfferDialog).open(
recipientDid, recipientDid,
recipientName, recipientName,
); );
} }
private async onClickCancelName() {
this.contactEdit = null;
this.contactNewName = "";
}
private async onClickSaveName(contact: Contact, newName: string) {
contact.name = newName;
return db.contacts
.update(contact.did, { name: newName })
.then(() => (this.contactEdit = null));
}
public async toggleShowContactAmounts() { public async toggleShowContactAmounts() {
const newShowValue = !this.showGiveNumbers; const newShowValue = !this.showGiveNumbers;
try { try {
@ -1211,74 +997,3 @@ export default class ContactsView extends Vue {
} }
} }
</script> </script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 500px;
}
/*
Tooltip, generated on "title" attributes on "fa" icons
Kudos to https://www.w3schools.com/css/css_tooltip.asp
*/
/* Tooltip container */
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
}
/* Tooltip text */
.tooltip .tooltiptext {
visibility: hidden;
width: 200px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
}
/* How do we share with the above so code isn't duplicated? */
.tooltip .tooltiptext-left {
visibility: hidden;
width: 200px;
background-color: black;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
position: absolute;
z-index: 1;
bottom: 0%;
right: 105%;
margin-left: -60px;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
}
.tooltip:hover .tooltiptext-left {
visibility: visible;
}
</style>

439
src/views/DIDView.vue

@ -26,8 +26,20 @@
didInfoForContact(viewingDid, activeDid, contact, allMyDids) didInfoForContact(viewingDid, activeDid, contact, allMyDids)
.displayName .displayName
}} }}
<button
@click="
contactEdit = true;
contactNewName = contact.name || '';
"
title="Edit"
>
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
</button>
</h2> </h2>
<button @click="showDidDetails = !showDidDetails" class="ml-2 mr-2"> <button
@click="showDidDetails = !showDidDetails"
class="ml-2 mr-2 mt-4"
>
Details Details
<fa v-if="showDidDetails" icon="chevron-up" class="text-blue-400" /> <fa v-if="showDidDetails" icon="chevron-up" class="text-blue-400" />
<fa v-else icon="chevron-down" class="text-blue-400" /> <fa v-else icon="chevron-down" class="text-blue-400" />
@ -49,15 +61,76 @@
/> />
</span> </span>
</div> </div>
<div class="mt-4"> <div class="flex justify-between mt-4">
<div class="flex justify-center">Auto-Generated Icon:</div> <div class="flex items-center">
<div class="flex justify-center"> <div v-if="activeDid" class="flex justify-between">
<EntityIcon <div>
:entityId="viewingDid" <button
:iconSize="64" v-if="contact?.seesMe && contact.did !== activeDid"
class="inline-block align-middle border border-slate-300 rounded-md mr-1" class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="showLargeIdenticonId = viewingDid" @click="confirmSetVisibility(contact, false)"
/> title="They can see you"
>
<fa icon="eye" class="fa-fw" />
</button>
<button
v-else-if="!contact?.seesMe && contact?.did !== activeDid"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="confirmSetVisibility(contact, true)"
title="They cannot see you"
>
<fa icon="eye-slash" class="fa-fw" />
</button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="eye" class="text-white mx-2.5" />
<button
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="checkVisibility(contact)"
title="Check Visibility"
v-if="contact?.did !== activeDid"
>
<fa icon="rotate" class="fa-fw" />
</button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="rotate" class="text-white mx-2.5" />
</div>
<button
@click="confirmRegister(contact)"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
v-if="contact?.did !== activeDid"
title="Registration"
>
<fa
v-if="contact?.registered"
icon="person-circle-check"
class="fa-fw"
/>
<fa v-else icon="person-circle-question" class="fa-fw" />
</button>
<!-- otherwise it's this user so hide it -->
<fa v-else icon="rotate" class="text-white ml-6 px-2.5" />
</div>
<button
@click="confirmDeleteContact(contact)"
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Delete"
>
<fa icon="trash-can" class="fa-fw" />
</button>
</div>
<div v-if="!contact?.profileImageUrl">
<div>Auto-Generated Icon</div>
<div class="flex justify-center">
<EntityIcon
:entityId="viewingDid"
:iconSize="64"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
@click="showLargeIdenticonId = viewingDid"
/>
</div>
</div> </div>
</div> </div>
<div <div
@ -80,6 +153,32 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="contactEdit" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Edit Name</h1>
<input
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="Name"
v-model="contactNewName"
/>
<div class="flex justify-between">
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
@click="onClickSaveName(contactNewName)"
>
<fa icon="save" />
</button>
<span class="inline-block w-2" />
<button
class="text-sm bg-blue-600 text-white px-2 py-1.5 rounded -ml-1.5 border-l border-blue-400"
@click="onClickCancelName()"
>
<fa icon="ban" />
</button>
</div>
</div>
</div>
<!-- Loading Animation --> <!-- Loading Animation -->
<div <div
@ -126,15 +225,16 @@
v-if="!isLoading && claims.length === 0" v-if="!isLoading && claims.length === 0"
class="flex justify-center mt-4" class="flex justify-center mt-4"
> >
<span>They Are in No Claims Visible to You</span> <span>They are in no claims visible to you.</span>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { AxiosError } from "axios";
import * as yaml from "js-yaml";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import * as yaml from "js-yaml";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
import InfiniteScroll from "@/components/InfiniteScroll.vue"; import InfiniteScroll from "@/components/InfiniteScroll.vue";
@ -152,6 +252,8 @@ import {
GenericVerifiableCredential, GenericVerifiableCredential,
GiveVerifiableCredential, GiveVerifiableCredential,
OfferVerifiableCredential, OfferVerifiableCredential,
register,
setVisibilityUtil,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import EntityIcon from "@/components/EntityIcon.vue"; import EntityIcon from "@/components/EntityIcon.vue";
@ -174,7 +276,9 @@ export default class DIDView extends Vue {
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
apiServer = ""; apiServer = "";
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = []; claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
contact?: Contact; contact: Contact;
contactEdit = false;
contactNewName?: string;
contactYaml = ""; contactYaml = "";
hitEnd = false; hitEnd = false;
isLoading = false; isLoading = false;
@ -195,23 +299,29 @@ export default class DIDView extends Vue {
this.apiServer = (settings?.apiServer as string) || ""; this.apiServer = (settings?.apiServer as string) || "";
const pathParam = window.location.pathname.substring("/did/".length); const pathParam = window.location.pathname.substring("/did/".length);
let theContact: Contact | undefined;
if (pathParam) { if (pathParam) {
this.viewingDid = decodeURIComponent(pathParam); this.viewingDid = decodeURIComponent(pathParam);
this.contact = await db.contacts.get(this.viewingDid); theContact = await db.contacts.get(this.viewingDid);
this.contactYaml = yaml.dump(this.contact); }
await this.loadClaimsAbout(); if (theContact) {
this.contact = theContact;
} else { } else {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: "No claim ID was provided.", text: "No valid claim ID was provided.",
}, },
-1, -1,
); );
return;
} }
this.contactYaml = yaml.dump(this.contact);
await this.loadClaimsAbout();
await accountsDB.open(); await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray(); const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did); this.allMyDids = allAccounts.map((acc) => acc.did);
@ -227,6 +337,128 @@ export default class DIDView extends Vue {
} }
} }
// prompt with confirmation if they want to delete a contact
confirmDeleteContact(contact: Contact) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Delete",
text:
"Are you sure you want to remove " +
libsUtil.nameForContact(contact, false) +
" from your contact list?",
onYes: async () => {
await this.deleteContact(contact);
},
},
-1,
);
}
async deleteContact(contact: Contact) {
await db.open();
await db.contacts.delete(contact.did);
this.$notify(
{
group: "alert",
type: "success",
title: "Deleted",
text: "Contact has been removed.",
},
3000,
);
(this.$router as Router).push({ name: "contacts" });
}
// confirm to register a new contact
async confirmRegister(contact: Contact) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Register",
text:
"Are you sure you want to register " +
libsUtil.nameForContact(this.contact, false) +
(contact.registered
? " -- especially since they are already marked as registered"
: "") +
"?",
onYes: async () => {
await this.register(contact);
},
},
-1,
);
}
// note that this is also in ContactView.vue
async register(contact: Contact) {
this.$notify({ group: "alert", type: "toast", title: "Sent..." }, 1000);
try {
const regResult = await register(
this.activeDid,
this.apiServer,
this.axios,
contact,
);
if (regResult.success) {
contact.registered = true;
await db.contacts.update(contact.did, { registered: true });
this.$notify(
{
group: "alert",
type: "success",
title: "Registration Success",
text:
(contact.name || "That unnamed person") + " has been registered.",
},
5000,
);
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Registration Error",
text:
(regResult.error as string) ||
"Something went wrong during registration.",
},
5000,
);
}
} catch (error) {
console.error("Error when registering:", error);
let userMessage = "There was an error. See logs for more info.";
const serverError = error as AxiosError;
if (serverError) {
if (serverError.response?.data?.error?.message) {
userMessage = serverError.response.data.error.message;
} else if (serverError.message) {
userMessage = serverError.message; // Info for the user
} else {
userMessage = JSON.stringify(serverError.toJSON());
}
} else {
userMessage = error as string;
}
// Now set that error for the user to see.
this.$notify(
{
group: "alert",
type: "danger",
title: "Registration Error",
text: userMessage,
},
5000,
);
}
}
public async loadClaimsAbout() { public async loadClaimsAbout() {
if (!this.viewingDid) { if (!this.viewingDid) {
console.error("This should never be called without a DID."); console.error("This should never be called without a DID.");
@ -323,5 +555,178 @@ export default class DIDView extends Vue {
claimDescription(claim: GenericVerifiableCredential) { claimDescription(claim: GenericVerifiableCredential) {
return claim.claim.name || claim.claim.description || ""; return claim.claim.name || claim.claim.description || "";
} }
private async onClickCancelName() {
this.contactEdit = false;
}
private async onClickSaveName(newName: string) {
this.contact.name = newName;
return db.contacts
.update(this.contact.did, { name: newName })
.then(() => (this.contactEdit = false));
}
// note that this is also in ContactView.vue
async confirmSetVisibility(contact: Contact, visibility: boolean) {
const visibilityPrompt = visibility
? "Are you sure you want to make your activity visible to them?"
: "Are you sure you want to hide all your activity from them?";
this.$notify(
{
group: "modal",
type: "confirm",
title: "Set Visibility",
text: visibilityPrompt,
onYes: async () => {
const success = await this.setVisibility(contact, visibility, true);
if (success) {
contact.seesMe = visibility; // didn't work inside setVisibility
}
},
},
-1,
);
}
// note that this is also in ContactView.vue
async setVisibility(
contact: Contact,
visibility: boolean,
showSuccessAlert: boolean,
) {
const result = await setVisibilityUtil(
this.activeDid,
this.apiServer,
this.axios,
db,
contact,
visibility,
);
if (result.success) {
//contact.seesMe = visibility; // why doesn't it affect the UI from here?
//console.log("Set result & seesMe", result, contact.seesMe, contact.did);
if (showSuccessAlert) {
this.$notify(
{
group: "alert",
type: "success",
title: "Visibility Set",
text:
(contact.name || "That user") +
" can " +
(visibility ? "" : "not ") +
"see your activity.",
},
3000,
);
}
return true;
} else {
console.error("Got strange result from setting visibility:", result);
const message =
(result.error as string) || "Could not set visibility on the server.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Visibility",
text: message,
},
5000,
);
return false;
}
}
// note that this is also in ContactView.vue
async checkVisibility(contact: Contact) {
const url =
this.apiServer +
"/api/report/canDidExplicitlySeeMe?did=" +
encodeURIComponent(contact.did);
const headers = await getHeaders(this.activeDid);
if (!headers["Authorization"]) {
this.$notify(
{
group: "alert",
type: "danger",
title: "No Identity",
text: "There is no identity to use to check visibility.",
},
3000,
);
return;
}
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
const visibility = resp.data;
contact.seesMe = visibility;
//console.log("Visi check:", visibility, contact.seesMe, contact.did);
await db.contacts.update(contact.did, { seesMe: visibility });
this.$notify(
{
group: "alert",
type: "info",
title: "Visibility Refreshed",
text:
libsUtil.nameForContact(contact, true) +
" can " +
(visibility ? "" : "not ") +
"see your activity.",
},
3000,
);
} else {
console.error("Got bad server response checking visibility:", resp);
const message = resp.data.error?.message || "Got bad server response.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Checking Visibility",
text: message,
},
5000,
);
}
} catch (err) {
console.error("Caught error from request to check visibility:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Checking Visibility",
text: "Check connectivity and try again.",
},
3000,
);
}
}
} }
</script> </script>
<style>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.dialog {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
width: 100%;
max-width: 500px;
}
</style>

4
test-playwright/30-record-gift.spec.ts

@ -2,8 +2,8 @@ import { test, expect } from '@playwright/test';
import { importUser } from './testUtils'; import { importUser } from './testUtils';
test('Record something given', async ({ page }) => { test('Record something given', async ({ page }) => {
// Generate a random string of 3 characters // Generate a random string of a few characters
const randomString = Math.random().toString(36).substring(2, 5); const randomString = Math.random().toString(36).substring(2, 6);
// Generate a random non-zero single-digit number // Generate a random non-zero single-digit number
const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1; const randomNonZeroNumber = Math.floor(Math.random() * 99) + 1;

9
test-playwright/40-add-contact.spec.ts

@ -21,14 +21,14 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
const finalTitle = standardTitle + finalRandomString; const finalTitle = standardTitle + finalRandomString;
// Contact name // Contact name
const contactName = 'Contact 00'; const contactName = 'Contact #000';
// Import user 01 // Import user 01
await importUser(page, '01'); await importUser(page, '01');
// Add new contact 00 // Add new contact 00
await page.goto('./contacts'); await page.goto('./contacts');
await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F'); await page.getByPlaceholder('URL or DID, Name, Public Key').fill('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F, User #000');
await page.locator('button > svg.fa-plus').click(); await page.locator('button > svg.fa-plus').click();
await expect(page.locator('div[role="alert"]')).toBeVisible(); await expect(page.locator('div[role="alert"]')).toBeVisible();
@ -36,10 +36,11 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
// await page.locator('div[role="alert"] button:has-text("Yes")').click(); // await page.locator('div[role="alert"] button:has-text("Yes")').click();
// Verify added contact // Verify added contact
await expect(page.locator('li.border-b')).toContainText('did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F'); await expect(page.locator('li.border-b')).toContainText('User #000');
// Rename contact // Rename contact
await page.locator('li.border-b h2 > button[title="Edit"]').click(); await page.locator('li.border-b h2 > a[title="See more about this person"]').click();
await page.locator('h2 > button[title="Edit"]').click();
await expect(page.locator('div.dialog-overlay > div.dialog').filter({ hasText: 'Edit Name' })).toBeVisible(); await expect(page.locator('div.dialog-overlay > div.dialog').filter({ hasText: 'Edit Name' })).toBeVisible();
await page.getByPlaceholder('Name', { exact: true }).fill(contactName); await page.getByPlaceholder('Name', { exact: true }).fill(contactName);
await page.locator('.dialog > .flex > button').first().click(); await page.locator('.dialog > .flex > button').first().click();

Loading…
Cancel
Save