Browse Source

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.
pull/127/head
Matthew Raymer 2 weeks ago
parent
commit
f4c5567471
  1. 1
      src/interfaces/records.ts
  2. 2
      src/lib/capacitor/app.ts
  3. 2
      src/libs/endorserServer.ts
  4. 69
      src/views/ContactImportView.vue
  5. 202
      src/views/InviteOneAcceptView.vue
  6. 249
      src/views/OfferDetailsView.vue
  7. 9
      src/views/ProjectViewView.vue
  8. 23
      test-deeplinks.sh

1
src/interfaces/records.ts

@ -58,6 +58,7 @@ export interface PlanSummaryRecord {
name?: string;
startTime?: string;
url?: string;
jwtId?: string;
}
/**

2
src/lib/capacitor/app.ts

@ -4,7 +4,7 @@ import {
App as CapacitorApp,
AppLaunchUrl,
BackButtonListener,
} from '../../../node_modules/@capacitor/app';
} from "../../../node_modules/@capacitor/app";
import type { PluginListenerHandle } from "@capacitor/core";
/**

2
src/libs/endorserServer.ts

@ -175,7 +175,7 @@ export function isEmptyOrHiddenDid(did?: string): boolean {
*/
function testRecursivelyOnStrings(
func: (arg0: any) => boolean,
input: any
input: any,
): boolean {
// Test direct string values
if (Object.prototype.toString.call(input) === "[object String]") {

69
src/views/ContactImportView.vue

@ -3,7 +3,10 @@
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1 class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" @click="$router.back()">
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@ -17,28 +20,43 @@
<font-awesome icon="spinner" class="animate-spin" />
</div>
<div v-else>
<span v-if="contactsImporting.length > sameCount" class="flex justify-center">
<span
v-if="contactsImporting.length > sameCount"
class="flex justify-center"
>
<input v-model="makeVisible" type="checkbox" class="mr-2" />
Make my activity visible to these contacts.
</span>
<div v-if="sameCount > 0">
<span v-if="sameCount == 1">One contact is the same as an existing contact</span>
<span v-else>{{ sameCount }} contacts are the same as existing contacts</span>
<span v-if="sameCount == 1"
>One contact is the same as an existing contact</span
>
<span v-else
>{{ sameCount }} contacts are the same as existing contacts</span
>
</div>
<!-- Results List -->
<ul v-if="contactsImporting.length > sameCount" class="border-t border-slate-300">
<ul
v-if="contactsImporting.length > sameCount"
class="border-t border-slate-300"
>
<li v-for="(contact, index) in contactsImporting" :key="contact.did">
<div v-if="
<div
v-if="
!contactsExisting[contact.did] ||
!R.isEmpty(contactDifferences[contact.did])
" class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4">
"
class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4"
>
<h2 class="text-base font-semibold">
<input v-model="contactsSelected[index]" type="checkbox" />
{{ contact.name || AppString.NO_CONTACT_NAME }}
-
<span v-if="contactsExisting[contact.did]" class="text-orange-500">Existing</span>
<span v-if="contactsExisting[contact.did]" class="text-orange-500"
>Existing</span
>
<span v-else class="text-green-500">New</span>
</h2>
<div class="text-sm truncate">
@ -51,9 +69,13 @@
<div class="font-bold">Old Value</div>
<div class="font-bold">New Value</div>
</div>
<div v-for="(value, contactField) in contactDifferences[
<div
v-for="(value, contactField) in contactDifferences[
contact.did
]" :key="contactField" class="grid grid-cols-3 border">
]"
:key="contactField"
class="grid grid-cols-3 border"
>
<div class="border font-bold p-1">
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
</div>
@ -66,7 +88,8 @@
</li>
<button
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-sm text-white mt-2 px-2 py-1.5 rounded"
@click="importContacts">
@click="importContacts"
>
Import Selected Contacts
</button>
</ul>
@ -78,10 +101,18 @@
get the full text and paste it. (Note that iOS cuts off data in text
messages.) Ask the person to send the data a different way, eg. email.
<div class="mt-4 text-center">
<textarea v-model="inputJwt" placeholder="Contact-import data"
class="mt-4 border-2 border-gray-300 p-2 rounded" cols="30" @input="() => checkContactJwt(inputJwt)" />
<textarea
v-model="inputJwt"
placeholder="Contact-import data"
class="mt-4 border-2 border-gray-300 p-2 rounded"
cols="30"
@input="() => checkContactJwt(inputJwt)"
/>
<br />
<button class="ml-2 p-2 bg-blue-500 text-white rounded" @click="() => processContactJwt(inputJwt)">
<button
class="ml-2 p-2 bg-blue-500 text-white rounded"
@click="() => processContactJwt(inputJwt)"
>
Check Import
</button>
</div>
@ -391,7 +422,12 @@ export default class ContactImportView extends Vue {
}
> = {};
Object.keys(contactIn).forEach((key) => {
if (!R.equals(contactIn[key as keyof Contact], existingContact[key as keyof Contact])) {
if (
!R.equals(
contactIn[key as keyof Contact],
existingContact[key as keyof Contact],
)
) {
differences[key] = {
old: existingContact[key as keyof Contact],
new: contactIn[key as keyof Contact],
@ -517,7 +553,8 @@ export default class ContactImportView extends Vue {
group: "alert",
type: "danger",
title: "Visibility Error",
text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${failedVisibileToContacts.length == 1 ? "" : "s"
text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${
failedVisibileToContacts.length == 1 ? "" : "s"
}. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
},
-1,

202
src/views/InviteOneAcceptView.vue

@ -39,7 +39,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { Router, RouteLocationNormalized } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER, NotificationIface } from "../constants/app";
@ -52,19 +52,69 @@ import { decodeEndorserJwt } from "../libs/crypto/vc";
import { errorStringForLog } from "../libs/endorserServer";
import { generateSaveAndActivateIdentity } from "../libs/util";
@Component({ components: { QuickNav } })
/**
* Invite One Accept View Component
* @author Matthew Raymer
*
* This component handles accepting single-use invitations to join the platform.
* It supports multiple invitation formats and provides user feedback during the process.
*
* Workflow:
* 1. Component loads with JWT from route or user input
* 2. Validates JWT format and signature
* 3. Processes invite data and redirects to contacts page
* 4. Handles errors with user feedback
*
* Supported Invite Formats:
* 1. Direct JWT in URL path: /invite-one-accept/{jwt}
* 2. JWT in text message URL: https://app.example.com/invite-one-accept/{jwt}
* 3. JWT surrounded by other text: "Your invite code is {jwt}"
*
* Security Features:
* - JWT validation
* - Identity generation if needed
* - Error handling for invalid/expired invites
*
* @see ContactsView for completion of invite process
*/
@Component({
components: { QuickNav },
})
export default class InviteOneAcceptView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Router instance for navigation */
$router!: Router;
/** Route instance for current route */
$route!: RouteLocationNormalized;
activeDid: string = "";
apiServer: string = "";
checkingInvite: boolean = true;
inputJwt: string = "";
/** Active user's DID */
activeDid = "";
/** API server endpoint */
apiServer = "";
/** Loading state for invite processing */
checkingInvite = true;
/** User input for manual JWT entry */
inputJwt = "";
/**
* Component lifecycle hook that initializes invite processing
*
* Workflow:
* 1. Opens database connection
* 2. Retrieves account settings
* 3. Ensures active DID exists or generates one
* 4. Extracts JWT from URL path
* 5. Processes invite automatically
*
* @throws Will not throw but logs errors
* @emits Notifications on errors
*/
async mounted() {
this.checkingInvite = true;
await db.open();
// Load or generate identity
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
@ -73,41 +123,109 @@ export default class InviteOneAcceptView extends Vue {
this.activeDid = await generateSaveAndActivateIdentity();
}
const jwt = window.location.pathname.substring(
"/invite-one-accept/".length,
);
// Extract JWT from route path
const jwt = (this.$route.params.jwt as string) || "";
await this.processInvite(jwt, false);
this.checkingInvite = false;
}
// process the invite JWT and/or text message containing the URL with the JWT
/**
* Processes an invite JWT and/or text containing the invite
*
* Handles multiple input formats:
* 1. Direct JWT:
* - Raw JWT string starting with "ey"
* - Example: eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ...
*
* 2. URL containing JWT:
* - Full URL with JWT in path
* - Pattern: /invite-one-accept/{jwt}
* - Example: https://app.example.com/invite-one-accept/eyJ0eXAiOiJKV1Q...
*
* 3. Text with embedded JWT:
* - JWT surrounded by other text
* - Uses regex to extract JWT pattern
* - Example: "Your invite code is eyJ0eXAiOiJKV1Q... Click to accept"
*
* Extraction Process:
* 1. First attempts URL pattern match
* 2. If no URL found, looks for JWT pattern (ey...)
* 3. Validates extracted JWT format
* 4. Redirects to contacts page on success
*
* Error Handling:
* - Missing JWT: Shows "Missing Invite" notification
* - Invalid JWT: Logs error and shows generic error message
* - Network Issues: Captured in try/catch block
*
* @param jwtInput Raw input that may contain a JWT
* @param notifyOnFailure Whether to show error notifications
* - true: Shows UI notifications for errors
* - false: Silently logs errors (used for auto-processing)
* @throws Will not throw but logs errors
* @emits Notifications on errors if notifyOnFailure is true
* @emits Router navigation on success to /contacts?inviteJwt={jwt}
*/
async processInvite(jwtInput: string, notifyOnFailure: boolean) {
this.checkingInvite = true;
try {
let jwt: string = jwtInput ?? "";
const jwt = this.extractJwtFromInput(jwtInput);
if (!jwt) {
this.handleMissingJwt(notifyOnFailure);
return;
}
await this.validateAndRedirect(jwt);
} catch (error) {
this.handleError(error, notifyOnFailure);
} finally {
this.checkingInvite = false;
}
}
// parse the string: extract the URL or JWT if surrounded by spaces
// and then extract the JWT from the URL
// (For another approach used with contacts, see getContactPayloadFromJwtUrl)
/**
* Extracts JWT from various input formats
* @param input Raw input text
* @returns Extracted JWT or empty string
*/
private extractJwtFromInput(input: string): string {
const jwtInput = input ?? "";
// Try URL format first
const urlMatch = jwtInput.match(/(https?:\/\/[^\s]+)/);
if (urlMatch && urlMatch[1]) {
// extract the JWT from the URL, meaning any character except "?"
if (urlMatch?.[1]) {
const internalMatch = urlMatch[1].match(/\/invite-one-accept\/([^?]+)/);
if (internalMatch && internalMatch[1]) {
jwt = internalMatch[1];
if (internalMatch?.[1]) return internalMatch[1];
}
} else {
// extract the JWT (which starts with "ey") if it is surrounded by other input
// Try direct JWT format
const spaceMatch = jwtInput.match(/(ey[\w.-]+)/);
if (spaceMatch && spaceMatch[1]) {
jwt = spaceMatch[1];
if (spaceMatch?.[1]) return spaceMatch[1];
return "";
}
/**
* Validates JWT and redirects to contacts page
* @param jwt JWT to validate
*/
private async validateAndRedirect(jwt: string) {
decodeEndorserJwt(jwt);
this.$router.push({
name: "contacts",
query: { inviteJwt: jwt },
});
}
if (!jwt) {
if (notifyOnFailure) {
/**
* Handles missing JWT error
* @param notify Whether to show notification
*/
private handleMissingJwt(notify: boolean) {
if (notify) {
this.$notify(
{
group: "alert",
@ -118,21 +236,18 @@ export default class InviteOneAcceptView extends Vue {
5000,
);
}
} else {
//const payload: JWTPayload =
decodeEndorserJwt(jwt);
// That's good enough for an initial check.
// Send them to the contacts page to finish, with inviteJwt in the query string.
this.$router.push({
name: "contacts",
query: { inviteJwt: jwt },
});
}
} catch (error) {
/**
* Handles processing errors
* @param error Error that occurred
* @param notify Whether to show notification
*/
private handleError(error: unknown, notify: boolean) {
const fullError = "Error accepting invite: " + errorStringForLog(error);
logConsoleAndDb(fullError, true);
if (notifyOnFailure) {
if (notify) {
this.$notify(
{
group: "alert",
@ -144,10 +259,19 @@ export default class InviteOneAcceptView extends Vue {
);
}
}
this.checkingInvite = false;
}
// check the invite JWT
/**
* Validates invite data format
*
* Checks for common error cases:
* - Truncated URLs
* - Missing JWT data
* - Invalid URL formats
*
* @param jwtInput Raw input to validate
* @throws Will not throw but shows notifications
* @emits Notifications on validation errors
*/
async checkInvite(jwtInput: string) {
if (
jwtInput.endsWith(APP_SERVER) ||

249
src/views/OfferDetailsView.vue

@ -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,13 +391,23 @@ export default class OfferDetailsView extends Vue {
this.validThroughDateInput =
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
}
try {
/**
* 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;
}
/**
* 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();
@ -311,26 +418,16 @@ export default class OfferDetailsView extends Vue {
allContacts,
);
}
// these should be functions but something's wrong with the syntax in the <> conditional
// Set assignment flags
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 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
*
* 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
*
* @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"
* @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(
{

9
src/views/ProjectViewView.vue

@ -285,7 +285,7 @@
<ul v-else class="text-sm border-t border-slate-300">
<li
v-for="offer in offersToThis"
:key="offer.id"
:key="offer.jwtId"
class="py-1.5 border-b border-slate-300"
>
<div class="flex justify-between gap-4">
@ -365,7 +365,7 @@
<ul v-else class="text-sm border-t border-slate-300">
<li
v-for="give in givesToThis"
:key="give.id"
:key="give.jwtId"
class="py-1.5 border-b border-slate-300"
>
<div class="flex justify-between gap-4">
@ -461,7 +461,7 @@
<ul v-else class="text-sm border-t border-slate-300">
<li
v-for="give in givesProvidedByThis"
:key="give.id"
:key="give.jwtId"
class="py-1.5 border-b border-slate-300"
>
<div class="flex justify-between gap-4">
@ -572,7 +572,8 @@ import HiddenDidDialog from "../components/HiddenDidDialog.vue";
* Project View Component
* @author Matthew Raymer
*
* This component displays detailed project information and manages interactions including:
* This component displays and manages detailed project information. It handles:
* - Project loading and display from URL-encoded project handles
* - Project metadata (name, description, dates, location)
* - Issuer information and verification
* - Project contributions and fulfillments

23
test-deeplinks.sh

@ -82,13 +82,9 @@ test_link "timesafari://claim-add-raw/123?claimJwtId=jwt123"
echo "\nTesting Project Routes:"
test_link "timesafari://project/https%3A%2F%2Fendorser.ch%2Fentity%2F01JKW0QZX1XVCVZV85VXAMB31R"
# Test invite routes
echo "\nTesting Invite Routes:"
test_link "timesafari://invite-one-accept/eyJhbGciOiJFUzI1NksifQ"
# Test gift routes
echo "\nTesting Gift Routes:"
test_link "timesafari://confirm-gift/789"
test_link "timesafari://confirm-gift/01JMTC8T961KFPP2N8ZB92ER4K"
# Test offer routes
echo "\nTesting Offer Routes:"
@ -112,5 +108,22 @@ test_link "timesafari://invalid-route/123"
test_link "timesafari://claim/123?view=invalid"
test_link "timesafari://did/invalid-did"
# Single invite JWT test
# Header: {"typ":"JWT","alg":"ES256K"}
# Payload: {
# "iat": 1740740453,
# "contact": {
# "did": "did:ethr:0xFEd3b416946b23F3F472799053144B4E34155B5b",
# "name": "Jordan",
# "nextPubKeyHashB64": "IBfRZfwdzeKOzqCx8b+WlLpMJHOAT9ZknIDJo7F3rZE=",
# "publicKeyBase64": "A1eIndfaxgMpVwyD5dYe74DgjuIo5SwPZFCcLdOemjf"
# },
# "iss": "did:ethr:0xD53114830D4a5D90416B43Fc99a25b0dF8bb1BAd"
# }
SINGLE_INVITE_JWT="eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE3NDA3NDA0NTMsImNvbnRhY3QiOnsiZGlkIjoiZGlkOmV0aHI6MHhGRWQzYjQxNjk0NmIyM0YzRjQ3Mjc5OTA1MzE0NEI0RTM0MTU1QjViIiwibmFtZSI6IkpvcmRhbiIsIm5leHRQdWJLZXlIYXNoQjY0IjoiSUJmUlpmd2R6ZUtPenFDeDhiK1dsTHBNSkhPQVQ5WmtuSURKbzdGM3JaRT0iLCJwdWJsaWNLZXlCYXNlNjQiOiJBMWVJbmRmYXhnTXBWd3lENWRZZTc0RGdqdUlvNVN3UFpGQ2NMZEtlbWpmIn0sImlzcyI6ImRpZDpldGhyOjB4RDUzMTE0ODMwRDRhNUQ5MDQxNkI0M0ZjOTlhMjViMGRGOGJiMUJBZCJ9.yKEFounxUGU9-grAMFHA12dif9BKYkftg8F3wAIcFYh0H_k1tevjEYyD1fvAyIxYxK5xR0E8moqMhi78ipJXcg"
test_link "timesafari://invite-one-accept/$SINGLE_INVITE_JWT" "Single contact invite via JWT"
echo "\nDeep link testing complete"
echo "======================================"
Loading…
Cancel
Save