From 10bb79f695be626ee50f76b605bb1ec8a0c93cba Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 28 Nov 2024 11:26:51 -0700 Subject: [PATCH] refactor project screen: add action to record a give from it, and add checks to give confirmation buttons --- src/components/GiftedDialog.vue | 10 +- src/libs/util.ts | 153 +++++++++++++- src/views/ClaimView.vue | 56 +++--- src/views/ConfirmGiftView.vue | 53 +++-- src/views/ContactGiftingView.vue | 2 +- src/views/GiftedDetailsView.vue | 2 +- src/views/OfferDetailsView.vue | 2 +- src/views/ProjectViewView.vue | 330 +++++++++++++++++++++---------- 8 files changed, 428 insertions(+), 180 deletions(-) diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index 91352cb..4000c82 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -47,7 +47,8 @@ giverDid: giver?.did, giverName: giver?.name, offerId, - fulfillsProjectId: projectId, + fulfillsProjectId: toProjectId, + providerProjectId: fromProjectId, recipientDid: receiver?.did, recipientName: receiver?.name, unitCode, @@ -98,7 +99,8 @@ import { Contact } from "@/db/tables/contacts"; export default class GiftedDialog extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; - @Prop projectId = ""; + @Prop fromProjectId = ""; + @Prop toProjectId = ""; activeDid = ""; allContacts: Array = []; @@ -294,9 +296,11 @@ export default class GiftedDialog extends Vue { description, amount, unitCode, - this.projectId, + this.toProjectId, this.offerId, this.isTrade, + undefined, + this.fromProjectId, ); if ( diff --git a/src/libs/util.ts b/src/libs/util.ts index 2b0d855..c11f046 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -5,7 +5,7 @@ import { Buffer } from "buffer"; import * as R from "ramda"; import { useClipboard } from "@vueuse/core"; -import { DEFAULT_PUSH_SERVER } from "@/constants/app"; +import { DEFAULT_PUSH_SERVER, NotificationIface } from "@/constants/app"; import { accountsDB, retrieveSettingsForActiveAccount, @@ -21,6 +21,7 @@ import { containsHiddenDid, GenericCredWrapper, GenericVerifiableCredential, + GiveSummaryRecord, OfferVerifiableCredential, } from "@/libs/endorserServer"; import { KeyMeta } from "@/libs/crypto/vc"; @@ -101,10 +102,14 @@ export const isGlobalUri = (uri: string) => { return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/)); }; +export const isGiveClaimType = (claimType?: string) => { + return claimType === "GiveAction"; +}; + export const isGiveAction = ( veriClaim: GenericCredWrapper, ) => { - return veriClaim.claimType === "GiveAction"; + return isGiveClaimType(veriClaim.claimType); }; export const nameForDid = ( @@ -136,16 +141,75 @@ export const doCopyTwoSecRedo = (text: string, fn: () => void) => { .then(() => setTimeout(fn, 2000)); }; +export interface ConfirmerData { + confirmerIdList: string[]; + confsVisibleToIdList: string[]; + numConfsNotVisible: number; +} + +/** + * @return only confirmers, excluding the issuer and hidden DIDs + */ +export async function retrieveConfirmerIdList( + apiServer: string, + claimId: string, + claimIssuerId: string, + userDid: string, +): Promise { + const confirmUrl = + apiServer + + "/api/report/issuersWhoClaimedOrConfirmed?claimId=" + + encodeURIComponent(serverUtil.stripEndorserPrefix(claimId)); + const confirmHeaders = await serverUtil.getHeaders(userDid); + const response = await axios.get(confirmUrl, { + headers: confirmHeaders, + }); + if (response.status === 200) { + const resultList1 = response.data.result || []; + //const publicUrls = resultList.publicUrls || []; + delete resultList1.publicUrls; + // exclude hidden DIDs + const resultList2 = R.reject(serverUtil.isHiddenDid, resultList1); + // exclude the issuer + const resultList3 = R.reject( + (did: string) => did === claimIssuerId, + resultList2, + ); + const confirmerIdList = resultList3; + let numConfsNotVisible = resultList1.length - resultList2.length; + if (resultList3.length === resultList2.length) { + // the issuer was not in the "visible" list so they must be hidden + // so subtract them from the non-visible confirmers count + numConfsNotVisible = numConfsNotVisible - 1; + } + const confsVisibleToIdList = response.data.result.resultVisibleToDids || []; + const result: ConfirmerData = { + confirmerIdList, + confsVisibleToIdList, + numConfsNotVisible, + }; + return result; + } else { + console.error( + "Bad response status of", + response.status, + "for confirmers:", + response, + ); + return undefined; + } +} + /** * @returns true if the user can confirm the claim * @param veriClaim is expected to have fields: claim, claimType, and issuer */ -export const isGiveRecordTheUserCanConfirm = ( +export function isGiveRecordTheUserCanConfirm( isRegistered: boolean, veriClaim: GenericCredWrapper, activeDid: string, confirmerIdList: string[] = [], -) => { +): boolean { return ( isRegistered && isGiveAction(veriClaim) && @@ -153,7 +217,78 @@ export const isGiveRecordTheUserCanConfirm = ( veriClaim.issuer !== activeDid && !containsHiddenDid(veriClaim.claim) ); -}; +} + +export function notifyWhyCannotConfirm( + notifyFun: (notification: NotificationIface, timeout: number) => void, + isRegistered: boolean, + claimType: string | undefined, + giveDetails: GiveSummaryRecord | undefined, + activeDid: string, + confirmerIdList: string[] = [], +) { + if (!isRegistered) { + notifyFun( + { + group: "alert", + type: "info", + title: "Not Registered", + text: "Someone needs to register you before you can confirm.", + }, + 3000, + ); + } else if (!isGiveClaimType(claimType)) { + notifyFun( + { + group: "alert", + type: "info", + title: "Not A Give", + text: "This is not a giving action to confirm.", + }, + 3000, + ); + } else if (confirmerIdList.includes(activeDid)) { + notifyFun( + { + group: "alert", + type: "info", + title: "Already Confirmed", + text: "You already confirmed this claim.", + }, + 3000, + ); + } else if (giveDetails?.issuerDid == activeDid) { + notifyFun( + { + group: "alert", + type: "info", + title: "Cannot Confirm", + text: "You cannot confirm this because you issued this claim.", + }, + 3000, + ); + } else if (serverUtil.containsHiddenDid(giveDetails?.fullClaim)) { + notifyFun( + { + group: "alert", + type: "info", + title: "Cannot Confirm", + text: "You cannot confirm this because some people are hidden.", + }, + 3000, + ); + } else { + notifyFun( + { + group: "alert", + type: "info", + title: "Cannot Confirm", + text: "You cannot confirm this claim. There are no other details -- we can help more if you contact us and send us screenshots.", + }, + 3000, + ); + } +} export async function blobToBase64(blob: Blob): Promise { return new Promise((resolve, reject) => { @@ -191,9 +326,9 @@ export function base64ToBlob(base64DataUrl: string, sliceSize = 512) { * @returns the DID of the person who offered, or undefined if hidden * @param veriClaim is expected to have fields: claim and issuer */ -export const offerGiverDid: ( - arg0: GenericCredWrapper, -) => string | undefined = (veriClaim) => { +export function offerGiverDid( + veriClaim: GenericCredWrapper, +): string | undefined { let giver; if ( veriClaim.claim.offeredBy?.identifier && @@ -204,7 +339,7 @@ export const offerGiverDid: ( giver = veriClaim.issuer; } return giver; -}; +} /** * @returns true if the user can fulfill the offer diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 0e48473..bb615d6 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -24,9 +24,11 @@ {{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }} Idea -

{{ name }}

+

+ {{ name }} + +

@@ -104,15 +114,6 @@ - -
@@ -159,31 +160,14 @@
-
-
- -
-
- -

Record a contribution from:

    -
  • +
  • -
  • +
  • +
  • + + ... or someone else... + +
- - - - Show More Contacts… - - - +
+
-

Offered To This Idea

+
+
+ +
+
+ + +

Offered To This Idea

(None yet. Wanna @@ -300,15 +298,27 @@
+ +
-

Given To This Idea

+
+
+ +
+
+ +

Given To This Idea

(None yet. If you've seen something, say something by clicking a contact above.)
-
-
- @@ -468,6 +521,7 @@ export default class ProjectViewView extends Vue { allMyDids: Array = []; allContacts: Array = []; apiServer = ""; + checkingConfirmationForJwtId = ""; description = ""; expanded = false; fulfilledByThis: PlanSummaryRecord | null = null; @@ -486,6 +540,7 @@ export default class ProjectViewView extends Vue { offersToThis: Array = []; offersHitLimit = false; projectId = ""; // handle ID + recentlyCheckedAndUnconfirmableJwts = []; showDidCopy = false; startTime = ""; truncatedDesc = ""; @@ -847,12 +902,21 @@ export default class ProjectViewView extends Vue { ); } - openGiftDialog(contact?: libsUtil.GiverReceiverInputInfo) { - (this.$refs.customGiveDialog as GiftedDialog).open( + openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) { + (this.$refs.giveDialogToThis as GiftedDialog).open( contact, undefined, undefined, - "Given by " + (contact?.name || "someone not named"), + (contact?.name || "Someone not named") + ` gave to this project`, + ); + } + + openGiftDialogFromProject() { + (this.$refs.giveDialogFromThis as GiftedDialog).open( + undefined, + { did: this.activeDid, name: "You" }, + undefined, + `This project gave to you`, ); } @@ -896,7 +960,7 @@ export default class ProjectViewView extends Vue { const giver: libsUtil.GiverReceiverInputInfo = { did: libsUtil.offerGiverDid(offerRecord), }; - (this.$refs.customGiveDialog as GiftedDialog).open( + (this.$refs.giveDialogToThis as GiftedDialog).open( giver, undefined, offer.handleId, @@ -932,20 +996,70 @@ export default class ProjectViewView extends Vue { } } - checkIsConfirmable(give: GiveSummaryRecord) { + /** + * @param confirmerIdList optional list of DIDs who confirmed; if missing, doesn't do a full server check + */ + checkIsConfirmable(give: GiveSummaryRecord, confirmerIdList?: string[]) { const giveDetails: GenericCredWrapper = { ...BLANK_GENERIC_SERVER_RECORD, claim: give.fullClaim, claimType: "GiveAction", - issuer: give.agentDid, + issuer: give.issuerDid, }; return libsUtil.isGiveRecordTheUserCanConfirm( this.isRegistered, giveDetails, this.activeDid, + confirmerIdList, + ); + } + + shallowNotifyWhyCannotConfirm(give: GiveSummaryRecord) { + const confirmerIds = this.recentlyCheckedAndUnconfirmableJwts.includes( + give.jwtId, + ) + ? [this.activeDid] + : []; + libsUtil.notifyWhyCannotConfirm( + this.$notify, + this.isRegistered, + "GiveAction", + give, + this.activeDid, + confirmerIds, ); } + async deepCheckConfirmable(give: GiveSummaryRecord) { + this.checkingConfirmationForJwtId = give.jwtId; + const confirmerInfo: libsUtil.ConfirmerData | undefined = + await libsUtil.retrieveConfirmerIdList( + this.apiServer, + give.jwtId, + give.issuerDid, + this.activeDid, + ); + if ( + this.checkIsConfirmable(give, confirmerInfo?.confirmerIdList as string[]) + ) { + this.confirmConfirmClaim(give); + } else { + this.recentlyCheckedAndUnconfirmableJwts = [ + ...this.recentlyCheckedAndUnconfirmableJwts, + give.jwtId, + ]; + libsUtil.notifyWhyCannotConfirm( + this.$notify, + this.isRegistered, + "GiveAction", + give, + this.activeDid, + confirmerInfo?.confirmerIdList as string[], + ); + } + this.checkingConfirmationForJwtId = ""; + } + confirmConfirmClaim(give: GiveSummaryRecord) { this.$notify( { @@ -994,6 +1108,10 @@ export default class ProjectViewView extends Vue { }, 5000, ); + this.recentlyCheckedAndUnconfirmableJwts = [ + ...this.recentlyCheckedAndUnconfirmableJwts, + give.jwtId, + ]; } else { console.error("Got error submitting the confirmation:", result); const message =