forked from jsnbuchanan/crowd-funder-for-time-pwa
test: enhance deep link testing with real JWT examples
Changes: - Add real JWT example for invite testing - Add detailed JWT payload documentation - Update test-deeplinks.sh with valid claim IDs - Add test case for single contact invite - Improve test descriptions and organization This improves test coverage by using real-world JWT examples and valid claim identifiers.
This commit is contained in:
@@ -193,6 +193,38 @@ import {
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
|
||||
/**
|
||||
* Offer Details View Component
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This component handles the creation and editing of offers within the platform.
|
||||
* It supports both new offers and editing existing ones, with validation and
|
||||
* submission handling.
|
||||
*
|
||||
* Features:
|
||||
* - Offer amount and unit selection
|
||||
* - Item description
|
||||
* - Conditional requirements
|
||||
* - Expiration date setting
|
||||
* - Project or recipient targeting
|
||||
* - Raw claim editing option
|
||||
*
|
||||
* Data Flow:
|
||||
* 1. Component loads with optional previous offer data
|
||||
* 2. Retrieves account settings and contact information
|
||||
* 3. Populates form with existing or default values
|
||||
* 4. Validates and submits offer to server
|
||||
* 5. Redirects on success or shows error
|
||||
*
|
||||
* Security Features:
|
||||
* - DID validation
|
||||
* - JWT handling for edits
|
||||
* - Server-side validation
|
||||
* - Privacy controls for data sharing
|
||||
*
|
||||
* @see GiftedDialog for related gift creation
|
||||
* @see ClaimAddRawView for raw claim editing
|
||||
*/
|
||||
@Component({
|
||||
components: {
|
||||
QuickNav,
|
||||
@@ -200,35 +232,96 @@ import { retrieveAccountDids } from "../libs/util";
|
||||
},
|
||||
})
|
||||
export default class OfferDetailsView extends Vue {
|
||||
/** Notification function injected by Vue */
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
/** Current route instance */
|
||||
$route!: RouteLocationNormalizedLoaded;
|
||||
/** Router instance for navigation */
|
||||
$router!: Router;
|
||||
|
||||
/** Currently active DID */
|
||||
activeDid = "";
|
||||
/** API server endpoint */
|
||||
apiServer = "";
|
||||
|
||||
/** Offer amount input field */
|
||||
amountInput = "0";
|
||||
/** Conditions for the offer */
|
||||
descriptionOfCondition = "";
|
||||
/** Description of offered item */
|
||||
descriptionOfItem = "";
|
||||
/** Path to redirect after completion */
|
||||
destinationPathAfter = "";
|
||||
/** Controls back button visibility */
|
||||
hideBackButton = false;
|
||||
/** Additional message to display */
|
||||
message = "";
|
||||
/** Flag for project assignment */
|
||||
offeredToProject = false;
|
||||
/** Flag for recipient assignment */
|
||||
offeredToRecipient = false;
|
||||
/** DID of offer creator */
|
||||
offererDid: string | undefined;
|
||||
/** Offer ID for editing */
|
||||
offerId = "";
|
||||
/** Previous offer data for editing */
|
||||
prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>;
|
||||
/** Project ID if offer is for project */
|
||||
projectId = "";
|
||||
/** Project name display */
|
||||
projectName = "a project";
|
||||
/** Recipient DID if offer is for person */
|
||||
recipientDid = "";
|
||||
/** Recipient name display */
|
||||
recipientName = "";
|
||||
/** Advanced features visibility flag */
|
||||
showGeneralAdvanced = false;
|
||||
/** Unit type for offer amount */
|
||||
unitCode = "HUR";
|
||||
/** Expiration date input */
|
||||
validThroughDateInput = "";
|
||||
|
||||
/** Utility library reference */
|
||||
libsUtil = libsUtil;
|
||||
|
||||
/**
|
||||
* Component lifecycle hook that initializes the offer form
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Extracts previous offer data if editing
|
||||
* 2. Sets initial form values from route or previous offer
|
||||
* 3. Loads account settings and contacts
|
||||
* 4. Retrieves project information if needed
|
||||
* 5. Sets offer assignment flags
|
||||
*
|
||||
* @throws Will not throw but shows notifications
|
||||
* @emits Notifications on loading errors
|
||||
*/
|
||||
async mounted() {
|
||||
try {
|
||||
await this.loadPreviousOffer();
|
||||
await this.initializeFormValues();
|
||||
await this.loadAccountSettings();
|
||||
await this.loadRecipientInfo();
|
||||
await this.loadProjectInfo();
|
||||
} catch (err: any) {
|
||||
console.error("Error in mounted:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: err.message || "There was an error loading the offer details.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads previous offer data if editing an existing offer
|
||||
* @throws Will not throw but shows notifications
|
||||
*/
|
||||
private async loadPreviousOffer() {
|
||||
try {
|
||||
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
|
||||
? (JSON.parse(
|
||||
@@ -246,34 +339,38 @@ export default class OfferDetailsView extends Vue {
|
||||
5000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes form values from route params or previous offer
|
||||
*/
|
||||
private async initializeFormValues() {
|
||||
const prevAmount =
|
||||
this.prevCredToEdit?.claim?.includesObject?.amountOfThisGood;
|
||||
this.amountInput =
|
||||
(this.$route.query["amountInput"] as string) ||
|
||||
(prevAmount ? String(prevAmount) : "") ||
|
||||
this.amountInput;
|
||||
|
||||
this.unitCode = ((this.$route.query["unitCode"] as string) ||
|
||||
this.prevCredToEdit?.claim?.includesObject?.unitCode ||
|
||||
this.unitCode) as string;
|
||||
|
||||
this.descriptionOfCondition =
|
||||
this.prevCredToEdit?.claim?.description || this.descriptionOfCondition;
|
||||
|
||||
this.descriptionOfItem =
|
||||
(this.$route.query["description"] as string) ||
|
||||
this.prevCredToEdit?.claim?.itemOffered?.description ||
|
||||
this.descriptionOfItem;
|
||||
|
||||
this.destinationPathAfter =
|
||||
(this.$route.query["destinationPathAfter"] as string) || "";
|
||||
this.offererDid = ((this.$route.query["offererDid"] as string) ||
|
||||
(this.prevCredToEdit?.claim?.agent as unknown as { identifier: string })
|
||||
?.identifier ||
|
||||
this.offererDid) as string;
|
||||
this.hideBackButton =
|
||||
(this.$route.query["hideBackButton"] as string) === "true";
|
||||
this.message = (this.$route.query["message"] as string) || "";
|
||||
|
||||
// find any project ID
|
||||
// Set project info from previous offer or route
|
||||
let project;
|
||||
if (
|
||||
this.prevCredToEdit?.claim?.itemOffered?.isPartOf?.["@type"] ===
|
||||
@@ -294,43 +391,43 @@ export default class OfferDetailsView extends Vue {
|
||||
|
||||
this.validThroughDateInput =
|
||||
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer ?? "";
|
||||
this.activeDid = settings.activeDid ?? "";
|
||||
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
|
||||
/**
|
||||
* Loads account settings and updates component state
|
||||
* @throws Will not throw but logs errors
|
||||
*/
|
||||
private async loadAccountSettings() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer ?? "";
|
||||
this.activeDid = settings.activeDid ?? "";
|
||||
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
|
||||
}
|
||||
|
||||
if (this.recipientDid && !this.recipientName) {
|
||||
const allContacts = await db.contacts.toArray();
|
||||
const allMyDids = await retrieveAccountDids();
|
||||
this.recipientName = didInfo(
|
||||
this.recipientDid,
|
||||
this.activeDid,
|
||||
allMyDids,
|
||||
allContacts,
|
||||
);
|
||||
}
|
||||
// these should be functions but something's wrong with the syntax in the <> conditional
|
||||
this.offeredToProject = !!this.projectId;
|
||||
this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
console.error("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: err.message || "There was an error retrieving your settings.",
|
||||
},
|
||||
5000,
|
||||
/**
|
||||
* Loads recipient information if recipient DID exists
|
||||
*/
|
||||
private async loadRecipientInfo() {
|
||||
if (this.recipientDid && !this.recipientName) {
|
||||
const allContacts = await db.contacts.toArray();
|
||||
const allMyDids = await retrieveAccountDids();
|
||||
this.recipientName = didInfo(
|
||||
this.recipientDid,
|
||||
this.activeDid,
|
||||
allMyDids,
|
||||
allContacts,
|
||||
);
|
||||
}
|
||||
// Set assignment flags
|
||||
this.offeredToProject = !!this.projectId;
|
||||
this.offeredToRecipient = !this.offeredToProject && !!this.recipientDid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads project information if project ID exists
|
||||
*/
|
||||
private async loadProjectInfo() {
|
||||
if (this.projectId && !this.projectName) {
|
||||
// console.log("Getting project name from cache", this.projectId);
|
||||
const project = await getPlanFromCache(
|
||||
this.projectId,
|
||||
this.axios,
|
||||
@@ -343,16 +440,32 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the unit type for the offer amount
|
||||
*
|
||||
* Cycles through available unit types in UNIT_SHORT.
|
||||
* Updates display and internal state.
|
||||
*/
|
||||
changeUnitCode() {
|
||||
const units = Object.keys(this.libsUtil.UNIT_SHORT);
|
||||
const index = units.indexOf(this.unitCode);
|
||||
this.unitCode = units[(index + 1) % units.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the offer amount by 1
|
||||
*
|
||||
* Handles string to number conversion and updates display.
|
||||
*/
|
||||
increment() {
|
||||
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements the offer amount by 1
|
||||
*
|
||||
* Prevents negative values and handles string to number conversion.
|
||||
*/
|
||||
decrement() {
|
||||
this.amountInput = `${Math.max(
|
||||
0,
|
||||
@@ -360,6 +473,15 @@ export default class OfferDetailsView extends Vue {
|
||||
)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles cancellation of offer creation/editing
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Checks for destination path
|
||||
* 2. Navigates to destination or previous page
|
||||
*
|
||||
* @emits Router navigation
|
||||
*/
|
||||
cancel() {
|
||||
if (this.destinationPathAfter) {
|
||||
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||
@@ -368,10 +490,28 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles back button navigation
|
||||
*
|
||||
* @emits Router navigation to previous page
|
||||
*/
|
||||
cancelBack() {
|
||||
(this.$router as Router).back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and initiates offer submission
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Validates active DID exists
|
||||
* 2. Checks for negative amounts
|
||||
* 3. Ensures description or amount exists
|
||||
* 4. Shows processing notification
|
||||
* 5. Calls recordOffer for submission
|
||||
*
|
||||
* @throws Will not throw but shows notifications
|
||||
* @emits Notifications for validation errors or processing
|
||||
*/
|
||||
async confirm() {
|
||||
if (!this.activeDid) {
|
||||
this.$notify(
|
||||
@@ -426,6 +566,15 @@ export default class OfferDetailsView extends Vue {
|
||||
await this.recordOffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies user about project assignment restrictions
|
||||
*
|
||||
* Shows appropriate error message based on:
|
||||
* - Missing project ID
|
||||
* - Conflict with recipient assignment
|
||||
*
|
||||
* @emits Notification with error message
|
||||
*/
|
||||
notifyUserOfProject() {
|
||||
if (!this.projectId) {
|
||||
this.$notify(
|
||||
@@ -451,6 +600,15 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies user about recipient assignment restrictions
|
||||
*
|
||||
* Shows appropriate error message based on:
|
||||
* - Missing recipient DID
|
||||
* - Conflict with project assignment
|
||||
*
|
||||
* @emits Notification with error message
|
||||
*/
|
||||
notifyUserOfRecipient() {
|
||||
if (!this.recipientDid) {
|
||||
this.$notify(
|
||||
@@ -477,11 +635,18 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the offer to the server
|
||||
*
|
||||
* @param offererDid may be null
|
||||
* @param description may be an empty string
|
||||
* @param amountInput may be 0
|
||||
* @param unitCode may be omitted, defaults to "HUR"
|
||||
* Workflow:
|
||||
* 1. Determines if editing existing or creating new
|
||||
* 2. Prepares offer data with assignments
|
||||
* 3. Submits to server via appropriate method
|
||||
* 4. Handles success/error responses
|
||||
* 5. Navigates on success
|
||||
*
|
||||
* @throws Will not throw but shows notifications
|
||||
* @emits Notifications for success/failure
|
||||
* @emits Router navigation on success
|
||||
*/
|
||||
public async recordOffer() {
|
||||
try {
|
||||
@@ -568,6 +733,17 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs offer parameters for raw editing
|
||||
*
|
||||
* Creates a JSON string containing:
|
||||
* - Offer details
|
||||
* - Assignments
|
||||
* - Conditions
|
||||
* - Expiration
|
||||
*
|
||||
* @returns JSON string of offer parameters
|
||||
*/
|
||||
constructOfferParam() {
|
||||
const recipientDid = this.offeredToRecipient
|
||||
? this.recipientDid
|
||||
@@ -589,11 +765,11 @@ export default class OfferDetailsView extends Vue {
|
||||
return claimStr;
|
||||
}
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
/**
|
||||
* @param result response "data" from the server
|
||||
* @returns true if the result indicates an error
|
||||
* Checks if server response indicates an error
|
||||
*
|
||||
* @param result Response data from server
|
||||
* @returns true if response indicates error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isCreationError(result: any) {
|
||||
@@ -601,8 +777,10 @@ export default class OfferDetailsView extends Vue {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||
* @returns best guess at an error message
|
||||
* Extracts error message from server response
|
||||
*
|
||||
* @param result Server response object
|
||||
* @returns Best available error message
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getCreationErrorMessage(result: any) {
|
||||
@@ -613,6 +791,13 @@ export default class OfferDetailsView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows privacy information dialog
|
||||
*
|
||||
* Displays standard privacy message about data sharing.
|
||||
*
|
||||
* @emits Notification with privacy message
|
||||
*/
|
||||
explainData() {
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user