Browse Source

Merge branch 'deep_linking' of ssh://173.199.124.46:222/trent_larson/crowd-funder-for-time-pwa into deep_linking

pull/127/head
Matthew Raymer 2 days ago
parent
commit
b6213f5040
  1. 610
      src/views/ConfirmGiftView.vue

610
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 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 ( * 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)}`;
this.issuerName = this.didInfo(this.veriClaim.issuer); try {
const headers = await serverUtil.getHeaders(userDid);
const resp = await this.axios.get(giveUrl, { headers });
// use give record when possible since it may include edits if (resp.status === 200) {
const giveUrl = this.giveDetails = resp.data.data[0];
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 { } 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) {
console.error("Error getting detailed give info:", error);
throw new Error("Something went wrong retrieving gift data.");
}
}
// the logic already stops earlier if the claim doesn't exist but this helps with typechecking /**
if (!this.giveDetails) { * Processes give details and builds URL for new give
return; */
} 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();
} }
if (this.giveDetails.unit) {
this.urlForNewGive += /**
"&unitCode=" + encodeURIComponent(this.giveDetails.unit); * Adds basic give details to URL
} */
if (this.giveDetails.description) { private addGiveDetailsToUrl() {
this.urlForNewGive += if (this.giveDetails?.amount) {
"&description=" + encodeURIComponent(this.giveDetails.description); 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); 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