Browse Source

for scan on QR code screen, import and keep on that screen

kb/add-usage-guide
Trent Larson 7 months ago
parent
commit
bd148e88a3
  1. 47
      src/libs/endorserServer.ts
  2. 2
      src/libs/util.ts
  3. 13
      src/views/AccountViewView.vue
  4. 155
      src/views/ContactQRScanShowView.vue
  5. 124
      src/views/ContactsView.vue

47
src/libs/endorserServer.ts

@ -6,6 +6,8 @@ import { IIdentifier } from "@veramo/core";
import { Contact } from "@/db/tables/contacts"; import { Contact } from "@/db/tables/contacts";
import { accessToken, SimpleSigner } from "@/libs/crypto"; import { accessToken, SimpleSigner } from "@/libs/crypto";
import { NonsensitiveDexie } from "@/db/index";
import { getIdentity } from "@/libs/util";
export const SCHEMA_ORG_CONTEXT = "https://schema.org"; export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims // the object in RegisterAction claims
@ -706,6 +708,12 @@ export async function createAndSubmitClaim(
} }
} }
/**
* An AcceptAction is when someone accepts some contract or pledge.
*
* @param claim has properties '@context' & '@type'
* @return true if the claim is a schema.org AcceptAction
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isAccept = (claim: Record<string, any>) => { export const isAccept = (claim: Record<string, any>) => {
return ( return (
@ -902,3 +910,42 @@ export const bvcMeetingJoinClaim = (did: string, startTime: string) => {
}, },
}; };
}; };
export async function setVisibilityUtil(
activeDid: string,
apiServer: string,
axios: Axios,
db: NonsensitiveDexie,
contact: Contact,
visibility: boolean,
) {
if (!activeDid) {
return { error: "Cannot set visibility without an identifier." };
}
const url =
apiServer + "/api/report/" + (visibility ? "canSeeMe" : "cannotSeeMe");
const identity = await getIdentity(activeDid);
const headers = await getHeaders(identity);
const payload = JSON.stringify({ did: contact.did });
try {
const resp = await axios.post(url, payload, { headers });
if (resp.status === 200) {
contact.seesMe = visibility;
db.contacts.update(contact.did, { seesMe: visibility });
return { success: true };
} else {
console.error(
"Got some bad server response when setting visibility: ",
resp.status,
resp,
);
const message =
resp.data.error?.message || "Got some error setting visibility.";
return { error: message };
}
} catch (err) {
console.error("Got some error when setting visibility:", err);
return { error: "Check connectivity and try again." };
}
}

2
src/libs/util.ts

@ -201,7 +201,7 @@ export const getIdentity = async (activeDid: string): Promise<IIdentifier> => {
if (!identity) { if (!identity) {
throw new Error( throw new Error(
`Attempted to load Offer records for DID ${activeDid} but no identifier was found`, `Attempted to load identity ${activeDid} but no identifier was found`,
); );
} }
return identity; return identity;

13
src/views/AccountViewView.vue

@ -55,7 +55,7 @@
</p> </p>
<router-link <router-link
:to="{ name: 'start' }" :to="{ name: 'start' }"
class="inline-block text-md bg-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
> >
Create An Identifier Create An Identifier
</router-link> </router-link>
@ -71,10 +71,13 @@
</router-link> </router-link>
</h2> </h2>
</div> </div>
<span v-else> <span
v-else
class="block w-full text-center text-md bg-amber-200 text-blue-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
>
<router-link <router-link
:to="{ name: 'new-edit-account' }" :to="{ name: 'new-edit-account' }"
class="block w-full text-center text-md bg-amber-200 text-blue-500 uppercase border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2" class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
> >
Set Your Name Set Your Name
</router-link> </router-link>
@ -163,7 +166,7 @@
</p> </p>
<router-link <router-link
:to="{ name: 'contact-qr' }" :to="{ name: 'contact-qr' }"
class="inline-block text-md uppercase bg-gradient-to-b from-amber-400 to-amber-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
> >
Share Your Info Share Your Info
</router-link> </router-link>
@ -1168,7 +1171,7 @@ export default class AccountViewView extends Vue {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "warning", type: "danger",
title: "Update Error", title: "Update Error",
text: "Unable to update your settings. Check claim limits again.", text: "Unable to update your settings. Check claim limits again.",
}, },

155
src/views/ContactQRScanShowView.vue

@ -79,16 +79,25 @@ import { Component, Vue } from "vue-facing-decorator";
import { QrcodeStream } from "vue-qrcode-reader"; import { QrcodeStream } from "vue-qrcode-reader";
import { useClipboard } from "@vueuse/core"; import { useClipboard } from "@vueuse/core";
import QuickNav from "@/components/QuickNav.vue";
import { NotificationIface } from "@/constants/app"; import { NotificationIface } from "@/constants/app";
import { accountsDB, db } from "@/db/index"; import { accountsDB, db } from "@/db/index";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import { deriveAddress, nextDerivationPath, SimpleSigner } from "@/libs/crypto";
import QuickNav from "@/components/QuickNav.vue";
import { Account } from "@/db/tables/accounts"; import { Account } from "@/db/tables/accounts";
import { Contact } from "@/db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
import {
deriveAddress,
getContactPayloadFromJwtUrl,
nextDerivationPath,
SimpleSigner,
} from "@/libs/crypto";
import { import {
CONTACT_URL_PREFIX, CONTACT_URL_PREFIX,
ENDORSER_JWT_URL_LOCATION, ENDORSER_JWT_URL_LOCATION,
isDid,
setVisibilityUtil,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
@Component({ @Component({
@ -106,29 +115,12 @@ export default class ContactQRScanShow extends Vue {
givenName = ""; givenName = "";
qrValue = ""; qrValue = "";
public async getIdentity(activeDid: string) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account: Account | undefined = R.find(
(acc) => acc.did === activeDid,
accounts,
);
const identity = JSON.parse(account?.identity || "null");
if (!identity) {
throw new Error(
"Attempted to show contact info with no identifier available.",
);
}
return identity;
}
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);
this.activeDid = settings?.activeDid || ""; this.activeDid = (settings?.activeDid as string) || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = (settings?.apiServer as string) || "";
this.givenName = settings?.firstName || ""; this.givenName = (settings?.firstName as string) || "";
await accountsDB.open(); await accountsDB.open();
const accounts = await accountsDB.accounts.toArray(); const accounts = await accountsDB.accounts.toArray();
@ -172,25 +164,109 @@ export default class ContactQRScanShow extends Vue {
} }
} }
danger(message: string, title: string = "Error", timeout = 5000) {
this.$notify(
{
group: "alert",
type: "danger",
title: title,
text: message,
},
timeout,
);
}
public async getIdentity(activeDid: string) {
await accountsDB.open();
const accounts = await accountsDB.accounts.toArray();
const account: Account | undefined = R.find(
(acc) => acc.did === activeDid,
accounts,
);
const identity = JSON.parse((account?.identity as string) || "null");
if (!identity) {
throw new Error(
"Attempted to show contact info with no identifier available.",
);
}
return identity;
}
/** /**
* *
* @param content is the result of a QR scan, an array with one item with a rawValue property * @param content is the result of a QR scan, an array with one item with a rawValue property
*/ */
// Unfortunately, there are not typescript definitions for the qrcode-stream component yet. // Unfortunately, there are not typescript definitions for the qrcode-stream component yet.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanDetect(content: any) { async onScanDetect(content: any) {
const url = content[0]?.rawValue; const url = content[0]?.rawValue;
if (url) { if (url) {
let newContact: Contact;
try {
const payload = getContactPayloadFromJwtUrl(url);
if (!payload) {
this.$notify(
{
group: "alert",
type: "danger",
title: "No Contact Info",
text: "The contact info could not be parsed.",
},
3000,
);
return;
}
newContact = {
did: payload.iss as string,
name: payload.own.name,
nextPubKeyHashB64: payload.own.nextPublicEncKeyHash,
profileImageUrl: payload.own.profileImageUrl,
publicKeyBase64: payload.own.publicEncKey,
};
if (!newContact.did) {
this.danger("There is no DID.", "Incomplete Contact");
return;
}
if (!isDid(newContact.did)) {
this.danger("The DID must begin with 'did:'", "Invalid DID");
return;
}
} catch (e) {
console.error("Error parsing QR info:", e);
this.danger("Could not parse the QR info.", "Read Error");
return;
}
try { try {
localStorage.setItem("contactEndorserUrl", url); await db.open();
this.$router.push({ name: "contacts" }); await db.contacts.add(newContact);
let addedMessage;
if (this.activeDid) {
await this.setVisibility(newContact, true);
addedMessage =
"They were added, and your activity is visible to them.";
} else {
addedMessage = "They were added.";
}
this.$notify(
{
group: "alert",
type: "success",
title: "Contact Added",
text: addedMessage,
},
3000,
);
} catch (e) { } catch (e) {
console.error("Error saving contact info:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "warning", type: "danger",
title: "Invalid Contact QR Code", title: "Contact Error",
text: "The QR code isn't in the right format.", text: "Could not save contact info. Check if it already exists.",
}, },
5000, 5000,
); );
@ -199,7 +275,7 @@ export default class ContactQRScanShow extends Vue {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "warning", type: "danger",
title: "Invalid Contact QR Code", title: "Invalid Contact QR Code",
text: "No QR code detected with contact information.", text: "No QR code detected with contact information.",
}, },
@ -208,13 +284,29 @@ export default class ContactQRScanShow extends Vue {
} }
} }
async setVisibility(contact: Contact, visibility: boolean) {
const result = await setVisibilityUtil(
this.activeDid,
this.apiServer,
this.axios,
db,
contact,
visibility,
);
if (result.error) {
this.danger(result.error as string, "Error Setting Visibility");
} else if (!result.success) {
console.error("Got strange result from setting visibility:", result);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
onScanError(error: any) { onScanError(error: any) {
console.error("Scan was invalid:", error); console.error("Scan was invalid:", error);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "warning", type: "danger",
title: "Invalid Scan", title: "Invalid Scan",
text: "The scan was invalid.", text: "The scan was invalid.",
}, },
@ -223,6 +315,7 @@ export default class ContactQRScanShow extends Vue {
} }
onCopyToClipboard() { onCopyToClipboard() {
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
useClipboard() useClipboard()
.copy(this.qrValue) .copy(this.qrValue)
.then(() => { .then(() => {

124
src/views/ContactsView.vue

@ -325,6 +325,7 @@ import {
isDid, isDid,
RegisterVerifiableCredential, RegisterVerifiableCredential,
SERVICE_ID, SERVICE_ID,
setVisibilityUtil,
} from "@/libs/endorserServer"; } from "@/libs/endorserServer";
import * as libsUtil from "@/libs/util"; import * as libsUtil from "@/libs/util";
import QuickNav from "@/components/QuickNav.vue"; import QuickNav from "@/components/QuickNav.vue";
@ -344,7 +345,6 @@ export default class ContactsView extends Vue {
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
contactEndorserUrl = localStorage.getItem("contactEndorserUrl") || "";
contactInput = ""; contactInput = "";
contactEdit: Contact | null = null; contactEdit: Contact | null = null;
contactNewName = ""; contactNewName = "";
@ -388,12 +388,18 @@ export default class ContactsView extends Vue {
this.contacts = baseContacts.sort((a, b) => this.contacts = baseContacts.sort((a, b) =>
(a.name || "").localeCompare(b.name || ""), (a.name || "").localeCompare(b.name || ""),
); );
if (this.contactEndorserUrl) {
await this.addContactFromScan(this.contactEndorserUrl);
localStorage.removeItem("contactEndorserUrl");
this.contactEndorserUrl = "";
} }
danger(message: string, title: string = "Error", timeout = 5000) {
this.$notify(
{
group: "alert",
type: "danger",
title: title,
text: message,
},
timeout,
);
} }
public async getIdentity(activeDid: string): Promise<IIdentifier> { public async getIdentity(activeDid: string): Promise<IIdentifier> {
@ -528,22 +534,14 @@ export default class ContactsView extends Vue {
title: "Load Error", title: "Load Error",
text: "Got an error loading your gives.", text: "Got an error loading your gives.",
}, },
-1, 5000,
); );
} }
} }
async onClickNewContact(): Promise<void> { async onClickNewContact(): Promise<void> {
if (!this.contactInput) { if (!this.contactInput) {
this.$notify( this.danger("There was no contact info to add.", "No Contact");
{
group: "alert",
type: "warning",
title: "No Contact",
text: "There was no contact info to add.",
},
3000,
);
return; return;
} }
@ -573,15 +571,7 @@ export default class ContactsView extends Vue {
3000, // keeping it up so that the "visibility" message is seen 3000, // keeping it up so that the "visibility" message is seen
); );
} catch (e) { } catch (e) {
this.$notify( this.danger("An error occurred. Some contacts may have been added.");
{
group: "alert",
type: "danger",
title: "Contacts Maybe Added",
text: "An error occurred. Some contacts may have been added.",
},
-1,
);
} }
// .orderBy("name") wouldn't retrieve any entries with a blank name // .orderBy("name") wouldn't retrieve any entries with a blank name
@ -697,30 +687,13 @@ export default class ContactsView extends Vue {
async addContact(newContact: Contact) { async addContact(newContact: Contact) {
if (!newContact.did) { if (!newContact.did) {
this.$notify( this.danger("Cannot add a contact without a DID.", "Incomplete Contact");
{
group: "alert",
type: "danger",
title: "Incomplete Contact",
text: "Cannot add a contact without a DID.",
},
5000,
);
return; return;
} }
if (!isDid(newContact.did)) { if (!isDid(newContact.did)) {
this.$notify( this.danger("The DID must begin with 'did:'", "Invalid DID");
{
group: "alert",
type: "danger",
title: "Invalid DID",
text: "The DID is not valid. It must begin with 'did:'",
},
5000,
);
return; return;
} }
newContact.seesMe = true; // since we will immediately set that on the server
return db.contacts return db.contacts
.add(newContact) .add(newContact)
.then(() => { .then(() => {
@ -737,6 +710,7 @@ export default class ContactsView extends Vue {
} else { } else {
addedMessage = "They were added."; addedMessage = "They were added.";
} }
this.contactInput = "";
if (this.isRegistered) { if (this.isRegistered) {
this.$notify( this.$notify(
{ {
@ -771,15 +745,7 @@ export default class ContactsView extends Vue {
message += message +=
" Check that the contact doesn't conflict with any you already have."; " Check that the contact doesn't conflict with any you already have.";
} }
this.$notify( this.danger(message, "Contact Not Added", -1);
{
group: "alert",
type: "danger",
title: "Contact Not Added",
text: message,
},
-1,
);
}); });
} }
@ -962,17 +928,15 @@ export default class ContactsView extends Vue {
visibility: boolean, visibility: boolean,
showSuccessAlert: boolean, showSuccessAlert: boolean,
) { ) {
const url = const result = await setVisibilityUtil(
this.apiServer + this.activeDid,
"/api/report/" + this.apiServer,
(visibility ? "canSeeMe" : "cannotSeeMe"); this.axios,
const identity = await this.getIdentity(this.activeDid); db,
const headers = await this.getHeaders(identity); contact,
const payload = JSON.stringify({ did: contact.did }); visibility,
);
try { if (result.success) {
const resp = await this.axios.post(url, payload, { headers });
if (resp.status === 200) {
if (showSuccessAlert) { if (showSuccessAlert) {
this.$notify( this.$notify(
{ {
@ -980,7 +944,7 @@ export default class ContactsView extends Vue {
type: "success", type: "success",
title: "Visibility Set", title: "Visibility Set",
text: text:
this.nameForDid(this.contacts, contact.did) + (contact.name || "That user") +
" can " + " can " +
(visibility ? "" : "not ") + (visibility ? "" : "not ") +
"see your activity.", "see your activity.",
@ -988,37 +952,18 @@ export default class ContactsView extends Vue {
3000, 3000,
); );
} }
contact.seesMe = visibility; } else if (result.error) {
db.contacts.update(contact.did, { seesMe: visibility });
} else {
console.error(
"Got some bad server response when setting visibility: ",
resp.status,
resp,
);
const message =
resp.data.error?.message || "Got some error setting visibility.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Setting Visibility",
text: message,
},
5000,
);
}
} catch (err) {
console.error("Got some error when setting visibility:", err);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error Setting Visibility", title: "Error Setting Visibility",
text: "Check connectivity and try again.", text: result.error as string,
}, },
5000, 5000,
); );
} else {
console.error("Got strange result from setting visibility:", result);
} }
} }
@ -1087,7 +1032,8 @@ export default class ContactsView extends Vue {
private nameForContact(contact?: Contact, capitalize?: boolean): string { private nameForContact(contact?: Contact, capitalize?: boolean): string {
return ( return (
(contact?.name as string) || (capitalize ? "T" : "t") + "his unnamed user" (contact?.name as string) ||
(capitalize ? "This" : "this") + " unnamed user"
); );
} }

Loading…
Cancel
Save