diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index b5c1e20..13fb7c5 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -448,8 +448,25 @@ import * as libsUtil from "../libs/util"; import { isGiveAction, retrieveAccountDids } from "../libs/util"; import TopMessage from "../components/TopMessage.vue"; +/** + * ConfirmGiftView Component + * + * Displays details about a gift claim and allows users to confirm it if eligible. + * Shows gift details including giver, recipient, amount, description, and confirmation status. + * Handles visibility of hidden DIDs and provides access to detailed claim information. + * + * Key features: + * - Gift confirmation workflow + * - Detailed gift information display + * - Confirmation status tracking + * - Hidden DID handling + * - Claim details expansion + */ @Component({ - components: { TopMessage, QuickNav }, + components: { + QuickNav, + TopMessage, + }, }) export default class ConfirmGiftView extends Vue { $notify!: (notification: NotificationIface, timeout?: number) => void; @@ -485,94 +502,92 @@ export default class ConfirmGiftView extends Vue { serverUtil = serverUtil; displayAmount = displayAmount; - resetThisValues() { - this.confirmerIdList = []; - this.confsVisibleErrorMessage = ""; - this.confsVisibleToIdList = []; - this.giveDetails = undefined; - this.isRegistered = false; - this.numConfsNotVisible = 0; - this.urlForNewGive = ""; - this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; - this.veriClaimDump = ""; - } - + /** + * Initializes the view with gift claim information + * + * Workflow: + * 1. Retrieves active account settings + * 2. Loads gift claim details from ID in URL + * 3. Processes claim information for display + * 4. Checks user's ability to confirm the gift + */ async mounted() { this.isLoading = true; + try { + await this.initializeSettings(); + await this.loadClaimFromUrl(); + } catch (error) { + console.error("Error in mounted:", error); + this.handleMountError(error); + } finally { + this.isLoading = false; + } + } + + /** + * Initializes component settings and user data + */ + private async initializeSettings() { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ""; this.apiServer = settings.apiServer || ""; this.allContacts = await db.contacts.toArray(); this.isRegistered = settings.isRegistered || false; - this.allMyDids = await retrieveAccountDids(); - - const pathParam = window.location.pathname.substring( - "/confirm-gift/".length, - ); - let claimId; - if (pathParam) { - claimId = decodeURIComponent(pathParam); - await this.loadClaim(claimId, this.activeDid); - } else { - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "No claim ID was provided.", - }, - 3000, - ); - } - + + // Check share capability // When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare // then use this truer check: navigator.canShare && navigator.canShare() this.canShare = !!navigator.share; - - this.isLoading = false; - } - - // insert a space before any capital letters except the initial letter - // (and capitalize initial letter, just in case) - capitalizeAndInsertSpacesBeforeCaps(text: string) { - return !text - ? "" - : text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1"); } - capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string) { - const word = this.capitalizeAndInsertSpacesBeforeCaps(text); - if (word) { - // if the word starts with a vowel, use "an" instead of "a" - const firstLetter = word[0].toLowerCase(); - const vowels = ["a", "e", "i", "o", "u"]; - const particle = vowels.includes(firstLetter) ? "an" : "a"; - return particle + " " + word; - } else { - return ""; + /** + * Loads and processes claim from URL parameters + */ + private async loadClaimFromUrl() { + const pathParam = window.location.pathname.substring("/confirm-gift/".length); + if (!pathParam) { + throw new Error("No claim ID was provided."); } + + const claimId = decodeURIComponent(pathParam); + await this.loadClaim(claimId, this.activeDid); } - totalConfirmers() { - return ( - this.numConfsNotVisible + - this.confirmerIdList.length + - this.confsVisibleToIdList.length + /** + * Handles errors during component mounting + */ + private handleMountError(error: unknown) { + this.$notify( + { + group: "alert", + type: "danger", + title: "Error", + text: error instanceof Error ? error.message : "No claim ID was provided.", + }, + 3000, ); } - // Isn't there a better way to make this available to the template? - didInfo(did: string | undefined) { - return serverUtil.didInfo( - did, - this.activeDid, - this.allMyDids, - this.allContacts, - ); + /** + * Loads claim details and associated give information + * + * @param claimId - ID of claim to load + * @param userDid - User's DID + */ + private async loadClaim(claimId: string, userDid: string) { + await this.fetchClaimDetails(claimId, userDid); + if (this.veriClaim.claimType === "GiveAction") { + await this.fetchGiveDetails(claimId, userDid); + await this.processGiveDetails(); + await this.fetchConfirmerInfo(claimId, userDid); + } } - async loadClaim(claimId: string, userDid: string) { + /** + * Fetches basic claim details from server + */ + private async fetchClaimDetails(claimId: string, userDid: string) { const urlPath = libsUtil.isGlobalUri(claimId) ? "/api/claim/byHandle/" : "/api/claim/"; @@ -581,9 +596,7 @@ export default class ConfirmGiftView extends Vue { try { const headers = await serverUtil.getHeaders(userDid); const resp = await this.axios.get(url, { headers }); - // resp.data is: - // - a Jwt from https://api.endorser.ch/api-docs/ - // - with a Give from https://endorser.ch/doc/html/transactions.html#id3 + if (resp.status === 200) { this.veriClaim = resp.data; this.veriClaimDump = yaml.dump(this.veriClaim); @@ -591,210 +604,161 @@ export default class ConfirmGiftView extends Vue { this.veriClaim, true, ); + this.issuerName = this.didInfo(this.veriClaim.issuer); } else { - // actually, axios typically throws an error so we never get here - console.error("Error getting claim:", resp); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "There was a problem retrieving that claim.", - }, - 3000, - ); - return; + throw new Error("Error getting claim: " + resp.status); } + } catch (error) { + console.error("Error getting claim:", error); + throw new Error("There was a problem retrieving that claim."); + } + } - // retrieve more details on Give, Offer, or Plan - if (this.veriClaim.claimType !== "GiveAction") { - // no need to go further... this page is for gifts - return; + /** + * Fetches detailed give information + */ + private async fetchGiveDetails(claimId: string, userDid: string) { + const giveUrl = `${this.apiServer}/api/v2/report/gives?handleId=${encodeURIComponent(claimId)}`; + + try { + const headers = await serverUtil.getHeaders(userDid); + const resp = await this.axios.get(giveUrl, { headers }); + + if (resp.status === 200) { + this.giveDetails = resp.data.data[0]; + } else { + throw new Error("Error getting detailed give info: " + resp.status); } + } catch (error) { + console.error("Error getting detailed give info:", error); + throw new Error("Something went wrong retrieving gift data."); + } + } - this.issuerName = this.didInfo(this.veriClaim.issuer); + /** + * Processes give details and builds URL for new give + */ + private async processGiveDetails() { + if (!this.giveDetails) return; - // use give record when possible since it may include edits - const giveUrl = - this.apiServer + - "/api/v2/report/gives?handleId=" + - encodeURIComponent(this.veriClaim.handleId as string); - const giveHeaders = await serverUtil.getHeaders(userDid); - const giveResp = await this.axios.get(giveUrl, { - headers: giveHeaders, - }); - // giveResp.data is a Give from https://api.endorser.ch/api-docs/ - if (giveResp.status === 200) { - this.giveDetails = giveResp.data.data[0]; - } else { - console.error("Error getting detailed give info:", giveResp); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "Something went wrong retrieving gift data.", - }, - 3000, - ); - return; - } + this.urlForNewGive = "/gifted-details?"; + this.addGiveDetailsToUrl(); + this.processParticipantInfo(); + this.processAdditionalDetails(); + } - // the logic already stops earlier if the claim doesn't exist but this helps with typechecking - if (!this.giveDetails) { - return; - } + /** + * Adds basic give details to URL + */ + private addGiveDetailsToUrl() { + if (this.giveDetails?.amount) { + this.urlForNewGive += `&amountInput=${encodeURIComponent(String(this.giveDetails.amount))}`; + } + if (this.giveDetails?.unit) { + this.urlForNewGive += `&unitCode=${encodeURIComponent(this.giveDetails.unit)}`; + } + if (this.giveDetails?.description) { + this.urlForNewGive += `&description=${encodeURIComponent(this.giveDetails.description)}`; + } + } - this.urlForNewGive = "/gifted-details?"; - if (this.giveDetails.amount) { - this.urlForNewGive += - "&amountInput=" + encodeURIComponent(String(this.giveDetails.amount)); - } - if (this.giveDetails.unit) { - this.urlForNewGive += - "&unitCode=" + encodeURIComponent(this.giveDetails.unit); - } - if (this.giveDetails.description) { - this.urlForNewGive += - "&description=" + encodeURIComponent(this.giveDetails.description); - } + /** + * Processes participant (giver/recipient) information + */ + private processParticipantInfo() { + if (this.giveDetails?.agentDid) { this.giverName = this.didInfo(this.giveDetails.agentDid); - if (this.giveDetails.agentDid) { - this.urlForNewGive += - "&giverDid=" + - encodeURIComponent(this.giveDetails.agentDid) + - "&giverName=" + - encodeURIComponent(this.giverName); - } + this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`; + } + if (this.giveDetails?.recipientDid) { this.recipientName = this.didInfo(this.giveDetails.recipientDid); - if (this.giveDetails.recipientDid) { - this.urlForNewGive += - "&recipientDid=" + - encodeURIComponent(this.giveDetails.recipientDid) + - "&recipientName=" + - encodeURIComponent(this.recipientName); - } - if (this.giveDetails.fullClaim.image) { - this.urlForNewGive += - "&image=" + encodeURIComponent(this.giveDetails.fullClaim.image); - } - if ( - this.giveDetails.type == "Offer" && - this.giveDetails.fulfillsHandleId - ) { - this.urlForNewGive += - "&offerId=" + - encodeURIComponent(this.giveDetails?.fulfillsHandleId as string); - } - if (this.giveDetails.fulfillsPlanHandleId) { - this.urlForNewGive += - "&fulfillsProjectId=" + - encodeURIComponent(this.giveDetails.fulfillsPlanHandleId); - } - - // retrieve the list of confirmers - const confirmerInfo = await libsUtil.retrieveConfirmerIdList( - this.apiServer, - claimId, - this.veriClaim.issuer, - userDid, - ); - if (confirmerInfo) { - this.confirmerIdList = confirmerInfo.confirmerIdList; - this.confsVisibleToIdList = confirmerInfo.confsVisibleToIdList; - this.numConfsNotVisible = confirmerInfo.numConfsNotVisible; - } else { - this.confsVisibleErrorMessage = - "Had problems retrieving confirmations."; - } - } catch (error: unknown) { - const serverError = error as AxiosError; - console.error("Error retrieving claim:", serverError); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "Something went wrong retrieving claim data.", - }, - 3000, - ); + this.urlForNewGive += `&recipientDid=${encodeURIComponent(this.giveDetails.recipientDid)}&recipientName=${encodeURIComponent(this.recipientName)}`; } } - confirmConfirmClaim() { - this.$notify( - { - group: "modal", - type: "confirm", - title: "Confirm", - text: "Do you personally confirm that this is true?", - onYes: async () => { - await this.confirmClaim(); - }, - }, - -1, - ); + /** + * Processes additional give details (image, offer, plan) + */ + private processAdditionalDetails() { + if (this.giveDetails?.fullClaim.image) { + this.urlForNewGive += `&image=${encodeURIComponent(this.giveDetails.fullClaim.image)}`; + } + if (this.giveDetails?.type === "Offer" && this.giveDetails?.fulfillsHandleId) { + this.urlForNewGive += `&offerId=${encodeURIComponent(this.giveDetails.fulfillsHandleId)}`; + } + if (this.giveDetails?.fulfillsPlanHandleId) { + this.urlForNewGive += `&fulfillsProjectId=${encodeURIComponent(this.giveDetails.fulfillsPlanHandleId)}`; + } } - // similar code is found in ProjectViewView - async confirmClaim() { - // similar logic is found in endorser-mobile - const goodClaim = serverUtil.removeSchemaContext( - serverUtil.removeVisibleToDids( - serverUtil.addLastClaimOrHandleAsIdIfMissing( - this.veriClaim.claim, - this.veriClaim.id, - this.veriClaim.handleId, - ), - ), - ); - const confirmationClaim: GenericVerifiableCredential = { - "@context": "https://schema.org", - "@type": "AgreeAction", - object: goodClaim, - }; - const result = await serverUtil.createAndSubmitClaim( - confirmationClaim, - this.activeDid, + /** + * Fetches confirmer information for the claim + */ + private async fetchConfirmerInfo(claimId: string, userDid: string) { + const confirmerInfo = await libsUtil.retrieveConfirmerIdList( this.apiServer, - this.axios, + claimId, + this.veriClaim.issuer, + userDid, ); - if (result.type === "success") { - this.$notify( - { - group: "alert", - type: "success", - title: "Success", - text: "Confirmation submitted.", - }, - 3000, - ); + + if (confirmerInfo) { + this.confirmerIdList = confirmerInfo.confirmerIdList; + this.confsVisibleToIdList = confirmerInfo.confsVisibleToIdList; + this.numConfsNotVisible = confirmerInfo.numConfsNotVisible; } else { - console.error("Got error submitting the confirmation:", result); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: "There was a problem submitting the confirmation.", - }, - 5000, - ); + this.confsVisibleErrorMessage = "Had problems retrieving confirmations."; } } - showClaimPage(claimId: string) { - const route = { - path: "/claim/" + encodeURIComponent(claimId), - }; - (this.$router as Router).push(route).then(async () => { - this.resetThisValues(); - await this.loadClaim(claimId, this.activeDid); - }); + /** + * Calculates total number of confirmers for the gift + * Includes both direct confirmers and those visible through network + * + * @returns Total number of confirmers + */ + totalConfirmers(): number { + return ( + this.numConfsNotVisible + + this.confirmerIdList.length + + this.confsVisibleToIdList.length + ); + } + + /** + * Formats display amount with proper unit + * + * @param unit - Currency or unit code + * @param amount - Numeric amount + * @returns Formatted amount string + */ + displayAmount(unit: string, amount: number): string { + return displayAmount(unit, amount); + } + + /** + * Retrieves human-readable name for a DID + * Falls back to DID if no name available + * + * @param did - DID to get name for + * @returns Human-readable name + */ + didInfo(did: string): string { + return serverUtil.didInfo( + did, + this.activeDid, + this.allMyDids, + this.allContacts, + ); } - copyToClipboard(name: string, text: string) { + /** + * Copies text to clipboard and shows notification + * + * @param description - Description of copied content + * @param text - Text to copy + */ + copyToClipboard(description: string, text: string): void { useClipboard() .copy(text) .then(() => { @@ -803,14 +767,52 @@ export default class ConfirmGiftView extends Vue { group: "alert", type: "toast", title: "Copied", - text: (name || "That") + " was copied to the clipboard.", + text: (description || "That") + " was copied to the clipboard.", }, 2000, ); }); } - notifyWhyCannotConfirm() { + /** + * Navigates to claim page for detailed view + * + * @param claimId - ID of claim to view + */ + showClaimPage(claimId: string): void { + const route = { + path: "/claim/" + encodeURIComponent(claimId), + }; + (this.$router as Router).push(route).then(async () => { + this.resetThisValues(); + await this.loadClaim(claimId, this.activeDid); + }); + } + + /** + * Initiates claim confirmation process + * Verifies user eligibility and handles confirmation workflow + */ + async confirmConfirmClaim(): Promise { + this.$notify( + { + group: "modal", + type: "confirm", + title: "Confirm", + text: "Do you personally confirm that this is true?", + onYes: async () => { + await this.confirmClaim(); + }, + }, + -1, + ); + } + + /** + * Notifies user why they cannot confirm the gift + * Explains requirements or restrictions preventing confirmation + */ + notifyWhyCannotConfirm(): void { libsUtil.notifyWhyCannotConfirm( this.$notify, this.isRegistered, @@ -821,71 +823,35 @@ export default class ConfirmGiftView extends Vue { ); } - notifyWhyCannotConfirmBak() { - if (!this.isRegistered) { - this.$notify( - { - group: "alert", - type: "info", - title: "Not Registered", - text: "Someone needs to register you before you can contribute.", - }, - 3000, - ); - } else if (!isGiveAction(this.veriClaim)) { - this.$notify( - { - group: "alert", - type: "info", - title: "Not A Give", - text: "This is not a giving action to confirm.", - }, - 3000, - ); - } else if (this.confirmerIdList.includes(this.activeDid)) { - this.$notify( - { - group: "alert", - type: "info", - title: "Already Confirmed", - text: "You already confirmed this claim.", - }, - 3000, - ); - } else if (this.giveDetails?.issuerDid == this.activeDid) { - this.$notify( - { - group: "alert", - type: "info", - title: "Cannot Confirm", - text: "You cannot confirm this because you issued this claim.", - }, - 3000, - ); - } else if (serverUtil.containsHiddenDid(this.giveDetails?.fullClaim)) { - this.$notify( - { - group: "alert", - type: "info", - title: "Cannot Confirm", - text: "You cannot confirm this because some people are hidden.", - }, - 3000, - ); + /** + * Formats type string for display by adding spaces before capitals + * Optionally adds a prefix + * + * @param text - Text to format + * @param prefix - Optional prefix to add + * @returns Formatted string + */ + capitalizeAndInsertSpacesBeforeCapsWithAPrefix( + text: string, + prefix?: string + ): string { + const word = this.capitalizeAndInsertSpacesBeforeCaps(text); + if (word) { + // if the word starts with a vowel, use "an" instead of "a" + const firstLetter = word[0].toLowerCase(); + const vowels = ["a", "e", "i", "o", "u"]; + const particle = vowels.includes(firstLetter) ? "an" : "a"; + return particle + " " + word; } else { - this.$notify( - { - group: "alert", - type: "info", - title: "Cannot Confirm", - text: "You cannot confirm this claim. There are no other details, but we can help more if you contact us and send us screenshots.", - }, - 3000, - ); + return ""; } } - onClickShareClaim() { + /** + * Initiates sharing of claim information + * Handles share functionality based on platform capabilities + */ + async onClickShareClaim(): Promise { this.copyToClipboard("A link to this page", this.windowLocation); window.navigator.share({ title: "Help Connect Me", @@ -893,5 +859,23 @@ export default class ConfirmGiftView extends Vue { url: this.windowLocation, }); } + + resetThisValues() { + this.confirmerIdList = []; + this.confsVisibleErrorMessage = ""; + this.confsVisibleToIdList = []; + this.giveDetails = undefined; + this.isRegistered = false; + this.numConfsNotVisible = 0; + this.urlForNewGive = ""; + this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; + this.veriClaimDump = ""; + } + + capitalizeAndInsertSpacesBeforeCaps(text: string) { + return !text + ? "" + : text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1"); + } }