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 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");
/**
* 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.");
}
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 "";
}
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,200 +604,182 @@ 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;
}
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
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];
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 {
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;
throw new Error("Error getting detailed give info: " + resp.status);
}
// the logic already stops earlier if the claim doesn't exist but this helps with typechecking
if (!this.giveDetails) {
return;
} catch (error) {
console.error("Error getting detailed give info:", error);
throw new Error("Something went wrong retrieving gift data.");
}
}
/**
* Processes give details and builds URL for new give
*/
private async processGiveDetails() {
if (!this.giveDetails) return;
this.urlForNewGive = "/gifted-details?";
if (this.giveDetails.amount) {
this.urlForNewGive +=
"&amountInput=" + encodeURIComponent(String(this.giveDetails.amount));
this.addGiveDetailsToUrl();
this.processParticipantInfo();
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) {
this.urlForNewGive +=
"&unitCode=" + encodeURIComponent(this.giveDetails.unit);
if (this.giveDetails?.unit) {
this.urlForNewGive += `&unitCode=${encodeURIComponent(this.giveDetails.unit)}`;
}
if (this.giveDetails.description) {
this.urlForNewGive +=
"&description=" + encodeURIComponent(this.giveDetails.description);
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
this.urlForNewGive += `&recipientDid=${encodeURIComponent(this.giveDetails.recipientDid)}&recipientName=${encodeURIComponent(this.recipientName)}`;
}
}
/**
* 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)}`;
}
}
/**
* Fetches confirmer information for the claim
*/
private async fetchConfirmerInfo(claimId: string, userDid: string) {
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.confsVisibleErrorMessage = "Had problems retrieving confirmations.";
}
}
confirmConfirmClaim() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Confirm",
text: "Do you personally confirm that this is true?",
onYes: async () => {
await this.confirmClaim();
},
},
-1,
/**
* 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
);
}
// 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,
/**
* 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.apiServer,
this.axios,
);
if (result.type === "success") {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Confirmation submitted.",
},
3000,
this.allMyDids,
this.allContacts,
);
} 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(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the confirmation.",
type: "toast",
title: "Copied",
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 = {
path: "/claim/" + encodeURIComponent(claimId),
};
@ -794,23 +789,30 @@ export default class ConfirmGiftView extends Vue {
});
}
copyToClipboard(name: string, text: string) {
useClipboard()
.copy(text)
.then(() => {
/**
* Initiates claim confirmation process
* Verifies user eligibility and handles confirmation workflow
*/
async confirmConfirmClaim(): Promise<void> {
this.$notify(
{
group: "alert",
type: "toast",
title: "Copied",
text: (name || "That") + " was copied to the clipboard.",
group: "modal",
type: "confirm",
title: "Confirm",
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(
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<void> {
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");
}
}
</script>

Loading…
Cancel
Save