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. 614
      src/views/ConfirmGiftView.vue

614
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)
capitalizeAndInsertSpacesBeforeCaps(text: string) {
return !text
? ""
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
} }
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string) { /**
const word = this.capitalizeAndInsertSpacesBeforeCaps(text); * Loads and processes claim from URL parameters
if (word) { */
// if the word starts with a vowel, use "an" instead of "a" private async loadClaimFromUrl() {
const firstLetter = word[0].toLowerCase(); const pathParam = window.location.pathname.substring("/confirm-gift/".length);
const vowels = ["a", "e", "i", "o", "u"]; if (!pathParam) {
const particle = vowels.includes(firstLetter) ? "an" : "a"; throw new Error("No claim ID was provided.");
return particle + " " + word;
} else {
return "";
} }
const claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, this.activeDid);
} }
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,210 +604,161 @@ 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(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem retrieving that claim.",
},
3000,
);
return;
} }
} 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") { * Fetches detailed give information
// no need to go further... this page is for gifts */
return; 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 this.urlForNewGive = "/gifted-details?";
const giveUrl = this.addGiveDetailsToUrl();
this.apiServer + this.processParticipantInfo();
"/api/v2/report/gives?handleId=" + this.processAdditionalDetails();
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;
}
// the logic already stops earlier if the claim doesn't exist but this helps with typechecking /**
if (!this.giveDetails) { * Adds basic give details to URL
return; */
} 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) { * Processes participant (giver/recipient) information
this.urlForNewGive += */
"&amountInput=" + encodeURIComponent(String(this.giveDetails.amount)); private processParticipantInfo() {
} if (this.giveDetails?.agentDid) {
if (this.giveDetails.unit) {
this.urlForNewGive +=
"&unitCode=" + encodeURIComponent(this.giveDetails.unit);
}
if (this.giveDetails.description) {
this.urlForNewGive +=
"&description=" + encodeURIComponent(this.giveDetails.description);
}
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=" + if (this.giveDetails?.recipientDid) {
encodeURIComponent(this.giveDetails.agentDid) +
"&giverName=" +
encodeURIComponent(this.giverName);
}
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);
}
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,
);
} }
} }
confirmConfirmClaim() { /**
this.$notify( * Processes additional give details (image, offer, plan)
{ */
group: "modal", private processAdditionalDetails() {
type: "confirm", if (this.giveDetails?.fullClaim.image) {
title: "Confirm", this.urlForNewGive += `&image=${encodeURIComponent(this.giveDetails.fullClaim.image)}`;
text: "Do you personally confirm that this is true?", }
onYes: async () => { if (this.giveDetails?.type === "Offer" && this.giveDetails?.fulfillsHandleId) {
await this.confirmClaim(); this.urlForNewGive += `&offerId=${encodeURIComponent(this.giveDetails.fulfillsHandleId)}`;
}, }
}, if (this.giveDetails?.fulfillsPlanHandleId) {
-1, this.urlForNewGive += `&fulfillsProjectId=${encodeURIComponent(this.giveDetails.fulfillsPlanHandleId)}`;
); }
} }
// similar code is found in ProjectViewView /**
async confirmClaim() { * Fetches confirmer information for the claim
// similar logic is found in endorser-mobile */
const goodClaim = serverUtil.removeSchemaContext( private async fetchConfirmerInfo(claimId: string, userDid: string) {
serverUtil.removeVisibleToDids( const confirmerInfo = await libsUtil.retrieveConfirmerIdList(
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,
this.apiServer, this.apiServer,
this.axios, claimId,
this.veriClaim.issuer,
userDid,
); );
if (result.type === "success") {
this.$notify( if (confirmerInfo) {
{ this.confirmerIdList = confirmerInfo.confirmerIdList;
group: "alert", this.confsVisibleToIdList = confirmerInfo.confsVisibleToIdList;
type: "success", this.numConfsNotVisible = confirmerInfo.numConfsNotVisible;
title: "Success",
text: "Confirmation submitted.",
},
3000,
);
} else { } else {
console.error("Got error submitting the confirmation:", result); this.confsVisibleErrorMessage = "Had problems retrieving confirmations.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the confirmation.",
},
5000,
);
} }
} }
showClaimPage(claimId: string) { /**
const route = { * Calculates total number of confirmers for the gift
path: "/claim/" + encodeURIComponent(claimId), * Includes both direct confirmers and those visible through network
}; *
(this.$router as Router).push(route).then(async () => { * @returns Total number of confirmers
this.resetThisValues(); */
await this.loadClaim(claimId, this.activeDid); 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() useClipboard()
.copy(text) .copy(text)
.then(() => { .then(() => {
@ -803,14 +767,52 @@ export default class ConfirmGiftView extends Vue {
group: "alert", group: "alert",
type: "toast", type: "toast",
title: "Copied", title: "Copied",
text: (name || "That") + " was copied to the clipboard.", text: (description || "That") + " was copied to the clipboard.",
}, },
2000, 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<void> {
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( 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