Browse Source

docs: add comprehensive JSDoc documentation to views

Changes:
- Add detailed JSDoc headers to ContactImportView
- Add component-level documentation to ProjectViewView
- Document state management and data flow
- Add security considerations and usage examples
- Improve test script documentation and organization
- Add interface documentation for deep linking

This improves code maintainability by documenting component
architecture, workflows and integration points.
side_step
Matthew Raymer 2 weeks ago
parent
commit
02bf0b3f1a
  1. 103
      src/views/ContactEditView.vue
  2. 308
      src/views/ContactImportView.vue
  3. 193
      src/views/ProjectViewView.vue
  4. 55
      test-deeplinks.sh

103
src/views/ContactEditView.vue

@ -142,6 +142,37 @@ import { AppString, NotificationIface } from "../constants/app";
import { db } from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
/**
* Contact Edit View Component
* @author Matthew Raymer
*
* This component provides a full-featured contact editing interface with support for:
* - Basic contact information (name, notes)
* - Multiple contact methods with type selection
* - Data validation and persistence
*
* Workflow:
* 1. Component loads with DID from route params
* 2. Fetches existing contact data from IndexedDB
* 3. Presents editable form with current values
* 4. Validates and saves updates back to database
*
* Contact Method Types:
* - CELL: Mobile phone numbers
* - EMAIL: Email addresses
* - WHATSAPP: WhatsApp contact info
*
* State Management:
* - Maintains separate state for form fields to prevent direct mutation
* - Handles array cloning for contact methods to prevent reference issues
* - Manages dropdown state for method type selection
*
* Navigation:
* - Back button returns to previous view
* - Save redirects to contact detail view
* - Cancel returns to previous view
* - Invalid DID redirects to contacts list
*/
@Component({
components: {
QuickNav,
@ -149,22 +180,44 @@ import { Contact, ContactMethod } from "../db/tables/contacts";
},
})
export default class ContactEditView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Current route instance */
$route!: RouteLocationNormalizedLoaded;
/** Router instance for navigation */
$router!: Router;
/** Current contact data */
contact: Contact = {
did: "",
name: "",
notes: "",
};
/** Editable contact name field */
contactName = "";
/** Editable contact notes field */
contactNotes = "";
/** Array of editable contact methods */
contactMethods: Array<ContactMethod> = [];
/** Currently open dropdown index, null if none open */
dropdownIndex: number | null = null;
/** App string constants */
AppString = AppString;
/**
* Component lifecycle hook that initializes the contact edit form
*
* Workflow:
* 1. Extracts DID from route parameters
* 2. Queries database for existing contact
* 3. Populates form fields with contact data
* 4. Handles missing contact error case
*
* @throws Will not throw but redirects on error
* @emits Notification on contact not found
* @emits Router navigation on error
*/
async created() {
const contactDid = this.$route.params.did;
const contact = await db.contacts.get(contactDid || "");
@ -185,29 +238,75 @@ export default class ContactEditView extends Vue {
}
}
/**
* Adds a new empty contact method to the methods array
*
* Creates a new method object with empty fields for:
* - label: Custom label for the method
* - type: Communication type (CELL, EMAIL, WHATSAPP)
* - value: The contact information value
*/
addContactMethod() {
this.contactMethods.push({ label: "", type: "", value: "" });
}
/**
* Removes a contact method at the specified index
*
* @param index The array index of the method to remove
*/
removeContactMethod(index: number) {
this.contactMethods.splice(index, 1);
}
/**
* Toggles the type selection dropdown for a contact method
*
* If the clicked dropdown is already open, closes it.
* If another dropdown is open, closes it and opens the clicked one.
*
* @param index The array index of the method whose dropdown to toggle
*/
toggleDropdown(index: number) {
this.dropdownIndex = this.dropdownIndex === index ? null : index;
}
/**
* Sets the type for a contact method and closes the dropdown
*
* @param index The array index of the method to update
* @param type The new type value (CELL, EMAIL, WHATSAPP)
*/
setMethodType(index: number, type: string) {
this.contactMethods[index].type = type;
this.dropdownIndex = null;
}
/**
* Saves the edited contact information to the database
*
* Workflow:
* 1. Clones contact methods array to prevent reference issues
* 2. Normalizes method types to uppercase
* 3. Checks for changes in method types
* 4. Updates database with new values
* 5. Notifies user of success
* 6. Redirects to contact detail view
*
* @throws Will not throw but notifies on validation errors
* @emits Notification on type changes or success
* @emits Router navigation on success
*/
async saveEdit() {
// without this conversion, "Failed to execute 'put' on 'IDBObjectStore': [object Array] could not be cloned."
const contactMethodsObj = JSON.parse(JSON.stringify(this.contactMethods));
// Normalize method types to uppercase
const contactMethods = contactMethodsObj.map((method: ContactMethod) =>
R.set(R.lensProp("type"), method.type.toUpperCase(), method),
);
// Check for type changes
if (!R.equals(contactMethodsObj, contactMethods)) {
this.contactMethods = contactMethods;
this.$notify(
@ -221,11 +320,15 @@ export default class ContactEditView extends Vue {
);
return;
}
// Save to database
await db.contacts.update(this.contact.did, {
name: this.contactName,
notes: this.contactNotes,
contactMethods: contactMethods,
});
// Notify success and redirect
this.$notify({
group: "alert",
type: "success",

308
src/views/ContactImportView.vue

@ -3,10 +3,7 @@
<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>
@ -20,43 +17,28 @@
<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="
!contactsExisting[contact.did] ||
!R.isEmpty(contactDifferences[contact.did])
"
class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4"
>
<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">
<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">
@ -69,13 +51,9 @@
<div class="font-bold">Old Value</div>
<div class="font-bold">New Value</div>
</div>
<div
v-for="(value, contactField) in contactDifferences[
contact.did
]"
:key="contactField"
class="grid grid-cols-3 border"
>
<div v-for="(value, contactField) in contactDifferences[
contact.did
]" :key="contactField" class="grid grid-cols-3 border">
<div class="border font-bold p-1">
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
</div>
@ -88,8 +66,7 @@
</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>
@ -101,18 +78,10 @@
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>
@ -122,6 +91,77 @@
</template>
<script lang="ts">
/**
* @file Contact Import View Component
* @author Matthew Raymer
*
* This component handles the import of contacts into the TimeSafari app.
* It supports multiple import methods and handles duplicate detection,
* contact validation, and visibility settings.
*
* Import Methods:
* 1. Direct URL Query Parameters:
* Example: /contact-import?contacts=[{"did":"did:example:123","name":"Alice"}]
*
* 2. JWT in URL Path:
* Example: /contact-import/eyJhbGciOiJFUzI1NksifQ...
* - Supports both single and bulk imports
* - JWT payload can be either:
* a) Array format: { contacts: [{did: "...", name: "..."}, ...] }
* b) Single contact: { own: true, did: "...", name: "..." }
*
* 3. Manual JWT Input:
* - Accepts pasted JWT strings
* - Validates format and content before processing
*
* URL Examples:
* ```
* # Bulk import via query params
* /contact-import?contacts=[
* {"did":"did:example:123","name":"Alice"},
* {"did":"did:example:456","name":"Bob"}
* ]
*
* # Single contact via JWT
* /contact-import/eyJhbGciOiJFUzI1NksifQ.eyJvd24iOnRydWUsImRpZCI6ImRpZDpleGFtcGxlOjEyMyJ9...
*
* # Bulk import via JWT
* /contact-import/eyJhbGciOiJFUzI1NksifQ.eyJjb250YWN0cyI6W3siZGlkIjoiZGlkOmV4YW1wbGU6MTIzIn1dfQ...
*
* # Redirect to contacts page (single contact)
* /contacts?contactJwt=eyJhbGciOiJFUzI1NksifQ...
* ```
*
* Features:
* - Automatic duplicate detection
* - Field-by-field comparison for existing contacts
* - Batch visibility settings
* - Auto-import for single new contacts
* - Error handling and validation
*
* State Management:
* - Tracks existing contacts
* - Maintains selection state for bulk imports
* - Records differences for duplicate contacts
* - Manages visibility settings
*
* Security Considerations:
* - JWT validation for imported contacts
* - Visibility control per contact
* - Error handling for malformed data
*
* @example
* // Component usage in router
* {
* path: "/contact-import/:jwt?",
* name: "contact-import",
* component: ContactImportView
* }
*
* @see {@link Contact} for contact data structure
* @see {@link setVisibilityUtil} for visibility management
*/
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
@ -145,24 +185,75 @@ import {
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc";
/**
* Contact Import View Component
* @author Matthew Raymer
*
* This component handles the secure import of contacts into TimeSafari via JWT tokens.
* It supports both single and multiple contact imports with validation and duplicate detection.
*
* Import Workflows:
* 1. JWT in URL Path (/contact-import/[JWT])
* - Extracts JWT from path
* - Decodes and validates contact data
* - Handles both single and multiple contacts
*
* 2. JWT in Query Parameter (/contacts?contactJwt=[JWT])
* - Used for single contact redirects
* - Processes JWT from query parameter
* - Redirects to appropriate view
*
* JWT Payload Structure:
* ```json
* {
* "iat": 1740740453,
* "contacts": [{
* "did": "did:ethr:0x...",
* "name": "Optional Name",
* "nextPubKeyHashB64": "base64 string",
* "publicKeyBase64": "base64 string"
* }],
* "iss": "did:ethr:0x..."
* }
* ```
*
* Security Features:
* - JWT validation
* - Issuer verification
* - Duplicate detection
* - Contact data validation
*
* @component
*/
@Component({
components: { EntityIcon, OfferDialog, QuickNav },
})
export default class ContactImportView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Current route instance */
$route!: RouteLocationNormalizedLoaded;
/** Router instance for navigation */
$router!: Router;
// Constants
AppString = AppString;
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
libsUtil = libsUtil;
R = R;
// Component state
/** Active user's DID for authentication and visibility settings */
activeDid = "";
/** API server URL for backend communication */
apiServer = "";
contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID
contactsImporting: Array<Contact> = []; // contacts from the import
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected
/** Map of existing contacts keyed by DID for duplicate detection */
contactsExisting: Record<string, Contact> = {};
/** Array of contacts being imported from JWT */
contactsImporting: Array<Contact> = [];
/** Selection state for each importing contact */
contactsSelected: Array<boolean> = [];
/** Differences between existing and importing contacts */
contactDifferences: Record<
string,
Record<
@ -172,68 +263,117 @@ export default class ContactImportView extends Vue {
old: string | boolean | Array<ContactMethod> | undefined;
}
>
> = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key
> = {};
/** Loading state for import operations */
checkingImports = false;
/** JWT input for manual contact import */
inputJwt: string = "";
/** Visibility setting for imported contacts */
makeVisible = true;
/** Count of duplicate contacts found */
sameCount = 0;
/**
* Component lifecycle hook that initializes the contact import process
*
* This method handles three distinct import scenarios:
* 1. Query Parameter Import:
* - Checks for contacts in URL query parameters
* - Parses JSON array of contacts if present
*
* 2. JWT URL Import:
* - Extracts JWT from URL path using regex pattern '/contact-import/(ey.+)$'
* - Decodes JWT without validation (supports future-dated QR codes)
* - Handles two JWT payload formats:
* a. Array format: payload.contacts or direct array
* b. Single contact format: redirects to contacts page with JWT
*
* 3. Auto-Import Logic:
* - Automatically imports if exactly one new contact is present
* - Only triggers if no existing contacts match
*
* @throws Will not throw but logs errors during JWT processing
* @emits router.push when redirecting for single contact import
*/
async created() {
await this.initializeSettings();
await this.processQueryParams();
await this.processJwtFromPath();
await this.handleAutoImport();
}
/**
* Initializes component settings from active account
*/
private async initializeSettings() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
}
// look for any imported contact array from the query parameter
/**
* Processes contacts from URL query parameters
*/
private async processQueryParams() {
const importedContacts = this.$route.query["contacts"] as string;
if (importedContacts) {
await this.setContactsSelected(JSON.parse(importedContacts));
}
}
/**
* Processes JWT from URL path and handles different JWT formats
*/
private async processJwtFromPath() {
// JWT tokens always start with 'ey' (base64url encoded header)
const JWT_PATTERN = /\/contact-import\/(ey.+)$/;
const jwt = window.location.pathname.match(JWT_PATTERN)?.[1];
// look for a JWT after /contact-import/ in the window.location.pathname
const jwt = window.location.pathname.match(
/\/contact-import\/(ey.+)$/,
)?.[1];
if (jwt) {
// would prefer to validate but we've got an error with JWTs on QR codes generated in the future
// eslint-disable-next-line prettier/prettier
// const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt);
// decode the JWT
const parsedJwt = decodeEndorserJwt(jwt);
const contacts: Array<Contact> =
parsedJwt.payload.contacts || // someday this will be the only payload sent to this page
parsedJwt.payload.contacts ||
(Array.isArray(parsedJwt.payload) ? parsedJwt.payload : undefined);
if (!contacts && parsedJwt.payload.own) {
// handle this single-contact JWT in the contacts page, better suited to single additions
this.$router.push({
name: "contacts",
query: { contactJwt: jwt },
});
return;
}
if (contacts) {
await this.setContactsSelected(contacts);
} else {
// no contacts found so default message should be OK
}
}
}
/**
* Handles automatic import for single new contacts
*/
private async handleAutoImport() {
if (
this.contactsImporting.length === 1 &&
R.isEmpty(this.contactsExisting)
) {
// if there is only one contact and it's new, then we will automatically import it
this.contactsSelected[0] = true;
this.importContacts(); // ... which routes to the contacts list
await this.importContacts();
}
}
/**
* Processes contacts for import and checks for duplicates
* @param contacts Array of contacts to process
*/
async setContactsSelected(contacts: Array<Contact>) {
this.contactsImporting = contacts;
this.contactsSelected = new Array(this.contactsImporting.length).fill(true);
await db.open();
const baseContacts = await db.contacts.toArray();
// set the existing contacts, keyed by DID, if they exist in contactsImporting
// Check for existing contacts and differences
for (let i = 0; i < this.contactsImporting.length; i++) {
const contactIn = this.contactsImporting[i];
const existingContact = baseContacts.find(
@ -242,6 +382,7 @@ export default class ContactImportView extends Vue {
if (existingContact) {
this.contactsExisting[contactIn.did] = existingContact;
// Compare contact fields for differences
const differences: Record<
string,
{
@ -250,7 +391,6 @@ export default class ContactImportView extends Vue {
}
> = {};
Object.keys(contactIn).forEach((key) => {
// eslint-disable-next-line prettier/prettier
if (!R.equals(contactIn[key as keyof Contact], existingContact[key as keyof Contact])) {
differences[key] = {
old: existingContact[key as keyof Contact],
@ -263,13 +403,16 @@ export default class ContactImportView extends Vue {
this.sameCount++;
}
// don't automatically import previous data
// Don't auto-select duplicates
this.contactsSelected[i] = false;
}
}
}
// check the contact-import JWT
/**
* Validates contact import JWT format
* @param jwtInput JWT string to validate
*/
async checkContactJwt(jwtInput: string) {
if (
jwtInput.endsWith(APP_SERVER) ||
@ -289,14 +432,15 @@ export default class ContactImportView extends Vue {
}
}
// process the invite JWT and/or text message containing the URL with the JWT
/**
* Processes contact import JWT and updates contacts
* @param jwtInput JWT string containing contact data
*/
async processContactJwt(jwtInput: string) {
this.checkingImports = true;
try {
// (For another approach used with invites, see InviteOneAcceptView.processInvite)
const jwt: string = getContactJwtFromJwtUrl(jwtInput);
// JWT format: { header, payload, signature, data }
const payload = decodeEndorserJwt(jwt).payload;
if (Array.isArray(payload.contacts)) {
@ -320,10 +464,16 @@ export default class ContactImportView extends Vue {
this.checkingImports = false;
}
/**
* Imports selected contacts and sets visibility if requested
* Updates existing contacts or adds new ones
*/
async importContacts() {
this.checkingImports = true;
let importedCount = 0,
updatedCount = 0;
// Process selected contacts
for (let i = 0; i < this.contactsImporting.length; i++) {
if (this.contactsSelected[i]) {
const contact = this.contactsImporting[i];
@ -339,6 +489,8 @@ export default class ContactImportView extends Vue {
}
}
}
// Set visibility if requested
if (this.makeVisible) {
const failedVisibileToContacts = [];
for (let i = 0; i < this.contactsImporting.length; i++) {
@ -365,9 +517,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"
}. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
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,
);
@ -376,6 +527,7 @@ export default class ContactImportView extends Vue {
this.checkingImports = false;
// Show success notification
this.$notify(
{
group: "alert",

193
src/views/ProjectViewView.vue

@ -568,6 +568,37 @@ import * as serverUtil from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
/**
* Project View Component
* @author Matthew Raymer
*
* This component displays detailed project information and manages interactions including:
* - Project metadata (name, description, dates, location)
* - Issuer information and verification
* - Project contributions and fulfillments
* - Offers and gifts tracking
* - Contact interactions
*
* Data Flow:
* 1. Component loads with project ID from route
* 2. Fetches project data, contacts, and account settings
* 3. Loads related data (offers, gifts, fulfillments)
* 4. Updates UI with paginated results
*
* Security Features:
* - DID visibility controls
* - JWT validation for imports
* - Permission checks for actions
*
* State Management:
* - Maintains separate loading states for different data types
* - Handles pagination limits
* - Tracks confirmation states
*
* @see GiftedDialog for gift creation
* @see OfferDialog for offer creation
* @see HiddenDidDialog for DID privacy explanations
*/
@Component({
components: {
EntityIcon,
@ -580,49 +611,103 @@ import HiddenDidDialog from "../components/HiddenDidDialog.vue";
},
})
export default class ProjectViewView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Router instance for navigation */
$router!: Router;
// Account and Settings State
/** Currently active DID */
activeDid = "";
/** Project agent DID */
agentDid = "";
/** DIDs that can see the agent DID */
agentDidVisibleToDids: Array<string> = [];
/** All DIDs associated with current account */
allMyDids: Array<string> = [];
/** All known contacts */
allContacts: Array<Contact> = [];
/** API server endpoint */
apiServer = "";
checkingConfirmationForJwtId = "";
/** Registration status of current user */
isRegistered = false;
// Project Data
/** Project description */
description = "";
/** Project end time */
endTime = "";
/** Text expansion state */
expanded = false;
/** Project fulfilled by this project */
fulfilledByThis: PlanSummaryRecord | null = null;
/** Projects fulfilling this project */
fulfillersToThis: Array<PlanSummaryRecord> = [];
/** Flag for fulfiller pagination */
fulfillersToHitLimit = false;
givesToThis: Array<GiveSummaryRecord> = [];
givesHitLimit = false;
givesProvidedByThis: Array<GiveSummaryRecord> = [];
givesProvidedByHitLimit = false;
/** Project image URL */
imageUrl = "";
isRegistered = false;
/** Project issuer DID */
issuer = "";
/** Cached issuer information */
issuerInfoObject: {
known: boolean;
displayName: string;
profileImageUrl?: string;
} | null = null;
/** DIDs that can see issuer information */
issuerVisibleToDids: Array<string> = [];
/** Project location data */
latitude = 0;
longitude = 0;
/** Project name */
name = "";
/** Project ID (handle) */
projectId = "";
/** Project start time */
startTime = "";
/** Project URL */
url = "";
// Interaction Data
/** Gifts to this project */
givesToThis: Array<GiveSummaryRecord> = [];
/** Flag for gifts pagination */
givesHitLimit = false;
/** Gifts from this project */
givesProvidedByThis: Array<GiveSummaryRecord> = [];
/** Flag for provided gifts pagination */
givesProvidedByHitLimit = false;
/** Offers to this project */
offersToThis: Array<OfferSummaryRecord> = [];
/** Flag for offers pagination */
offersHitLimit = false;
projectId = ""; // handle ID
// UI State
/** JWT being checked for confirmation */
checkingConfirmationForJwtId = "";
/** Recently checked unconfirmable JWTs */
recentlyCheckedAndUnconfirmableJwts: string[] = [];
startTime = "";
truncatedDesc = "";
/** Truncation length */
truncateLength = 40;
url = "";
// Utility References
libsUtil = libsUtil;
serverUtil = serverUtil;
/**
* Component lifecycle hook that initializes the project view
*
* Workflow:
* 1. Loads account settings and contacts
* 2. Retrieves all account DIDs
* 3. Extracts project ID from URL
* 4. Initializes project data loading
*
* @throws Logs errors but continues loading
* @emits Notification on profile loading errors
*/
async created() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
@ -656,23 +741,19 @@ export default class ProjectViewView extends Vue {
this.loadProject(this.projectId, this.activeDid);
}
onEditClick() {
const route = {
name: "new-edit-project",
query: { projectId: this.projectId },
};
this.$router.push(route);
}
// Isn't there a better way to make this available to the template?
expandText() {
this.expanded = true;
}
collapseText() {
this.expanded = false;
}
/**
* Loads project data and related information
*
* Workflow:
* 1. Fetches project details from API
* 2. Updates component state with project data
* 3. Initializes related data loading (gifts, offers, fulfillments)
*
* @param projectId Project handle ID
* @param userDid Active user's DID
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadProject(projectId: string, userDid: string) {
this.projectId = projectId;
@ -759,6 +840,15 @@ export default class ProjectViewView extends Vue {
this.loadPlanFulfilledBy();
}
/**
* Loads gifts made to this project
*
* Handles pagination and updates component state with results.
* Uses beforeId for pagination based on last loaded gift.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadGives() {
const givesUrl =
this.apiServer +
@ -806,6 +896,15 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads gifts provided by this project
*
* Similar to loadGives but for outgoing gifts.
* Maintains separate pagination state.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadGivesProvidedBy() {
const providedByUrl =
this.apiServer +
@ -856,6 +955,15 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads offers made to this project
*
* Handles pagination and filtering of valid offers.
* Updates component state with results.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadOffers() {
const offersUrl =
this.apiServer +
@ -903,6 +1011,14 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads projects that fulfill this project
*
* Manages pagination state and updates component with results.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadPlanFulfillersTo() {
const fulfillsUrl =
this.apiServer +
@ -951,6 +1067,14 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads project that this project fulfills
*
* Updates fulfilledByThis state with result.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadPlanFulfilledBy() {
const fulfilledByUrl =
this.apiServer +
@ -990,6 +1114,23 @@ export default class ProjectViewView extends Vue {
}
}
onEditClick() {
const route = {
name: "new-edit-project",
query: { projectId: this.projectId },
};
this.$router.push(route);
}
// Isn't there a better way to make this available to the template?
expandText() {
this.expanded = true;
}
collapseText() {
this.expanded = false;
}
/**
* Handle clicking on a project entry found in the list
* @param id of the project

55
test-deeplinks.sh

@ -8,6 +8,7 @@ MANUAL_CONTINUE=true
test_link() {
echo "----------------------------------------"
echo "Testing: $1"
echo "Description: $2"
echo "----------------------------------------"
adb shell am start -W -a android.intent.action.VIEW -d "$1" app.timesafari.app
@ -31,20 +32,55 @@ echo "======================================"
echo "Pause duration: $PAUSE_DURATION seconds"
echo "Manual continue: $MANUAL_CONTINUE"
# Contact Import Routes
echo "\nTesting Contact Import Routes:"
# 1. Direct Query Parameter Import (URL-encoded JSON)
QUERY_CONTACTS='[{
"did":"did:ethr:0xf969A5DeE7a5806d1C37f4Ec49A555Ab97911089"
},{
"did":"did:ethr:0xFEd3b416946b23F3F472799053144B4E34155B5b",
"name":"Jordan",
"nextPubKeyHashB64":"IBfRZfwdzeKOzqCx8b+WlLpMJHOAT9ZknIDJo7F3rZE=",
"publicKeyBase64":"A1eIndfaxgMpVwyD5dYe74DgjuIo5SwPZFCcLdOemjf"
}]'
ENCODED_CONTACTS=$(echo $QUERY_CONTACTS | jq -c | python3 -c "import urllib.parse; print(urllib.parse.quote(input()))")
test_link "timesafari://contact-import?contacts=$ENCODED_CONTACTS" "Bulk import via query parameters"
# 2. JWT Path Imports
# Original JWT with multiple contacts
BULK_JWT="eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJpYXQiOjE3NDA3NDA0NTMsImNvbnRhY3RzIjpbeyJkaWQiOiJkaWQ6ZXRocjoweGY5NjlBNURlRTdhNTgwNmQxQzM3ZjRFYzQ5QTU1NUFiOTc5MTEwODkifSx7ImRpZCI6ImRpZDpldGhyOjB4RkVkM2I0MTY5NDZiMjNGM0Y0NzI3OTkwNTMxNDRCNEUzNDE1NUI1YiIsIm5hbWUiOiJKb3JkYW4iLCJuZXh0UHViS2V5SGFzaEI2NCI6IklCZlJaZndkemVLT3pxQ3g4YitXbExwTUpIT0FUOVprbklESm83RjNyWkU9IiwicHVibGljS2V5QmFzZTY0IjoiQTFlSW5kZmF4Z01wVnd5RDVkWWU3NERnanQ5SW81U3dQWkZDY0xkT2VtamYifV0sImlzcyI6ImRpZDpldGhyOjB4RDUzMTE0ODMwRDRhNUQ5MDQxNkI0M0ZjOTlhMjViMGRGOGJiMUJBZCJ9.yKEFounxUGU9-grAMFHA12dif9BKYkftg8F3wAIcFYh0H_k1tevjEYyD1fvAyIxYxK5xR0E8moqMhi78ipJXcg"
test_link "timesafari://contact-import/$BULK_JWT" "Multiple contacts via JWT"
# 3. Contact Page JWT Redirect
test_link "timesafari://contacts?contactJwt=$BULK_JWT" "Multiple contacts redirect"
# Contact Management Routes
test_link "timesafari://contact-edit/did:ethr:0xf969A5DeE7a5806d1C37f4Ec49A555Ab97911089" \
"Edit first contact"
# Error Cases
echo "\nTesting Contact Import Error Cases:"
test_link "timesafari://contact-import/eyJJTlZBTElEIn0" "Invalid JWT format"
test_link "timesafari://contact-import?contacts=[{invalid:data}]" "Invalid contact data"
# Original Routes (preserved from previous version)
echo "\nTesting Other Routes:"
# Test claim routes
echo "\nTesting Claim Routes:"
test_link "timesafari://claim/01JMAAFZRNSRTQ0EBSD70A8E1H"
test_link "timesafari://claim/01JMAAFZRNSRTQ0EBSD70A8E1H?view=details"
test_link "timesafari://claim-cert/01JMAAFZRNSRTQ0EBSD70A8E1H"
# Test contact routes
echo "\nTesting Contact Routes:"
test_link "timesafari://contact-import/eyJhbGciOiJFUzI1NksifQ"
test_link "timesafari://contact-edit/did:example:123"
echo "\nTesting Additional Claim Routes:"
test_link "timesafari://claim/123?view=certificate"
test_link "timesafari://claim/123?view=raw"
test_link "timesafari://claim-add-raw/123?claimJwtId=jwt123"
# Test project routes
echo "\nTesting Project Routes:"
test_link "timesafari://project/456?view=details"
test_link "timesafari://project/https%3A%2F%2Fendorser.ch%2Fentity%2F01JKW0QZX1XVCVZV85VXAMB31R"
# Test invite routes
echo "\nTesting Invite Routes:"
@ -67,11 +103,6 @@ echo "\nTesting DID Routes:"
test_link "timesafari://did/did:example:123"
test_link "timesafari://did/did:example:456?view=details"
echo "\nTesting Additional Claim Routes:"
test_link "timesafari://claim/123?view=certificate"
test_link "timesafari://claim/123?view=raw"
test_link "timesafari://claim-add-raw/123?claimJwtId=jwt123"
echo "\nTesting Additional Contact Routes:"
test_link "timesafari://contact-import/jwt?contacts=%5B%7B%22did%22%3A%22did%3Aexample%3A123%22%7D%5D"
test_link "timesafari://contact-edit/did:example:123?action=edit"
@ -81,9 +112,5 @@ test_link "timesafari://invalid-route/123"
test_link "timesafari://claim/123?view=invalid"
test_link "timesafari://did/invalid-did"
# Test contact import one route
echo "\nTesting Contact Import One Route:"
test_link "timesafari://contacts?contactJwt=eyJhbGciOiJFUzI1NksifQ"
echo "\nDeep link testing complete"
echo "======================================"
Loading…
Cancel
Save