Browse Source

Merge branch 'build-improvement' into performance-optimizations-testing

pull/159/head
Matthew Raymer 2 weeks ago
parent
commit
12dd69e8bd
  1. 33
      src/components/ActivityListItem.vue
  2. 5
      src/components/EntitySelectionStep.vue
  3. 13
      src/components/GiftDetailsStep.vue
  4. 62
      src/components/GiftedDialog.vue
  5. 28
      src/views/ContactGiftingView.vue
  6. 8
      src/views/ContactQRScanShowView.vue
  7. 33
      src/views/DIDView.vue
  8. 39
      src/views/GiftedDetailsView.vue
  9. 123
      src/views/HomeView.vue
  10. 294
      src/views/OnboardMeetingListView.vue
  11. 6
      src/views/ShareMyContactInfoView.vue

33
src/components/ActivityListItem.vue

@ -251,8 +251,7 @@
import { Component, Prop, Vue, Emit } from "vue-facing-decorator"; import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "@/interfaces/give"; import { GiveRecordWithContactInfo } from "@/interfaces/give";
import EntityIcon from "./EntityIcon.vue"; import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util"; import { isHiddenDid } from "../libs/endorserServer";
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue"; import ProjectIcon from "./ProjectIcon.vue";
import { createNotifyHelpers } from "@/utils/notify"; import { createNotifyHelpers } from "@/utils/notify";
import { import {
@ -272,7 +271,6 @@ export default class ActivityListItem extends Vue {
@Prop() lastViewedClaimId?: string; @Prop() lastViewedClaimId?: string;
@Prop() isRegistered!: boolean; @Prop() isRegistered!: boolean;
@Prop() activeDid!: string; @Prop() activeDid!: string;
@Prop() confirmerIdList?: string[];
/** /**
* Function prop for handling image caching * Function prop for handling image caching
@ -331,15 +329,6 @@ export default class ActivityListItem extends Vue {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode; return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
} }
get canConfirm(): boolean {
if (!this.isRegistered) return false;
if (!isGiveClaimType(this.record.fullClaim?.["@type"])) return false;
if (this.confirmerIdList?.includes(this.activeDid)) return false;
if (this.record.issuerDid === this.activeDid) return false;
if (containsHiddenDid(this.record.fullClaim)) return false;
return true;
}
// Emit methods using @Emit decorator // Emit methods using @Emit decorator
@Emit("viewImage") @Emit("viewImage")
emitViewImage(imageUrl: string) { emitViewImage(imageUrl: string) {
@ -351,26 +340,6 @@ export default class ActivityListItem extends Vue {
return jwtId; return jwtId;
} }
@Emit("confirmClaim")
emitConfirmClaim() {
if (!this.canConfirm) {
notifyWhyCannotConfirm(
(msg, timeout) => this.notify.info(msg.text ?? "", timeout),
this.isRegistered,
this.record.fullClaim?.["@type"],
this.record,
this.activeDid,
this.confirmerIdList,
);
return;
}
return this.record;
}
handleConfirmClick() {
this.emitConfirmClaim();
}
get friendlyDate(): string { get friendlyDate(): string {
const date = new Date(this.record.issuedAt); const date = new Date(this.record.issuedAt);
return date.toLocaleDateString(undefined, { return date.toLocaleDateString(undefined, {

5
src/components/EntitySelectionStep.vue

@ -261,12 +261,13 @@ export default class EntitySelectionStep extends Vue {
giverProjectName: this.giver?.name || "", giverProjectName: this.giver?.name || "",
giverProjectImage: this.giver?.image || "", giverProjectImage: this.giver?.image || "",
giverProjectHandleId: this.giver?.handleId || "", giverProjectHandleId: this.giver?.handleId || "",
giverDid: this.giver?.did || "", giverDid: this.giverEntityType === "person" ? this.giver?.did || "" : "",
recipientProjectId: this.toProjectId || "", recipientProjectId: this.toProjectId || "",
recipientProjectName: this.receiver?.name || "", recipientProjectName: this.receiver?.name || "",
recipientProjectImage: this.receiver?.image || "", recipientProjectImage: this.receiver?.image || "",
recipientProjectHandleId: this.receiver?.handleId || "", recipientProjectHandleId: this.receiver?.handleId || "",
recipientDid: this.receiver?.did || "", recipientDid:
this.recipientEntityType === "person" ? this.receiver?.did || "" : "",
}; };
} }

13
src/components/GiftDetailsStep.vue

@ -315,16 +315,15 @@ export default class GiftDetailsStep extends Vue {
giverName: this.giver?.name, giverName: this.giver?.name,
offerId: this.offerId, offerId: this.offerId,
fulfillsProjectId: fulfillsProjectId:
this.giverEntityType === "person" && this.recipientEntityType === "project" ? this.toProjectId : undefined,
this.recipientEntityType === "project"
? this.toProjectId
: undefined,
providerProjectId: providerProjectId:
this.giverEntityType === "project" && this.giverEntityType === "project"
this.recipientEntityType === "person"
? this.giver?.handleId ? this.giver?.handleId
: this.fromProjectId, : this.fromProjectId,
recipientDid: this.receiver?.did, recipientDid:
this.recipientEntityType === "person"
? this.receiver?.did
: undefined,
recipientName: this.receiver?.name, recipientName: this.receiver?.name,
unitCode: this.localUnitCode, unitCode: this.localUnitCode,
}, },

62
src/components/GiftedDialog.vue

@ -81,6 +81,12 @@ import GiftDetailsStep from "../components/GiftDetailsStep.vue";
import { PlanData } from "../interfaces/records"; import { PlanData } from "../interfaces/records";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import {
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
} from "@/constants/notifications";
@Component({ @Component({
components: { components: {
@ -288,23 +294,24 @@ export default class GiftedDialog extends Vue {
async confirm() { async confirm() {
if (!this.activeDid) { if (!this.activeDid) {
this.safeNotify.error( this.safeNotify.error(
"You must select an identifier before you can record a give.", NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message,
TIMEOUTS.STANDARD, TIMEOUTS.SHORT,
); );
return; return;
} }
if (parseFloat(this.amountInput) < 0) { if (parseFloat(this.amountInput) < 0) {
this.safeNotify.error( this.safeNotify.error(
"You may not send a negative number.", NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT.message,
TIMEOUTS.SHORT, TIMEOUTS.SHORT,
); );
return; return;
} }
if (!this.description && !parseFloat(this.amountInput)) { if (!this.description && !parseFloat(this.amountInput)) {
this.safeNotify.error( this.safeNotify.error(
`You must enter a description or some number of ${ NOTIFY_GIFT_ERROR_NO_DESCRIPTION.message.replace(
this.libsUtil.UNIT_LONG[this.unitCode] "{unit}",
}.`, this.libsUtil.UNIT_SHORT[this.unitCode] || this.unitCode,
),
TIMEOUTS.SHORT, TIMEOUTS.SHORT,
); );
return; return;
@ -320,7 +327,11 @@ export default class GiftedDialog extends Vue {
} }
this.close(); this.close();
this.safeNotify.toast("Recording the give...", undefined, TIMEOUTS.BRIEF); this.safeNotify.toast(
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message,
undefined,
TIMEOUTS.BRIEF,
);
// this is asynchronous, but we don't need to wait for it to complete // this is asynchronous, but we don't need to wait for it to complete
await this.recordGive( await this.recordGive(
(this.giver?.did as string) || null, (this.giver?.did as string) || null,
@ -447,10 +458,13 @@ export default class GiftedDialog extends Vue {
name: contact.name || contact.did, name: contact.name || contact.did,
}; };
} else { } else {
this.giver = { // Only set to "Unnamed" if no giver is currently set
did: "", if (!this.giver || !this.giver.did) {
name: "Unnamed", this.giver = {
}; did: "",
name: "Unnamed",
};
}
} }
this.firstStep = false; this.firstStep = false;
} }
@ -460,6 +474,10 @@ export default class GiftedDialog extends Vue {
this.firstStep = true; this.firstStep = true;
} }
moveToStep2() {
this.firstStep = false;
}
async loadProjects() { async loadProjects() {
try { try {
const response = await fetch(this.apiServer + "/api/v2/report/plans", { const response = await fetch(this.apiServer + "/api/v2/report/plans", {
@ -502,10 +520,13 @@ export default class GiftedDialog extends Vue {
name: contact.name || contact.did, name: contact.name || contact.did,
}; };
} else { } else {
this.receiver = { // Only set to "Unnamed" if no receiver is currently set
did: "", if (!this.receiver || !this.receiver.did) {
name: "Unnamed", this.receiver = {
}; did: "",
name: "Unnamed",
};
}
} }
this.firstStep = false; this.firstStep = false;
} }
@ -529,16 +550,13 @@ export default class GiftedDialog extends Vue {
giverName: this.giver?.name, giverName: this.giver?.name,
offerId: this.offerId, offerId: this.offerId,
fulfillsProjectId: fulfillsProjectId:
this.giverEntityType === "person" && this.recipientEntityType === "project" ? this.toProjectId : undefined,
this.recipientEntityType === "project"
? this.toProjectId
: undefined,
providerProjectId: providerProjectId:
this.giverEntityType === "project" && this.giverEntityType === "project"
this.recipientEntityType === "person"
? this.giver?.handleId ? this.giver?.handleId
: this.fromProjectId, : this.fromProjectId,
recipientDid: this.receiver?.did, recipientDid:
this.recipientEntityType === "person" ? this.receiver?.did : undefined,
recipientName: this.receiver?.name, recipientName: this.receiver?.name,
unitCode: this.unitCode, unitCode: this.unitCode,
}; };

28
src/views/ContactGiftingView.vue

@ -195,7 +195,7 @@ export default class ContactGiftingView extends Vue {
let giver: GiverReceiverInputInfo | undefined; let giver: GiverReceiverInputInfo | undefined;
if (this.stepType === "giver") { if (this.stepType === "giver") {
// We're selecting a giver, so recipient is either a project or the current user // We're selecting a giver, so preserve the existing recipient from context
if (this.recipientEntityType === "project") { if (this.recipientEntityType === "project") {
recipient = { recipient = {
did: this.recipientProjectHandleId, did: this.recipientProjectHandleId,
@ -204,7 +204,20 @@ export default class ContactGiftingView extends Vue {
handleId: this.recipientProjectHandleId, handleId: this.recipientProjectHandleId,
}; };
} else { } else {
recipient = { did: this.activeDid, name: "You" }; // Preserve the existing recipient from context
if (this.recipientDid === this.activeDid) {
// Recipient was "You"
recipient = { did: this.activeDid, name: "You" };
} else if (this.recipientDid) {
// Recipient was a regular contact
recipient = {
did: this.recipientDid,
name: this.recipientProjectName || "Someone",
};
} else {
// Fallback to "You" if no recipient was previously selected
recipient = { did: this.activeDid, name: "You" };
}
} }
giver = undefined; // Will be set to "Unnamed" in GiftedDialog giver = undefined; // Will be set to "Unnamed" in GiftedDialog
} else { } else {
@ -239,12 +252,8 @@ export default class ContactGiftingView extends Vue {
this.unitCode, this.unitCode,
); );
// Immediately select "Unnamed" and move to Step 2 based on stepType // Move to Step 2 - entities are already set by the open() call
if (this.stepType === "giver") { (this.$refs.giftedDialog as GiftedDialog).moveToStep2();
(this.$refs.giftedDialog as GiftedDialog).selectGiver();
} else {
(this.$refs.giftedDialog as GiftedDialog).selectRecipient();
}
} else { } else {
// Regular case: contact is a GiverReceiverInputInfo // Regular case: contact is a GiverReceiverInputInfo
let giver: GiverReceiverInputInfo; let giver: GiverReceiverInputInfo;
@ -317,6 +326,9 @@ export default class ContactGiftingView extends Vue {
this.amountInput, this.amountInput,
this.unitCode, this.unitCode,
); );
// Move to Step 2 - entities are already set by the open() call
(this.$refs.giftedDialog as GiftedDialog).moveToStep2();
} }
} }
} }

8
src/views/ContactQRScanShowView.vue

@ -213,15 +213,11 @@ export default class ContactQRScanShow extends Vue {
$router!: Router; $router!: Router;
// Notification helper system // Notification helper system
private notify = createNotifyHelpers(this.$notify); notify!: ReturnType<typeof createNotifyHelpers>;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
// Axios instance for API calls
get axios() {
return (this as any).$platformService.axios;
}
givenName = ""; givenName = "";
hideRegisterPromptOnNewContact = false; hideRegisterPromptOnNewContact = false;
isRegistered = false; isRegistered = false;
@ -288,6 +284,8 @@ export default class ContactQRScanShow extends Vue {
} }
async created() { async created() {
this.notify = createNotifyHelpers(this.$notify);
try { try {
const settings = await this.$accountSettings(); const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";

33
src/views/DIDView.vue

@ -273,6 +273,7 @@ import {
displayAmount, displayAmount,
getHeaders, getHeaders,
register, register,
setVisibilityUtil,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import EntityIcon from "../components/EntityIcon.vue"; import EntityIcon from "../components/EntityIcon.vue";
@ -324,6 +325,7 @@ export default class DIDView extends Vue {
apiServer = ""; apiServer = "";
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = []; claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
contactFromDid?: Contact; contactFromDid?: Contact;
contactYaml = ""; contactYaml = "";
hitEnd = false; hitEnd = false;
isLoading = false; isLoading = false;
@ -722,18 +724,31 @@ export default class DIDView extends Vue {
visibility: boolean, visibility: boolean,
showSuccessAlert: boolean, showSuccessAlert: boolean,
) { ) {
// Update contact visibility using mixin method const result = await setVisibilityUtil(
await this.$updateContact(contact.did, { seesMe: visibility }); this.activeDid,
this.apiServer,
this.axios,
contact,
visibility,
);
if (showSuccessAlert) { if (result.success) {
if (showSuccessAlert) {
const message =
(contact.name || "That user") +
" can " +
(visibility ? "" : "not ") +
"see your activity.";
this.notify.success(message, TIMEOUTS.SHORT);
}
return true;
} else {
logger.error("Got strange result from setting visibility:", result);
const message = const message =
(contact.name || "That user") + (result.error as string) || "Could not set visibility on the server.";
" can " + this.notify.error(message, TIMEOUTS.LONG);
(visibility ? "" : "not ") + return false;
"see your activity.";
this.notify.success(message, TIMEOUTS.SHORT);
} }
return true;
} }
/** /**

39
src/views/GiftedDetailsView.vue

@ -48,24 +48,12 @@
placeholder="What was received" placeholder="What was received"
/> />
<div class="flex mb-4"> <div class="flex mb-4">
<button <AmountInput
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2" :value="parseFloat(amountInput) || 0"
@click="amountInput === '0' ? null : decrement()" :min="0"
> input-id="inputGivenAmount"
<font-awesome icon="chevron-left" /> :on-update-value="handleAmountChange"
</button>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
/> />
<button
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
</button>
<select <select
v-model="unitCode" v-model="unitCode"
@ -275,6 +263,7 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import ImageMethodDialog from "../components/ImageMethodDialog.vue"; import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import QuickNav from "../components/QuickNav.vue"; import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue"; import TopMessage from "../components/TopMessage.vue";
import AmountInput from "../components/AmountInput.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import { GenericCredWrapper, GiveActionClaim } from "../interfaces"; import { GenericCredWrapper, GiveActionClaim } from "../interfaces";
import { import {
@ -296,9 +285,11 @@ import {
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM, NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR, NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR,
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER, NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER,
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE, NOTIFY_GIFTED_DETAILS_RECORDING_GIVE,
NOTIFY_GIFTED_DETAILS_CREATE_GIVE_ERROR, NOTIFY_GIFTED_DETAILS_CREATE_GIVE_ERROR,
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED, NOTIFY_GIFTED_DETAILS_GIFT_RECORDED,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
} from "@/constants/notifications"; } from "@/constants/notifications";
@Component({ @Component({
@ -306,6 +297,7 @@ import {
ImageMethodDialog, ImageMethodDialog,
QuickNav, QuickNav,
TopMessage, TopMessage,
AmountInput,
}, },
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
@ -528,6 +520,10 @@ export default class GiftedDetails extends Vue {
)}`; )}`;
} }
handleAmountChange(value: number): void {
this.amountInput = value.toString();
}
cancel() { cancel() {
this.deleteImage(); // not awaiting, so they'll go back immediately this.deleteImage(); // not awaiting, so they'll go back immediately
if (this.destinationPathAfter) { if (this.destinationPathAfter) {
@ -609,14 +605,17 @@ export default class GiftedDetails extends Vue {
} }
if (parseFloat(this.amountInput) < 0) { if (parseFloat(this.amountInput) < 0) {
this.notify.error( this.notify.error(
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message, NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT.message,
TIMEOUTS.SHORT, TIMEOUTS.SHORT,
); );
return; return;
} }
if (!this.description && !parseFloat(this.amountInput)) { if (!this.description && !parseFloat(this.amountInput)) {
this.notify.error( this.notify.error(
NOTIFY_GIFTED_DETAILS_NO_IDENTIFIER.message, NOTIFY_GIFT_ERROR_NO_DESCRIPTION.message.replace(
"{unit}",
this.libsUtil.UNIT_SHORT[this.unitCode] || this.unitCode,
),
TIMEOUTS.SHORT, TIMEOUTS.SHORT,
); );
return; return;
@ -625,7 +624,7 @@ export default class GiftedDetails extends Vue {
this.notify.toast( this.notify.toast(
NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message, NOTIFY_GIFTED_DETAILS_RECORDING_GIVE.message,
undefined, undefined,
TIMEOUTS.SHORT, TIMEOUTS.BRIEF,
); );
// this is asynchronous, but we don't need to wait for it to complete // this is asynchronous, but we don't need to wait for it to complete

123
src/views/HomeView.vue

@ -249,11 +249,9 @@ Raymer * @version 1.0.0 */
:last-viewed-claim-id="feedLastViewedClaimId" :last-viewed-claim-id="feedLastViewedClaimId"
:is-registered="isUserRegistered" :is-registered="isUserRegistered"
:active-did="activeDid" :active-did="activeDid"
:confirmer-id-list="record.confirmerIdList"
:on-image-cache="cacheImageData" :on-image-cache="cacheImageData"
@load-claim="onClickLoadClaim" @load-claim="onClickLoadClaim"
@view-image="openImageViewer" @view-image="openImageViewer"
@confirm-claim="confirmClaim"
/> />
</ul> </ul>
</InfiniteScroll> </InfiniteScroll>
@ -327,16 +325,12 @@ import {
GiverReceiverInputInfo, GiverReceiverInputInfo,
OnboardPage, OnboardPage,
} from "../libs/util"; } from "../libs/util";
import { GiveSummaryRecord, PlanSummaryRecord } from "../interfaces/records"; import { GiveSummaryRecord } from "../interfaces/records";
import * as serverUtil from "../libs/endorserServer";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "../interfaces/give"; import { GiveRecordWithContactInfo } from "../interfaces/give";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
NOTIFY_CONTACT_LOADING_ISSUE,
NOTIFY_CONFIRMATION_ERROR,
} from "@/constants/notifications";
import * as Package from "../../package.json"; import * as Package from "../../package.json";
// consolidate this with GiveActionClaim in src/interfaces/claims.ts // consolidate this with GiveActionClaim in src/interfaces/claims.ts
@ -664,42 +658,6 @@ export default class HomeView extends Vue {
} }
} }
/**
* Loads user settings from database using ultra-concise mixin
* Used for displaying settings in feed and actions
*
* @internal
* Called by mounted() and reloadFeedOnChange()
*/
private async loadSettings() {
// Use the current activeDid (set in initializeIdentity) to get user-specific settings
const settings = await this.$accountSettings(this.activeDid, {
apiServer: "",
activeDid: "",
filterFeedByVisible: false,
filterFeedByNearby: false,
isRegistered: false,
});
this.apiServer = settings.apiServer || "";
// **CRITICAL**: Ensure correct API server for platform
await this.ensureCorrectApiServer();
this.activeDid = settings.activeDid || "";
this.feedLastViewedClaimId = settings.lastViewedClaimId;
this.givenName = settings.firstName || "";
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isRegistered = !!settings.isRegistered;
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId;
this.searchBoxes = settings.searchBoxes || [];
this.showShortcutBvc = !!settings.showShortcutBvc;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
}
/** /**
* Loads user contacts from database using ultra-concise mixin * Loads user contacts from database using ultra-concise mixin
* Used for displaying contact info in feed and actions * Used for displaying contact info in feed and actions
@ -714,36 +672,6 @@ export default class HomeView extends Vue {
.map((c) => c.did); .map((c) => c.did);
} }
/**
* Verifies user registration status with endorser service
* - Checks if unregistered user can access API
* - Updates registration status if successful
* - Preserves unregistered state on failure
*
* @internal
* Called by mounted() and initializeIdentity()
*/
private async checkRegistrationStatus() {
if (!this.isRegistered && this.activeDid) {
try {
const resp = await fetchEndorserRateLimits(
this.apiServer,
this.axios,
this.activeDid,
);
if (resp.status === 200) {
// Ultra-concise settings update with automatic cache invalidation!
await this.$saveMySettings({ isRegistered: true });
this.isRegistered = true;
// Force Vue to re-render the template
await this.$nextTick();
}
} catch (e) {
// ignore the error... just keep us unregistered
}
}
}
/** /**
* Initializes feed data * Initializes feed data
* Triggers updateAllFeed() to populate activity feed * Triggers updateAllFeed() to populate activity feed
@ -2074,53 +2002,6 @@ export default class HomeView extends Vue {
this.isImageViewerOpen = true; this.isImageViewerOpen = true;
} }
/**
* Handles claim confirmation
*
* @public
* Called by ActivityListItem component
* @param record Record to confirm
*/
async confirmClaim(record: GiveRecordWithContactInfo) {
this.notify.confirm(
"Do you personally confirm that this is true?",
async () => {
const goodClaim = serverUtil.removeSchemaContext(
serverUtil.removeVisibleToDids(
serverUtil.addLastClaimOrHandleAsIdIfMissing(
record.fullClaim,
record.jwtId,
record.handleId,
),
),
);
const confirmationClaim = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
};
const result = await serverUtil.createAndSubmitClaim(
confirmationClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (result.success) {
this.notify.confirmationSubmitted();
// Refresh the feed to show updated confirmation status
await this.updateAllFeed();
} else {
logger.error("Error submitting confirmation:", result);
this.notify.error(NOTIFY_CONFIRMATION_ERROR.message, TIMEOUTS.LONG);
}
},
);
}
private handleQRCodeClick() { private handleQRCodeClick() {
if (Capacitor.isNativePlatform()) { if (Capacitor.isNativePlatform()) {
this.$router.push({ name: "contact-qr-scan-full" }); this.$router.push({ name: "contact-qr-scan-full" });

294
src/views/OnboardMeetingListView.vue

@ -115,6 +115,9 @@ import {
serverMessageForUser, serverMessageForUser,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { logger } from "@/utils/logger";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NotificationIface } from "@/constants/app";
interface Meeting { interface Meeting {
name: string; name: string;
@ -129,19 +132,11 @@ interface Meeting {
mixins: [PlatformServiceMixin], mixins: [PlatformServiceMixin],
}) })
export default class OnboardMeetingListView extends Vue { export default class OnboardMeetingListView extends Vue {
$notify!: ( $notify!: (notification: NotificationIface, timeout?: number) => void;
notification: {
group: string;
type: string;
title: string;
text: string;
onYes?: () => void;
yesText?: string;
},
timeout?: number,
) => void;
$router!: Router; $router!: Router;
notify!: ReturnType<typeof createNotifyHelpers>;
activeDid = ""; activeDid = "";
apiServer = ""; apiServer = "";
attendingMeeting: Meeting | null = null; attendingMeeting: Meeting | null = null;
@ -153,30 +148,66 @@ export default class OnboardMeetingListView extends Vue {
selectedMeeting: Meeting | null = null; selectedMeeting: Meeting | null = null;
showPasswordDialog = false; showPasswordDialog = false;
/**
* Vue lifecycle hook - component initialization
*
* Initializes the component by loading user settings and fetching available
* onboarding meetings. This method is called when the component is created
* and sets up all necessary data for the meeting list interface.
*
* Workflow:
* 1. Initialize notification system using createNotifyHelpers
* 2. Load user account settings (DID, API server, registration status)
* 3. Fetch available onboarding meetings from the server
*
* Dependencies:
* - PlatformServiceMixin for settings access ($accountSettings)
* - Server API for meeting data (fetchMeetings)
*
* Error Handling:
* - Server errors during meeting fetch are handled in fetchMeetings()
*
* @author Matthew Raymer
*/
async created() { async created() {
const settings = await this.$accountSettings(); this.notify = createNotifyHelpers(this.$notify);
if (settings?.activeDid) { // Load user account settings
try { const settings = await this.$accountSettings();
// Verify database settings are accessible
await this.$query("SELECT * FROM settings WHERE accountDid = ?", [
settings.activeDid,
]);
} catch (error) {
logger.error("Error checking database settings:", error);
}
}
this.activeDid = settings?.activeDid || ""; this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || ""; this.apiServer = settings?.apiServer || "";
this.firstName = settings?.firstName || ""; this.firstName = settings?.firstName || "";
this.isRegistered = !!settings?.isRegistered; this.isRegistered = !!settings?.isRegistered;
if (this.isRegistered) { await this.fetchMeetings();
await this.fetchMeetings();
}
} }
/**
* Fetches available onboarding meetings from the server
*
* This method retrieves the list of onboarding meetings that the user can join.
* It first checks if the user is already attending a meeting, and if so,
* displays that meeting instead of the full list.
*
* Workflow:
* 1. Check if user is already attending a meeting (groupOnboardMember endpoint)
* 2. If attending: Fetch meeting details and display single meeting view
* 3. If not attending: Fetch all available meetings (groupsOnboarding endpoint)
* 4. Handle loading states and error conditions
*
* API Endpoints Used:
* - GET /api/partner/groupOnboardMember - Check current attendance
* - GET /api/partner/groupOnboard/{id} - Get meeting details
* - GET /api/partner/groupsOnboarding - Get all available meetings
*
* State Management:
* - Sets isLoading flag during API calls
* - Updates attendingMeeting or meetings array
* - Handles error states with user notifications
*
* @author Matthew Raymer
*/
async fetchMeetings() { async fetchMeetings() {
this.isLoading = true; this.isLoading = true;
try { try {
@ -226,20 +257,36 @@ export default class OnboardMeetingListView extends Vue {
"Error fetching meetings: " + errorStringForLog(error), "Error fetching meetings: " + errorStringForLog(error),
true, true,
); );
this.$notify( this.notify.error(
{ serverMessageForUser(error) || "Failed to fetch meetings.",
group: "alert", TIMEOUTS.LONG,
type: "danger",
title: "Error",
text: serverMessageForUser(error) || "Failed to fetch meetings.",
},
5000,
); );
} finally { } finally {
this.isLoading = false; this.isLoading = false;
} }
} }
/**
* Opens the password dialog for joining a meeting
*
* This method initiates the process of joining an onboarding meeting by
* opening a modal dialog that prompts the user for the meeting password.
* The dialog is focused and ready for input when displayed.
*
* Workflow:
* 1. Clear any previous password input
* 2. Store the selected meeting for later use
* 3. Show the password dialog modal
* 4. Focus the password input field for immediate typing
*
* UI State Changes:
* - Sets showPasswordDialog to true (shows modal)
* - Clears password field for fresh input
* - Stores selectedMeeting for password submission
*
* @param meeting - The meeting object the user wants to join
* @author Matthew Raymer
*/
promptPassword(meeting: Meeting) { promptPassword(meeting: Meeting) {
this.password = ""; this.password = "";
this.selectedMeeting = meeting; this.selectedMeeting = meeting;
@ -252,12 +299,61 @@ export default class OnboardMeetingListView extends Vue {
}); });
} }
/**
* Cancels the password dialog and resets state
*
* This method handles the cancellation of the meeting password dialog.
* It cleans up the dialog state and resets all related variables to
* their initial state, ensuring a clean slate for future dialog interactions.
*
* State Cleanup:
* - Clears password input field
* - Removes selected meeting reference
* - Hides password dialog modal
*
* This ensures that if the user reopens the dialog, they start with
* a fresh state without any leftover data from previous attempts.
*
* @author Matthew Raymer
*/
cancelPasswordDialog() { cancelPasswordDialog() {
this.password = ""; this.password = "";
this.selectedMeeting = null; this.selectedMeeting = null;
this.showPasswordDialog = false; this.showPasswordDialog = false;
} }
/**
* Submits the password and joins the selected meeting
*
* This method handles the complete workflow of joining an onboarding meeting.
* It encrypts the user's member data with the provided password and sends
* it to the server to register the user as a meeting participant.
*
* Workflow:
* 1. Validate that a meeting is selected (safety check)
* 2. Create member data object with user information
* 3. Encrypt member data using the meeting password
* 4. Send encrypted data to server via groupOnboardMember endpoint
* 5. On success: Navigate to meeting members view with credentials
* 6. On failure: Show error notification to user
*
* Data Encryption:
* - Member data includes: name, DID, registration status
* - Data is encrypted using the meeting password for security
* - Encrypted data is sent to server for verification
*
* Navigation:
* - On successful join: Redirects to onboard-meeting-members view
* - Passes groupId, password, and memberId as route parameters
* - Allows user to see other meeting participants
*
* Error Handling:
* - Invalid passwords result in server rejection
* - Network errors are caught and displayed to user
* - All errors are logged for debugging purposes
*
* @author Matthew Raymer
*/
async submitPassword() { async submitPassword() {
if (!this.selectedMeeting) { if (!this.selectedMeeting) {
// this should never happen // this should never happen
@ -316,69 +412,93 @@ export default class OnboardMeetingListView extends Vue {
"Error joining meeting: " + errorStringForLog(error), "Error joining meeting: " + errorStringForLog(error),
true, true,
); );
this.$notify( this.notify.error(
{ serverMessageForUser(error) || "You failed to join the meeting.",
group: "alert", TIMEOUTS.LONG,
type: "danger",
title: "Error",
text:
serverMessageForUser(error) || "You failed to join the meeting.",
},
5000,
); );
} }
} }
/**
* Prompts user to confirm leaving the current meeting
*
* This method initiates the process of leaving an onboarding meeting.
* It shows a confirmation dialog to prevent accidental departures,
* then handles the server-side removal and UI updates.
*
* Workflow:
* 1. Display confirmation dialog asking user to confirm departure
* 2. On confirmation: Send DELETE request to groupOnboardMember endpoint
* 3. On success: Clear attending meeting state and refresh meeting list
* 4. Show success notification to user
* 5. On failure: Show error notification with details
*
* Server Interaction:
* - DELETE /api/partner/groupOnboardMember - Removes user from meeting
* - Requires authentication headers for user verification
* - Server handles the actual removal from meeting database
*
* State Management:
* - Clears attendingMeeting when successfully left
* - Refreshes meetings list to show updated availability
* - Updates UI to show meeting list instead of single meeting
*
* User Experience:
* - Confirmation prevents accidental departures
* - Clear feedback on success/failure
* - Seamless transition back to meeting list
*
* @author Matthew Raymer
*/
async leaveMeeting() { async leaveMeeting() {
this.$notify( this.notify.confirm(
{ "Are you sure you want to leave this meeting?",
group: "modal", async () => {
type: "confirm", try {
title: "Leave Meeting", const headers = await getHeaders(this.activeDid);
text: "Are you sure you want to leave this meeting?", await this.axios.delete(
onYes: async () => { this.apiServer + "/api/partner/groupOnboardMember",
try { { headers },
const headers = await getHeaders(this.activeDid); );
await this.axios.delete(
this.apiServer + "/api/partner/groupOnboardMember", this.attendingMeeting = null;
{ headers }, await this.fetchMeetings();
);
this.notify.success("You left the meeting.", TIMEOUTS.LONG);
this.attendingMeeting = null; } catch (error) {
await this.fetchMeetings(); this.$logAndConsole(
"Error leaving meeting: " + errorStringForLog(error),
this.$notify( true,
{ );
group: "alert", this.notify.error(
type: "success", serverMessageForUser(error) || "You failed to leave the meeting.",
title: "Success", TIMEOUTS.LONG,
text: "You left the meeting.", );
}, }
5000,
);
} catch (error) {
this.$logAndConsole(
"Error leaving meeting: " + errorStringForLog(error),
true,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
serverMessageForUser(error) ||
"You failed to leave the meeting.",
},
5000,
);
}
},
}, },
-1,
); );
} }
/**
* Navigates to the meeting creation page
*
* This method handles the navigation to the meeting setup page where
* registered users can create new onboarding meetings. It's only
* available to users who are registered in the system.
*
* Navigation:
* - Routes to onboard-meeting-setup view
* - Allows user to configure new meeting settings
* - Only accessible to registered users (controlled by template)
*
* User Flow:
* - User clicks "Create Meeting" button
* - System navigates to setup page
* - User can configure meeting name, password, etc.
* - New meeting becomes available to other users
*
* @author Matthew Raymer
*/
createMeeting() { createMeeting() {
this.$router.push({ name: "onboard-meeting-setup" }); this.$router.push({ name: "onboard-meeting-setup" });
} }

6
src/views/ShareMyContactInfoView.vue

@ -75,15 +75,9 @@ export default class ShareMyContactInfoView extends Vue {
isLoading = false; isLoading = false;
async mounted() { async mounted() {
// Debug logging for test diagnosis
const settings = await this.$settings(); const settings = await this.$settings();
const activeDid = settings?.activeDid; const activeDid = settings?.activeDid;
// @ts-expect-error - Debug property for testing contact sharing functionality
window.__SHARE_CONTACT_DEBUG__ = { settings, activeDid };
// eslint-disable-next-line no-console
if (!activeDid) { if (!activeDid) {
// eslint-disable-next-line no-console
this.$router.push({ name: "home" }); this.$router.push({ name: "home" });
} }
} }

Loading…
Cancel
Save