Browse Source

refactor: Improve ConfirmGiftView component organization and error handling

- Split loadClaim into focused methods for better separation of concerns
- Add proper error handling and error messages
- Add JSDoc comments for all methods
- Extract URL parameter handling into dedicated method
- Improve gift confirmation and sharing workflows

The changes improve code maintainability by:
1. Breaking down monolithic methods into smaller, focused functions
2. Adding clear error handling and user feedback
3. Improving method documentation with JSDoc
4. Separating data fetching from processing logic
5. Making component behavior more predictable
pull/127/head
Matthew Raymer 2 days ago
parent
commit
93219219ba
  1. 570
      src/views/ConfirmGiftView.vue

570
src/views/ConfirmGiftView.vue

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

Loading…
Cancel
Save