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. 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