diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d14a8589..669d61aca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Nothing +## [0.3.9] - 2024.04-21 +### Added +- Offer on contacts page +### Changed in DB or environment +- Nothing + + ## [0.3.8] - 2024.04-20 - 15c026c80ce03a26cae3ff80b0888934c101c7e2 ### Added - Profile image for user diff --git a/src/components/EntityIcon.vue b/src/components/EntityIcon.vue index ce6f9cf23..949f5a85a 100644 --- a/src/components/EntityIcon.vue +++ b/src/components/EntityIcon.vue @@ -23,6 +23,10 @@ export default class EntityIcon extends Vue { if (!identifier) { return ``; } + // https://api.dicebear.com/8.x/avataaars/svg?seed= + // ... does not render things with the same seed as this library. + // "did:ethr:0x222BB77E6Ff3774d34c751f3c1260866357B677b" yields a girl with flowers in her hair and a lightning earring + // ... which looks similar to '' at the dicebear site but which is different. const options: StyleOptions = { seed: (identifier as string) || "", size: this.iconSize, diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index ec5a77bb1..cb4d9f4c2 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -2,12 +2,12 @@

- {{ message }} {{ giver?.name || "somebody not named" }} + {{ customTitle || message + " " + giver?.name || "somebody not named" }}

@@ -42,12 +42,15 @@ name: 'gifted-details', query: { amountInput, + customTitle, description, giverDid: giver?.did, giverName: giver?.name, message, offerId, projectId, + recipientDid: receiver?.did, + recipientName: receiver?.name, unitCode, }, }" @@ -90,7 +93,7 @@ import { NotificationIface } from "@/constants/app"; import { createAndSubmitGive, didInfo, - GiverInputInfo, + GiverReceiverInputInfo, } from "@/libs/endorserServer"; import * as libsUtil from "@/libs/util"; import { accountsDB, db } from "@/db/index"; @@ -103,7 +106,6 @@ export default class GiftedDialog extends Vue { @Prop message = ""; @Prop projectId = ""; - @Prop showGivenToUser = false; activeDid = ""; allContacts: Array = []; @@ -111,22 +113,32 @@ export default class GiftedDialog extends Vue { apiServer = ""; amountInput = "0"; + callbackOnSuccess?: (amount: number) => void = () => {}; + customTitle?: string; description = ""; - givenToUser = false; - giver?: GiverInputInfo; // undefined means no identified giver agent + giver?: GiverReceiverInputInfo; // undefined means no identified giver agent isTrade = false; offerId = ""; + receiver?: GiverReceiverInputInfo; unitCode = "HUR"; visible = false; libsUtil = libsUtil; - async open(giver?: GiverInputInfo, offerId?: string) { + async open( + giver?: GiverReceiverInputInfo, + receiver?: GiverReceiverInputInfo, + offerId?: string, + customTitle?: string, + callbackOnSuccess?: (amount: number) => void, + ) { + this.customTitle = customTitle; this.description = ""; - this.giver = giver || {}; + this.giver = giver; + this.receiver = receiver; // if we show "given to user" selection, default checkbox to true - this.givenToUser = this.showGivenToUser; this.amountInput = "0"; + this.callbackOnSuccess = callbackOnSuccess; this.offerId = offerId || ""; try { @@ -141,7 +153,7 @@ export default class GiftedDialog extends Vue { const allAccounts = await accountsDB.accounts.toArray(); this.allMyDids = allAccounts.map((acc) => acc.did); - if (!this.giver.name) { + if (this.giver && !this.giver.name) { this.giver.name = didInfo( this.giver.did, this.activeDid, @@ -196,7 +208,6 @@ export default class GiftedDialog extends Vue { eraseValues() { this.description = ""; this.giver = undefined; - this.givenToUser = this.showGivenToUser; this.amountInput = "0"; this.unitCode = "HUR"; } @@ -254,6 +265,7 @@ export default class GiftedDialog extends Vue { // this is asynchronous, but we don't need to wait for it to complete await this.recordGive( (this.giver?.did as string) || null, + (this.receiver?.did as string) || null, this.description, parseFloat(this.amountInput), this.unitCode, @@ -265,14 +277,16 @@ export default class GiftedDialog extends Vue { /** * * @param giverDid may be null + * @param recipientDid may be null * @param description may be an empty string - * @param amountInput may be 0 + * @param amount may be 0 * @param unitCode may be omitted, defaults to "HUR" */ - public async recordGive( + async recordGive( giverDid: string | null, + recipientDid: string | null, description: string, - amountInput: number, + amount: number, unitCode: string = "HUR", ) { try { @@ -282,9 +296,9 @@ export default class GiftedDialog extends Vue { this.apiServer, identity, giverDid, - this.givenToUser ? this.activeDid : undefined, + this.receiver?.did as string, description, - amountInput, + amount, unitCode, this.projectId, this.offerId, @@ -316,6 +330,9 @@ export default class GiftedDialog extends Vue { }, 7000, ); + if (this.callbackOnSuccess) { + this.callbackOnSuccess(amount); + } } // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue index 980a61f11..e04888894 100644 --- a/src/components/OfferDialog.vue +++ b/src/components/OfferDialog.vue @@ -43,7 +43,7 @@
@@ -69,6 +69,7 @@ diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 86010b2ad..f233d7766 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -188,7 +188,6 @@ export default class ContactQRScanShow extends Vue { if (url) { try { const fullData = getContactPayloadFromJwtUrl(url); - console.log("fullData", fullData); localStorage.setItem("contactEndorserUrl", url); this.$router.push({ name: "contacts" }); } catch (e) { diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index aecd23393..511d22de4 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -43,6 +43,7 @@
+ + In the following, only the most recent hours are included. To see more, + click + + +
-
- (Only most recent hours included. To see more, click - - - - )
@@ -189,10 +189,11 @@ > @@ -237,7 +241,7 @@ name: 'contact-amounts', query: { contactDid: contact.did }, }" - class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md" + class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-slate-400" title="See more given activity" > @@ -249,6 +253,7 @@

There are no contacts.

+
@@ -313,8 +318,8 @@ import { import { CONTACT_CSV_HEADER, CONTACT_URL_PREFIX, + GiverReceiverInputInfo, GiveSummaryRecord, - GiveVerifiableCredential, isDid, RegisterVerifiableCredential, SERVICE_ID, @@ -322,13 +327,14 @@ import { import * as libsUtil from "@/libs/util"; import QuickNav from "@/components/QuickNav.vue"; import EntityIcon from "@/components/EntityIcon.vue"; +import GiftedDialog from "@/components/GiftedDialog.vue"; import OfferDialog from "@/components/OfferDialog.vue"; import { Account } from "@/db/tables/accounts"; import { Buffer } from "buffer/"; @Component({ - components: { EntityIcon, OfferDialog, QuickNav }, + components: { GiftedDialog, EntityIcon, OfferDialog, QuickNav }, }) export default class ContactsView extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; @@ -352,8 +358,6 @@ export default class ContactsView extends Vue { givenToMeConfirmed: Record = {}; // { "did:...": amount } entry for each contact givenToMeUnconfirmed: Record = {}; - hourDescriptionInput = ""; - hourInput = "0"; isRegistered = false; showDidCopy = false; showGiveNumbers = false; @@ -1041,27 +1045,33 @@ export default class ContactsView extends Vue { } private nameForDid(contacts: Array, 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 || (capitalize ? "T" : "t") + "his unnamed user"; + return ( + (contact?.name as string) || (capitalize ? "T" : "t") + "his unnamed user" + ); } - async onClickAddGive(fromDid: string, toDid: string): Promise { - const identity = await this.getIdentity(this.activeDid); - + private showGiftedDialog(giverDid: string, recipientDid: string) { // if they have unconfirmed amounts, ask to confirm those first - if (toDid == identity?.did && this.givenToMeUnconfirmed[fromDid] > 0) { - const isare = this.givenToMeUnconfirmed[fromDid] == 1 ? "is" : "are"; - const hours = this.givenToMeUnconfirmed[fromDid] == 1 ? "hour" : "hours"; + if ( + recipientDid == this.activeDid && + this.givenToMeUnconfirmed[giverDid] > 0 + ) { + const isAre = this.givenToMeUnconfirmed[giverDid] == 1 ? "is" : "are"; + const hours = this.givenToMeUnconfirmed[giverDid] == 1 ? "hour" : "hours"; if ( confirm( "There " + - isare + + isAre + " " + - this.givenToMeUnconfirmed[fromDid] + + this.givenToMeUnconfirmed[giverDid] + " unconfirmed " + hours + " from them." + @@ -1070,178 +1080,58 @@ export default class ContactsView extends Vue { ) { this.$router.push({ name: "contact-amounts", - query: { contactDid: fromDid }, + query: { contactDid: giverDid }, }); return; } } - if (!libsUtil.isNumeric(this.hourInput)) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Input Error", - text: "This is not a valid number of hours: " + this.hourInput, - }, - 3000, - ); - } else if (parseFloat(this.hourInput) == 0 && !this.hourDescriptionInput) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Input Error", - text: "Giving no hours or description does nothing.", - }, - 3000, - ); - } else if (!identity) { - this.$notify( - { - group: "alert", - type: "danger", - title: "Status Error", - text: "No identifier is available.", - }, - 3000, - ); + + let giver: GiverReceiverInputInfo, receiver: GiverReceiverInputInfo; + if (giverDid) { + giver = { + did: giverDid, + name: this.nameForDid(this.contacts, giverDid), + }; + } + if (recipientDid) { + receiver = { + did: recipientDid, + name: this.nameForDid(this.contacts, recipientDid), + }; + } + + let callback: (amount: number) => void; + let customTitle = "Given"; + // choose whether to open dialog to user or from user + if (giverDid == this.activeDid) { + callback = (amount: number) => { + const newList = R.clone(this.givenByMeUnconfirmed); + newList[recipientDid] = (newList[recipientDid] || 0) + amount; + this.givenByMeUnconfirmed = newList; + }; + customTitle = "To " + receiver.name; } else { - // ask to confirm amount - let toFrom; - if (fromDid == identity?.did) { - toFrom = "from you to " + this.nameForDid(this.contacts, toDid); - } else { - toFrom = "from " + this.nameForDid(this.contacts, fromDid) + " to you"; - } - let description; - if (this.hourDescriptionInput) { - description = " with description '" + this.hourDescriptionInput + "'"; - } else { - description = " with no description"; - } - if ( - confirm( - "Are you sure you want to record " + - this.hourInput + - " hour" + - (this.hourInput == "1" ? "" : "s") + - " " + - toFrom + - description + - "?", - ) - ) { - this.createAndSubmitContactGive( - identity, - fromDid, - toDid, - parseFloat(this.hourInput), - this.hourDescriptionInput, - ); - } + // must be (recipientDid == this.activeDid) + callback = (amount: number) => { + const newList = R.clone(this.givenToMeUnconfirmed); + newList[giverDid] = (newList[giverDid] || 0) + amount; + this.givenToMeUnconfirmed = newList; + }; + customTitle = "From " + giver.name; } + (this.$refs.customGivenDialog as GiftedDialog).open( + giver, + receiver, + undefined as string, + customTitle, + callback, + ); } openOfferDialog(recipientDid: string) { (this.$refs.customOfferDialog as OfferDialog).open(recipientDid); } - // similar function is in endorserServer.ts - private async createAndSubmitContactGive( - identity: IIdentifier, - fromDid: string, - toDid: string, - amount: number, - description: string, - ): Promise { - // Make a claim - const vcClaim: GiveVerifiableCredential = { - "@context": "https://schema.org", - "@type": "GiveAction", - agent: { identifier: fromDid }, - object: { amountOfThisGood: amount, unitCode: "HUR" }, - recipient: { identifier: toDid }, - }; - if (description) { - vcClaim.description = description; - } - // Make a payload for the claim - const vcPayload = { - vc: { - "@context": ["https://www.w3.org/2018/credentials/v1"], - type: ["VerifiableCredential"], - credentialSubject: vcClaim, - }, - }; - // Create a signature using private key of identity - if (identity.keys[0].privateKeyHex !== null) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const privateKeyHex: string = identity.keys[0].privateKeyHex!; - const signer = await SimpleSigner(privateKeyHex); - const alg = undefined; - // Create a JWT for the request - const vcJwt: string = await didJwt.createJWT(vcPayload, { - alg: alg, - issuer: identity.did, - signer: signer, - }); - - // Make the xhr request payload - - const payload = JSON.stringify({ jwtEncoded: vcJwt }); - const url = this.apiServer + "/api/v2/claim"; - const headers = await this.getHeaders(identity); - - try { - const resp = await this.axios.post(url, payload, { headers }); - if (resp.data?.success?.handleId) { - this.$notify( - { - group: "alert", - type: "success", - title: "Done", - text: "Successfully logged time to the server.", - }, - 5000, - ); - - if (fromDid === identity.did) { - const newList = R.clone(this.givenByMeUnconfirmed); - newList[toDid] = (newList[toDid] || 0) + amount; - this.givenByMeUnconfirmed = newList; - } else { - const newList = R.clone(this.givenToMeConfirmed); - newList[fromDid] = (newList[fromDid] || 0) + amount; - this.givenToMeConfirmed = newList; - } - } - } catch (error) { - console.error("Error in createAndSubmitContactGive: ", error); - let userMessage = "There was an error. See logs for more info."; - const serverError = error as AxiosError; - if (serverError) { - 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: "Error Sending Give", - text: userMessage, - }, - 5000, - ); - } - } - } - private async onClickCancelName() { this.contactEdit = null; this.contactNewName = ""; diff --git a/src/views/GiftedDetails.vue b/src/views/GiftedDetails.vue index 5b911aefe..bf31a79dd 100644 --- a/src/views/GiftedDetails.vue +++ b/src/views/GiftedDetails.vue @@ -18,8 +18,12 @@

What Was Given

- {{ message }} {{ giverName || "somebody not named" }} + {{ customTitle || message + " " + giverName || "somebody not named" }}

+
+ From {{ giverName || "somebody not named" }} + to {{ recipientName || "somebody not named" }} +