forked from trent_larson/crowd-funder-for-time-pwa
Modified handleContactVisibility method to return structured notification data with both title and message properties instead of just a string message. Updated addContact method to use notify.toast() with custom title and message from notification constants, providing more specific user feedback when contacts are added with or without visibility settings.
1278 lines
42 KiB
Vue
1278 lines
42 KiB
Vue
<template>
|
|
<QuickNav selected="Contacts" />
|
|
<TopMessage />
|
|
|
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
|
<!-- Heading -->
|
|
<h1 id="ViewHeading" class="text-4xl text-center font-light">
|
|
Your Contacts
|
|
</h1>
|
|
|
|
<div class="flex justify-between py-2 mt-8">
|
|
<span />
|
|
<span>
|
|
<a
|
|
:href="APP_SERVER + '/help-onboarding'"
|
|
target="_blank"
|
|
class="text-xs uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md ml-1"
|
|
>
|
|
Onboarding Guide
|
|
</a>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- New Contact -->
|
|
<ContactInputForm
|
|
v-model="contactInput"
|
|
:is-registered="isRegistered"
|
|
:on-submit="onClickNewContact"
|
|
:on-show-onboard-meeting="showOnboardMeetingDialog"
|
|
:on-registration-required="
|
|
() =>
|
|
notify.warning(
|
|
'You must get registered before you can create invites.',
|
|
)
|
|
"
|
|
:on-navigate-onboard-meeting="
|
|
() => $router.push({ name: 'onboard-meeting-list' })
|
|
"
|
|
:on-update-model-value="(value: string) => (contactInput = value)"
|
|
@qr-scan="handleQRCodeClick"
|
|
/>
|
|
|
|
<ContactListHeader
|
|
v-if="contacts.length > 0"
|
|
:show-give-numbers="showGiveNumbers"
|
|
:all-contacts-selected="allContactsSelected"
|
|
:copy-button-class="copyButtonClass"
|
|
:copy-button-disabled="copyButtonDisabled"
|
|
:give-amounts-button-text="giveAmountsButtonText"
|
|
:show-actions-button-text="showActionsButtonText"
|
|
:give-amounts-button-class="showGiveAmountsClassNames()"
|
|
@toggle-all-selection="toggleAllContactsSelection"
|
|
@copy-selected="copySelectedContacts"
|
|
@show-copy-info="showCopySelectionsInfo"
|
|
@toggle-give-totals="toggleShowGiveTotals"
|
|
@toggle-show-actions="toggleShowContactAmounts"
|
|
/>
|
|
<div v-if="showGiveNumbers" class="my-3">
|
|
<div class="w-full text-center text-sm italic text-slate-600">
|
|
Only the most recent hours are included. <br />To see more, click
|
|
<span
|
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-0.5 rounded"
|
|
>
|
|
<font-awesome icon="file-lines" class="text-xs fa-fw" />
|
|
</span>
|
|
<br />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results List -->
|
|
<ul
|
|
v-if="contacts.length > 0"
|
|
id="listContacts"
|
|
class="border-t border-slate-300 my-2"
|
|
>
|
|
<ContactListItem
|
|
v-for="contact in filteredContacts"
|
|
:key="contact.did"
|
|
:contact="contact"
|
|
:active-did="activeDid"
|
|
:show-checkbox="!showGiveNumbers"
|
|
:show-actions="showGiveNumbers"
|
|
:is-selected="contactsSelected.includes(contact.did)"
|
|
:show-give-totals="showGiveTotals"
|
|
:show-give-confirmed="showGiveConfirmed"
|
|
:given-to-me-descriptions="givenToMeDescriptions"
|
|
:given-to-me-confirmed="givenToMeConfirmed"
|
|
:given-to-me-unconfirmed="givenToMeUnconfirmed"
|
|
:given-by-me-descriptions="givenByMeDescriptions"
|
|
:given-by-me-confirmed="givenByMeConfirmed"
|
|
:given-by-me-unconfirmed="givenByMeUnconfirmed"
|
|
@toggle-selection="toggleContactSelection"
|
|
@show-identicon="showLargeIdenticon = $event"
|
|
@show-gifted-dialog="confirmShowGiftedDialog"
|
|
@open-offer-dialog="openOfferDialog"
|
|
/>
|
|
</ul>
|
|
<p v-else>There are no contacts.</p>
|
|
|
|
<ContactBulkActions
|
|
v-if="contacts.length > 0"
|
|
:show-give-numbers="showGiveNumbers"
|
|
:all-contacts-selected="allContactsSelected"
|
|
:copy-button-class="copyButtonClass"
|
|
:copy-button-disabled="copyButtonDisabled"
|
|
@toggle-all-selection="toggleAllContactsSelection"
|
|
@copy-selected="copySelectedContacts"
|
|
/>
|
|
|
|
<GiftedDialog ref="customGivenDialog" />
|
|
<OfferDialog ref="customOfferDialog" />
|
|
<ContactNameDialog ref="contactNameDialog" />
|
|
|
|
<LargeIdenticonModal
|
|
:contact="showLargeIdenticon"
|
|
@close="showLargeIdenticon = undefined"
|
|
/>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { AxiosError } from "axios";
|
|
import { Buffer } from "buffer/";
|
|
import { IndexableType } from "dexie";
|
|
import { JWTPayload } from "did-jwt";
|
|
import * as R from "ramda";
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
|
import { useClipboard } from "@vueuse/core";
|
|
import { Capacitor } from "@capacitor/core";
|
|
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import EntityIcon from "../components/EntityIcon.vue";
|
|
import GiftedDialog from "../components/GiftedDialog.vue";
|
|
import OfferDialog from "../components/OfferDialog.vue";
|
|
import ContactNameDialog from "../components/ContactNameDialog.vue";
|
|
import TopMessage from "../components/TopMessage.vue";
|
|
import ContactListItem from "../components/ContactListItem.vue";
|
|
import ContactInputForm from "../components/ContactInputForm.vue";
|
|
import ContactListHeader from "../components/ContactListHeader.vue";
|
|
import ContactBulkActions from "../components/ContactBulkActions.vue";
|
|
import LargeIdenticonModal from "../components/LargeIdenticonModal.vue";
|
|
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
|
|
// Legacy logging import removed - using PlatformServiceMixin methods
|
|
import { Contact } from "../db/tables/contacts";
|
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
|
import { decodeEndorserJwt } from "../libs/crypto/vc";
|
|
import {
|
|
CONTACT_CSV_HEADER,
|
|
createEndorserJwtForDid,
|
|
errorStringForLog,
|
|
getHeaders,
|
|
isDid,
|
|
register,
|
|
setVisibilityUtil,
|
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
|
CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI,
|
|
CONTACT_URL_PATH_ENDORSER_CH_OLD,
|
|
} from "../libs/endorserServer";
|
|
import { GiveSummaryRecord } from "@/interfaces/records";
|
|
import { UserInfo } from "@/interfaces/common";
|
|
import { VerifiableCredential } from "@/interfaces/claims-result";
|
|
import * as libsUtil from "../libs/util";
|
|
import { generateSaveAndActivateIdentity } from "../libs/util";
|
|
import { logger } from "../utils/logger";
|
|
// No longer needed - using PlatformServiceMixin methods
|
|
// import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
|
import { PROD_SHARE_DOMAIN } from "@/constants/app";
|
|
import {
|
|
NOTIFY_CONTACT_NO_INFO,
|
|
NOTIFY_CONTACTS_ADD_ERROR,
|
|
NOTIFY_CONTACT_NO_DID,
|
|
NOTIFY_CONTACT_INVALID_DID,
|
|
NOTIFY_CONTACTS_ADDED_VISIBLE,
|
|
NOTIFY_CONTACTS_ADDED,
|
|
NOTIFY_CONTACTS_ADDED_CONFIRM,
|
|
NOTIFY_CONTACT_IMPORT_ERROR,
|
|
NOTIFY_CONTACT_IMPORT_CONFLICT,
|
|
NOTIFY_CONTACT_IMPORT_CONSTRAINT,
|
|
NOTIFY_CONTACT_SETTING_SAVE_ERROR,
|
|
NOTIFY_CONTACT_INFO_COPY,
|
|
NOTIFY_CONTACTS_SELECT_TO_COPY,
|
|
NOTIFY_CONTACT_LINK_COPIED,
|
|
NOTIFY_BLANK_INVITE,
|
|
NOTIFY_INVITE_REGISTRATION_SUCCESS,
|
|
NOTIFY_CONTACTS_ADDED_CSV,
|
|
NOTIFY_CONTACT_INPUT_PARSE_ERROR,
|
|
NOTIFY_CONTACT_NO_CONTACT_FOUND,
|
|
NOTIFY_GIVES_LOAD_ERROR,
|
|
NOTIFY_MEETING_STATUS_ERROR,
|
|
NOTIFY_REGISTRATION_ERROR_FALLBACK,
|
|
NOTIFY_REGISTRATION_ERROR_GENERIC,
|
|
NOTIFY_VISIBILITY_ERROR_FALLBACK,
|
|
getRegisterPersonSuccessMessage,
|
|
getVisibilitySuccessMessage,
|
|
getGivesRetrievalErrorMessage,
|
|
} from "@/constants/notifications";
|
|
|
|
/**
|
|
* ContactsView - Main contact management interface
|
|
*
|
|
* This view provides comprehensive contact management functionality including:
|
|
* - Contact display and filtering
|
|
* - Contact creation from various input formats (DID, JWT, CSV, JSON)
|
|
* - Contact selection and bulk operations
|
|
* - Give amounts display and management
|
|
* - Contact registration and visibility settings
|
|
* - QR code scanning integration
|
|
* - Meeting onboarding functionality
|
|
*
|
|
* The component uses the Enhanced Triple Migration Pattern with:
|
|
* - PlatformServiceMixin for database operations
|
|
* - Centralized notification constants
|
|
* - Computed properties for template streamlining
|
|
* - Refactored methods for maintainability
|
|
*
|
|
* @author Matthew Raymer
|
|
*/
|
|
@Component({
|
|
name: "ContactsView",
|
|
components: {
|
|
GiftedDialog,
|
|
EntityIcon,
|
|
OfferDialog,
|
|
QuickNav,
|
|
ContactNameDialog,
|
|
TopMessage,
|
|
ContactListItem,
|
|
ContactInputForm,
|
|
ContactListHeader,
|
|
ContactBulkActions,
|
|
LargeIdenticonModal,
|
|
},
|
|
mixins: [PlatformServiceMixin],
|
|
})
|
|
export default class ContactsView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
$route!: RouteLocationNormalizedLoaded;
|
|
$router!: Router;
|
|
|
|
/** Notification helpers */
|
|
notify!: ReturnType<typeof createNotifyHelpers>;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
contacts: Array<Contact> = [];
|
|
contactInput = "";
|
|
contactEdit: Contact | null = null;
|
|
contactNewName = "";
|
|
contactsSelected: Array<string> = [];
|
|
// { "did:...": concatenated-descriptions } entry for each contact
|
|
givenByMeDescriptions: Record<string, string> = {};
|
|
// { "did:...": amount } entry for each contact
|
|
givenByMeConfirmed: Record<string, number> = {};
|
|
// { "did:...": amount } entry for each contact
|
|
givenByMeUnconfirmed: Record<string, number> = {};
|
|
// { "did:...": concatenated-descriptions } entry for each contact
|
|
givenToMeDescriptions: Record<string, string> = {};
|
|
// { "did:...": amount } entry for each contact
|
|
givenToMeConfirmed: Record<string, number> = {};
|
|
// { "did:...": amount } entry for each contact
|
|
givenToMeUnconfirmed: Record<string, number> = {};
|
|
hideRegisterPromptOnNewContact = false;
|
|
isRegistered = false;
|
|
showDidCopy = false;
|
|
showPubKeyCopy = false;
|
|
showPubKeyHashCopy = false;
|
|
showGiveNumbers = false;
|
|
showGiveTotals = true;
|
|
showGiveConfirmed = true;
|
|
showLargeIdenticon?: Contact;
|
|
|
|
APP_SERVER = APP_SERVER;
|
|
AppString = AppString;
|
|
libsUtil = libsUtil;
|
|
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
|
|
|
|
/**
|
|
* Component lifecycle hook - Initialize component state and load data
|
|
* Sets up notification helpers, loads user settings, processes URL parameters,
|
|
* and loads contacts from database
|
|
*/
|
|
public async created() {
|
|
this.notify = createNotifyHelpers(this.$notify);
|
|
|
|
const settings = await this.$accountSettings();
|
|
this.activeDid = settings.activeDid || "";
|
|
this.apiServer = settings.apiServer || "";
|
|
this.isRegistered = !!settings.isRegistered;
|
|
|
|
// if these detect a query parameter, they can and then redirect to this URL without a query parameter
|
|
// to avoid problems when they reload or they go forward & back and it tries to reprocess
|
|
await this.processContactJwt();
|
|
await this.processInviteJwt();
|
|
|
|
this.showGiveNumbers = !!settings.showContactGivesInline;
|
|
this.hideRegisterPromptOnNewContact =
|
|
!!settings.hideRegisterPromptOnNewContact;
|
|
|
|
if (this.showGiveNumbers) {
|
|
this.loadGives();
|
|
}
|
|
|
|
// Replace PlatformServiceFactory and manual SQL with mixin method
|
|
this.contacts = await this.$getAllContacts();
|
|
}
|
|
|
|
private async processContactJwt() {
|
|
// handle a contact sent via URL
|
|
//
|
|
// For external links, use /deep-link/contact-import/:jwt with a JWT that has an array of contacts
|
|
// because that will do better error checking for things like missing data on iOS platforms.
|
|
const importedContactJwt = this.$route.query["contactJwt"] as string;
|
|
if (importedContactJwt) {
|
|
// really should fully verify contents
|
|
const { payload } = decodeEndorserJwt(importedContactJwt);
|
|
const userInfo = payload["own"] as UserInfo;
|
|
const newContact = {
|
|
did: userInfo.did || payload["iss"], // ".did" is reliable as of v 0.3.49
|
|
name: userInfo.name,
|
|
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
|
|
profileImageUrl: userInfo.profileImageUrl,
|
|
publicKeyBase64: userInfo.publicEncKey,
|
|
registered: userInfo.registered,
|
|
} as Contact;
|
|
await this.addContact(newContact);
|
|
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
|
this.$router.push({ path: "/contacts" });
|
|
}
|
|
}
|
|
|
|
private async processInviteJwt() {
|
|
// handle an invite JWT sent via URL
|
|
const importedInviteJwt = this.$route.query["inviteJwt"] as string;
|
|
if (importedInviteJwt === "") {
|
|
// this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
|
|
this.notify.error(NOTIFY_BLANK_INVITE.message, TIMEOUTS.VERY_LONG);
|
|
} else if (importedInviteJwt) {
|
|
// Identity creation should be handled by router guard, but keep as fallback for invite processing
|
|
if (!this.activeDid) {
|
|
logger.info(
|
|
"[ContactsView] No active DID found, creating identity as fallback for invite processing",
|
|
);
|
|
this.activeDid = await generateSaveAndActivateIdentity();
|
|
}
|
|
// send invite directly to server, with auth for this user
|
|
const headers = await getHeaders(this.activeDid);
|
|
try {
|
|
const response = await this.axios.post(
|
|
this.apiServer + "/api/v2/claim",
|
|
{ jwtEncoded: importedInviteJwt },
|
|
{ headers },
|
|
);
|
|
if (response.status != 201) {
|
|
throw { error: { response: response } };
|
|
}
|
|
await this.$saveUserSettings(this.activeDid, { isRegistered: true });
|
|
this.isRegistered = true;
|
|
this.notify.success(NOTIFY_INVITE_REGISTRATION_SUCCESS.message);
|
|
|
|
// wait for a second before continuing so they see the registration message
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
|
|
// now add the inviter as a contact
|
|
// (similar code is in InviteOneAcceptView.vue)
|
|
const payload: JWTPayload =
|
|
decodeEndorserJwt(importedInviteJwt).payload;
|
|
const registration = payload as VerifiableCredential;
|
|
(this.$refs.contactNameDialog as ContactNameDialog).open(
|
|
"Who Invited You?",
|
|
"",
|
|
async (name) => {
|
|
await this.addContact({
|
|
did: (registration.vc.credentialSubject.agent as any).identifier,
|
|
name: name,
|
|
registered: true,
|
|
});
|
|
// wait for a second before continuing so they see the user-added message
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
this.showOnboardingInfo();
|
|
},
|
|
async () => {
|
|
// on cancel, will still add the contact
|
|
await this.addContact({
|
|
did: (registration.vc.credentialSubject.agent as any).identifier,
|
|
name: "(person who invited you)",
|
|
registered: true,
|
|
});
|
|
// wait for a second before continuing so they see the user-added message
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
this.showOnboardingInfo();
|
|
},
|
|
);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} catch (error: any) {
|
|
const fullError = "Error redeeming invite: " + errorStringForLog(error);
|
|
this.$logAndConsole(fullError, true);
|
|
let message = "Got an error sending the invite.";
|
|
if (
|
|
error.response &&
|
|
error.response.data &&
|
|
error.response.data.error
|
|
) {
|
|
if (error.response.data.error.message) {
|
|
message = error.response.data.error.message;
|
|
} else {
|
|
message = error.response.data.error;
|
|
}
|
|
} else if (error.message) {
|
|
message = error.message;
|
|
}
|
|
this.notify.error(message, TIMEOUTS.MODAL);
|
|
}
|
|
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
|
this.$router.push({ path: "/contacts" });
|
|
}
|
|
}
|
|
|
|
// Legacy danger() and warning() methods removed - now using this.notify.error() and this.notify.warning()
|
|
|
|
private showOnboardingInfo() {
|
|
this.notify.confirm(
|
|
NOTIFY_CONTACTS_ADDED_CONFIRM.message,
|
|
async () => {
|
|
this.$router.push({ name: "home" });
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
|
|
// Computed properties for template streamlining
|
|
get filteredContacts() {
|
|
return this.showGiveNumbers
|
|
? this.contactsSelected.length === 0
|
|
? this.contacts
|
|
: this.contacts.filter((contact) =>
|
|
this.contactsSelected.includes(contact.did),
|
|
)
|
|
: this.contacts;
|
|
}
|
|
|
|
get copyButtonClass() {
|
|
return this.contactsSelected.length > 0
|
|
? "text-md bg-gradient-to-b from-blue-400 to-blue-700 " +
|
|
"shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white " +
|
|
"ml-3 px-3 py-1.5 rounded-md cursor-pointer"
|
|
: "text-md bg-gradient-to-b from-slate-400 to-slate-700 " +
|
|
"shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 " +
|
|
"ml-3 px-3 py-1.5 rounded-md cursor-not-allowed";
|
|
}
|
|
|
|
get copyButtonDisabled() {
|
|
return this.contactsSelected.length === 0;
|
|
}
|
|
|
|
get giveAmountsButtonText() {
|
|
if (this.showGiveTotals) {
|
|
return "Totals";
|
|
}
|
|
return this.showGiveConfirmed ? "Confirmed Amounts" : "Unconfirmed Amounts";
|
|
}
|
|
|
|
get showActionsButtonText() {
|
|
return this.showGiveNumbers ? "Hide Actions" : "See Actions";
|
|
}
|
|
|
|
get allContactsSelected() {
|
|
return this.contactsSelected.length === this.contacts.length;
|
|
}
|
|
|
|
// Helper methods for template interactions
|
|
toggleAllContactsSelection(): void {
|
|
if (this.allContactsSelected) {
|
|
this.contactsSelected = [];
|
|
} else {
|
|
this.contactsSelected = this.contacts.map((contact) => contact.did);
|
|
}
|
|
}
|
|
|
|
toggleContactSelection(contactDid: string): void {
|
|
if (this.contactsSelected.includes(contactDid)) {
|
|
this.contactsSelected.splice(
|
|
this.contactsSelected.indexOf(contactDid),
|
|
1,
|
|
);
|
|
} else {
|
|
this.contactsSelected.push(contactDid);
|
|
}
|
|
}
|
|
|
|
private async loadGives() {
|
|
if (!this.activeDid) {
|
|
return;
|
|
}
|
|
|
|
const handleResponse = (
|
|
resp: { status: number; data: { data: GiveSummaryRecord[] } },
|
|
descriptions: Record<string, string>,
|
|
confirmed: Record<string, number>,
|
|
unconfirmed: Record<string, number>,
|
|
useRecipient: boolean,
|
|
) => {
|
|
if (resp.status === 200) {
|
|
const allData = resp.data.data;
|
|
for (const give of allData) {
|
|
const otherDid = useRecipient ? give.recipientDid : give.agentDid;
|
|
if (give.unit === "HUR") {
|
|
if (give.amountConfirmed) {
|
|
const prevAmount = confirmed[otherDid] || 0;
|
|
confirmed[otherDid] = prevAmount + give.amount;
|
|
} else {
|
|
const prevAmount = unconfirmed[otherDid] || 0;
|
|
unconfirmed[otherDid] = prevAmount + give.amount;
|
|
}
|
|
if (!descriptions[otherDid] && give.description) {
|
|
descriptions[otherDid] = give.description;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
logger.error(
|
|
"Got bad response status & data of",
|
|
resp.status,
|
|
resp.data,
|
|
);
|
|
this.notify.error(getGivesRetrievalErrorMessage(useRecipient));
|
|
}
|
|
};
|
|
|
|
try {
|
|
const headers = await getHeaders(this.activeDid);
|
|
const givenByUrl =
|
|
this.apiServer +
|
|
"/api/v2/report/gives?agentDid=" +
|
|
encodeURIComponent(this.activeDid);
|
|
const givenToUrl =
|
|
this.apiServer +
|
|
"/api/v2/report/gives?recipientDid=" +
|
|
encodeURIComponent(this.activeDid);
|
|
|
|
const [givenByMeResp, givenToMeResp] = await Promise.all([
|
|
this.axios.get(givenByUrl, { headers }),
|
|
this.axios.get(givenToUrl, { headers }),
|
|
]);
|
|
|
|
const givenByMeDescriptions = {};
|
|
const givenByMeConfirmed = {};
|
|
const givenByMeUnconfirmed = {};
|
|
handleResponse(
|
|
givenByMeResp,
|
|
givenByMeDescriptions,
|
|
givenByMeConfirmed,
|
|
givenByMeUnconfirmed,
|
|
true,
|
|
);
|
|
this.givenByMeDescriptions = givenByMeDescriptions;
|
|
this.givenByMeConfirmed = givenByMeConfirmed;
|
|
this.givenByMeUnconfirmed = givenByMeUnconfirmed;
|
|
|
|
const givenToMeDescriptions = {};
|
|
const givenToMeConfirmed = {};
|
|
const givenToMeUnconfirmed = {};
|
|
handleResponse(
|
|
givenToMeResp,
|
|
givenToMeDescriptions,
|
|
givenToMeConfirmed,
|
|
givenToMeUnconfirmed,
|
|
false,
|
|
);
|
|
this.givenToMeDescriptions = givenToMeDescriptions;
|
|
this.givenToMeConfirmed = givenToMeConfirmed;
|
|
this.givenToMeUnconfirmed = givenToMeUnconfirmed;
|
|
} catch (error) {
|
|
const fullError = "Error loading gives: " + errorStringForLog(error);
|
|
this.$logAndConsole(fullError, true);
|
|
this.notify.error(NOTIFY_GIVES_LOAD_ERROR.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main method to handle new contact input processing
|
|
* Routes to appropriate parsing method based on input format
|
|
*/
|
|
private async onClickNewContact(): Promise<void> {
|
|
const contactInput = this.contactInput.trim();
|
|
if (!contactInput) {
|
|
this.notify.error(NOTIFY_CONTACT_NO_INFO.message);
|
|
return;
|
|
}
|
|
|
|
// Try different parsing methods in order
|
|
if (await this.tryParseJwtContact(contactInput)) return;
|
|
if (await this.tryParseCsvContacts(contactInput)) return;
|
|
if (await this.tryParseDidContact(contactInput)) return;
|
|
if (await this.tryParseJsonContacts(contactInput)) return;
|
|
|
|
// If no parsing method succeeded
|
|
this.notify.error(NOTIFY_CONTACT_NO_CONTACT_FOUND.message);
|
|
}
|
|
|
|
/**
|
|
* Parse contact from JWT URL format
|
|
*/
|
|
private async tryParseJwtContact(contactInput: string): Promise<boolean> {
|
|
if (
|
|
contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI) ||
|
|
contactInput.includes(CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI) ||
|
|
contactInput.includes(CONTACT_URL_PATH_ENDORSER_CH_OLD)
|
|
) {
|
|
const jwt = getContactJwtFromJwtUrl(contactInput);
|
|
if (jwt) {
|
|
const { payload } = decodeEndorserJwt(jwt);
|
|
const userInfo = payload["own"] as UserInfo;
|
|
const newContact = {
|
|
did: userInfo.did || payload["iss"],
|
|
name: userInfo.name,
|
|
nextPubKeyHashB64: userInfo.nextPublicEncKeyHash,
|
|
profileImageUrl: userInfo.profileImageUrl,
|
|
publicKeyBase64: userInfo.publicEncKey,
|
|
registered: userInfo.registered,
|
|
} as Contact;
|
|
await this.addContact(newContact);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Parse contacts from CSV format
|
|
*/
|
|
private async tryParseCsvContacts(contactInput: string): Promise<boolean> {
|
|
if (contactInput.startsWith(CONTACT_CSV_HEADER)) {
|
|
const lines = contactInput.split(/\n/);
|
|
const lineAdded = [];
|
|
for (const line of lines) {
|
|
if (!line.trim() || line.startsWith(CONTACT_CSV_HEADER)) {
|
|
continue;
|
|
}
|
|
lineAdded.push(this.addContactFromEndorserMobileLine(line));
|
|
}
|
|
try {
|
|
await Promise.all(lineAdded);
|
|
this.notify.success(NOTIFY_CONTACTS_ADDED_CSV.message);
|
|
} catch (e) {
|
|
const fullError =
|
|
"Error adding contacts from CSV: " + errorStringForLog(e);
|
|
this.$logAndConsole(fullError, true);
|
|
this.notify.error(NOTIFY_CONTACTS_ADD_ERROR.message);
|
|
}
|
|
|
|
this.contacts = await this.$getAllContacts();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Parse contact from DID format with optional parameters
|
|
*/
|
|
private async tryParseDidContact(contactInput: string): Promise<boolean> {
|
|
if (contactInput.startsWith("did:")) {
|
|
const parsedContact = this.parseDidContactString(contactInput);
|
|
await this.addContact(parsedContact);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Parse DID contact string into Contact object
|
|
*/
|
|
private parseDidContactString(contactInput: string): Contact {
|
|
let did = contactInput;
|
|
let name, publicKeyInput, nextPublicKeyHashInput;
|
|
|
|
const commaPos1 = contactInput.indexOf(",");
|
|
if (commaPos1 > -1) {
|
|
did = contactInput.substring(0, commaPos1).trim();
|
|
name = contactInput.substring(commaPos1 + 1).trim();
|
|
const commaPos2 = contactInput.indexOf(",", commaPos1 + 1);
|
|
if (commaPos2 > -1) {
|
|
name = contactInput.substring(commaPos1 + 1, commaPos2).trim();
|
|
publicKeyInput = contactInput.substring(commaPos2 + 1).trim();
|
|
const commaPos3 = contactInput.indexOf(",", commaPos2 + 1);
|
|
if (commaPos3 > -1) {
|
|
publicKeyInput = contactInput
|
|
.substring(commaPos2 + 1, commaPos3)
|
|
.trim();
|
|
nextPublicKeyHashInput = contactInput.substring(commaPos3 + 1).trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert hex keys to base64 if needed
|
|
const publicKeyBase64 = this.convertHexToBase64(publicKeyInput);
|
|
const nextPubKeyHashB64 = this.convertHexToBase64(nextPublicKeyHashInput);
|
|
|
|
return {
|
|
did,
|
|
name,
|
|
publicKeyBase64,
|
|
nextPubKeyHashB64,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert hex string to base64 if it matches hex pattern
|
|
*/
|
|
private convertHexToBase64(hexString?: string): string | undefined {
|
|
if (!hexString || !/^[0-9A-Fa-f]{66}$/i.test(hexString)) {
|
|
return hexString;
|
|
}
|
|
return Buffer.from(hexString, "hex").toString("base64");
|
|
}
|
|
|
|
/**
|
|
* Parse contacts from JSON array format
|
|
*/
|
|
private async tryParseJsonContacts(contactInput: string): Promise<boolean> {
|
|
if (contactInput.includes("[")) {
|
|
const jsonContactInput = contactInput.substring(
|
|
contactInput.indexOf("["),
|
|
contactInput.lastIndexOf("]") + 1,
|
|
);
|
|
try {
|
|
const contacts = JSON.parse(jsonContactInput);
|
|
this.$router.push({
|
|
name: "contact-import",
|
|
query: { contacts: JSON.stringify(contacts) },
|
|
});
|
|
return true;
|
|
} catch (e) {
|
|
const fullError =
|
|
"Error adding contacts from array: " + errorStringForLog(e);
|
|
this.$logAndConsole(fullError, true);
|
|
this.notify.error(NOTIFY_CONTACT_INPUT_PARSE_ERROR.message);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private async addContactFromEndorserMobileLine(
|
|
lineRaw: string,
|
|
): Promise<IndexableType> {
|
|
const newContact = libsUtil.csvLineToContact(lineRaw);
|
|
// Replace PlatformServiceFactory with mixin method
|
|
await this.$insertContact(newContact);
|
|
// Return the DID as the indexable type for compatibility
|
|
return newContact.did as IndexableType;
|
|
}
|
|
|
|
/**
|
|
* Add a new contact to the database and update UI
|
|
* Validates contact data, inserts into database, updates local state,
|
|
* sets visibility, and handles registration prompts
|
|
*/
|
|
private async addContact(newContact: Contact) {
|
|
// Validate contact data
|
|
if (!this.validateContactData(newContact)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Insert contact into database
|
|
await this.$insertContact(newContact);
|
|
|
|
// Update local contacts list
|
|
this.updateContactsList(newContact);
|
|
|
|
// Set visibility and get success notification data
|
|
const notificationData = await this.handleContactVisibility(newContact);
|
|
|
|
// Clear input field
|
|
this.contactInput = "";
|
|
|
|
// Handle registration prompt if needed
|
|
await this.handleRegistrationPrompt(newContact);
|
|
|
|
// Show success notification with custom title and message
|
|
this.notify.toast(
|
|
notificationData.title,
|
|
notificationData.message,
|
|
TIMEOUTS.STANDARD,
|
|
);
|
|
} catch (err) {
|
|
this.handleContactAddError(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate contact data before insertion
|
|
*/
|
|
private validateContactData(newContact: Contact): boolean {
|
|
if (!newContact.did) {
|
|
this.notify.error(NOTIFY_CONTACT_NO_DID.message);
|
|
return false;
|
|
}
|
|
if (!isDid(newContact.did)) {
|
|
this.notify.error(NOTIFY_CONTACT_INVALID_DID.message);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Update local contacts list with new contact
|
|
*/
|
|
private updateContactsList(newContact: Contact): void {
|
|
const allContacts = this.contacts.concat([newContact]);
|
|
this.contacts = R.sort(
|
|
(a: Contact, b) => (a.name || "").localeCompare(b.name || ""),
|
|
allContacts,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Handle contact visibility settings and return appropriate notification data
|
|
*/
|
|
private async handleContactVisibility(
|
|
newContact: Contact,
|
|
): Promise<{ title: string; message: string }> {
|
|
if (this.activeDid) {
|
|
await this.setVisibility(newContact, true, false);
|
|
newContact.seesMe = true;
|
|
return {
|
|
title: NOTIFY_CONTACTS_ADDED_VISIBLE.title,
|
|
message: NOTIFY_CONTACTS_ADDED_VISIBLE.message,
|
|
};
|
|
} else {
|
|
return {
|
|
title: NOTIFY_CONTACTS_ADDED.title,
|
|
message: NOTIFY_CONTACTS_ADDED.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle registration prompt for new contacts
|
|
*/
|
|
private async handleRegistrationPrompt(newContact: Contact): Promise<void> {
|
|
if (
|
|
!this.isRegistered ||
|
|
this.hideRegisterPromptOnNewContact ||
|
|
newContact.registered
|
|
) {
|
|
return;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Register",
|
|
text: "Do you want to register them?",
|
|
onCancel: async (stopAsking?: boolean) => {
|
|
await this.handleRegistrationPromptResponse(stopAsking);
|
|
},
|
|
onNo: async (stopAsking?: boolean) => {
|
|
await this.handleRegistrationPromptResponse(stopAsking);
|
|
},
|
|
onYes: async () => {
|
|
await this.register(newContact);
|
|
},
|
|
promptToStopAsking: true,
|
|
},
|
|
-1,
|
|
);
|
|
}, 1000);
|
|
}
|
|
|
|
/**
|
|
* Handle user response to registration prompt
|
|
*/
|
|
private async handleRegistrationPromptResponse(
|
|
stopAsking?: boolean,
|
|
): Promise<void> {
|
|
if (stopAsking) {
|
|
await this.$saveSettings({
|
|
hideRegisterPromptOnNewContact: stopAsking,
|
|
});
|
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle errors during contact addition
|
|
*/
|
|
private handleContactAddError(err: any): void {
|
|
const fullError =
|
|
"Error when adding contact to storage: " + errorStringForLog(err);
|
|
this.$logAndConsole(fullError, true);
|
|
|
|
let message = NOTIFY_CONTACT_IMPORT_ERROR.message;
|
|
if (
|
|
(err as any).message?.indexOf("Key already exists in the object store.") >
|
|
-1
|
|
) {
|
|
message = NOTIFY_CONTACT_IMPORT_CONFLICT.message;
|
|
}
|
|
if ((err as any).name === "ConstraintError") {
|
|
message += " " + NOTIFY_CONTACT_IMPORT_CONSTRAINT.message;
|
|
}
|
|
|
|
this.notify.error(message, TIMEOUTS.LONG);
|
|
}
|
|
|
|
/**
|
|
* Register a contact with the endorser server
|
|
* Sends registration request and updates contact status on success
|
|
* Note: This method is also used in DIDView.vue
|
|
*/
|
|
private async register(contact: Contact) {
|
|
this.notify.sent();
|
|
|
|
try {
|
|
const regResult = await register(
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
contact,
|
|
);
|
|
if (regResult.success) {
|
|
contact.registered = true;
|
|
// Replace PlatformServiceFactory with mixin method
|
|
await this.$updateContact(contact.did, { registered: true });
|
|
|
|
this.notify.success(getRegisterPersonSuccessMessage(contact.name));
|
|
} else {
|
|
this.notify.error(
|
|
(regResult.error as string) ||
|
|
NOTIFY_REGISTRATION_ERROR_FALLBACK.message,
|
|
TIMEOUTS.MODAL,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
const fullError = "Error when registering: " + errorStringForLog(error);
|
|
this.$logAndConsole(fullError, true);
|
|
let userMessage = NOTIFY_REGISTRATION_ERROR_GENERIC.message;
|
|
const serverError = error as AxiosError;
|
|
if (serverError.isAxiosError) {
|
|
if (
|
|
serverError.response?.data &&
|
|
typeof serverError.response.data === "object" &&
|
|
"error" in serverError.response.data &&
|
|
typeof serverError.response.data.error === "object" &&
|
|
serverError.response.data.error !== null &&
|
|
"message" in serverError.response.data.error
|
|
) {
|
|
userMessage = serverError.response.data.error.message as string;
|
|
} else if (serverError.message) {
|
|
userMessage = serverError.message; // Info for the user
|
|
} else {
|
|
userMessage = JSON.stringify(serverError.toJSON());
|
|
}
|
|
} else {
|
|
userMessage = error as string;
|
|
}
|
|
// Now set that error for the user to see.
|
|
this.notify.error(userMessage, TIMEOUTS.MODAL);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set visibility for a contact on the endorser server
|
|
* Note: This method is also used in DIDView.vue
|
|
*/
|
|
private async setVisibility(
|
|
contact: Contact,
|
|
visibility: boolean,
|
|
showSuccessAlert: boolean,
|
|
) {
|
|
const result = await setVisibilityUtil(
|
|
this.activeDid,
|
|
this.apiServer,
|
|
this.axios,
|
|
contact,
|
|
visibility,
|
|
);
|
|
if (result.success) {
|
|
//contact.seesMe = visibility; // why doesn't it affect the UI from here?
|
|
if (showSuccessAlert) {
|
|
this.notify.success(
|
|
getVisibilitySuccessMessage(contact.name, visibility),
|
|
);
|
|
}
|
|
return true;
|
|
} else {
|
|
logger.error(
|
|
"Got strange result from setting visibility. It can happen when setting visibility on oneself.",
|
|
result,
|
|
);
|
|
const message =
|
|
(result.error as string) || NOTIFY_VISIBILITY_ERROR_FALLBACK.message;
|
|
this.notify.error(message, TIMEOUTS.LONG);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Confirm and show gifted dialog with unconfirmed amounts check
|
|
* If there are unconfirmed amounts, prompts user to confirm them first
|
|
*/
|
|
private confirmShowGiftedDialog(giverDid: string, recipientDid: string) {
|
|
// if they have unconfirmed amounts, ask to confirm those
|
|
if (
|
|
recipientDid === this.activeDid &&
|
|
this.givenToMeUnconfirmed[giverDid] > 0
|
|
) {
|
|
const isAre = this.givenToMeUnconfirmed[giverDid] == 1 ? "is" : "are";
|
|
const hours = this.givenToMeUnconfirmed[giverDid] == 1 ? "hour" : "hours";
|
|
const message =
|
|
"There " +
|
|
isAre +
|
|
" " +
|
|
this.givenToMeUnconfirmed[giverDid] +
|
|
" unconfirmed " +
|
|
hours +
|
|
" from them." +
|
|
" Would you like to confirm some of those hours?";
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Delete",
|
|
text: message,
|
|
onNo: async () => {
|
|
this.showGiftedDialog(giverDid, recipientDid);
|
|
},
|
|
onYes: async () => {
|
|
this.$router.push({
|
|
name: "contact-amounts",
|
|
query: { contactDid: giverDid },
|
|
});
|
|
},
|
|
},
|
|
-1,
|
|
);
|
|
} else {
|
|
this.showGiftedDialog(giverDid, recipientDid);
|
|
}
|
|
}
|
|
|
|
private showGiftedDialog(giverDid: string, recipientDid: string) {
|
|
let giver: libsUtil.GiverReceiverInputInfo | undefined;
|
|
let receiver: libsUtil.GiverReceiverInputInfo | undefined;
|
|
if (giverDid) {
|
|
giver = {
|
|
did: giverDid,
|
|
name: libsUtil.nameForDid(this.activeDid, this.contacts, giverDid),
|
|
};
|
|
}
|
|
if (recipientDid) {
|
|
receiver = {
|
|
did: recipientDid,
|
|
name: libsUtil.nameForDid(this.activeDid, this.contacts, recipientDid),
|
|
};
|
|
}
|
|
|
|
let callback: (amount: number) => void;
|
|
let customTitle = "";
|
|
// choose whether to open dialog to user or from user
|
|
if (giverDid == this.activeDid) {
|
|
callback = (amount: number) => {
|
|
const newList = R.clone(this.givenByMeUnconfirmed);
|
|
newList[recipientDid] = (newList[recipientDid] || 0) + amount;
|
|
this.givenByMeUnconfirmed = newList;
|
|
};
|
|
customTitle = "Given to " + (receiver?.name || "Someone Unnamed");
|
|
} else {
|
|
// must be (recipientDid == this.activeDid)
|
|
callback = (amount: number) => {
|
|
const newList = R.clone(this.givenToMeUnconfirmed);
|
|
newList[giverDid] = (newList[giverDid] || 0) + amount;
|
|
this.givenToMeUnconfirmed = newList;
|
|
};
|
|
customTitle = "Received from " + (giver?.name || "Someone Unnamed");
|
|
}
|
|
(this.$refs.customGivenDialog as GiftedDialog).open(
|
|
giver,
|
|
receiver,
|
|
undefined as unknown as string,
|
|
customTitle,
|
|
undefined as unknown as string,
|
|
callback,
|
|
);
|
|
}
|
|
|
|
openOfferDialog(recipientDid: string, recipientName?: string) {
|
|
(this.$refs.customOfferDialog as OfferDialog).open(
|
|
recipientDid,
|
|
recipientName,
|
|
);
|
|
}
|
|
|
|
private async toggleShowContactAmounts() {
|
|
const newShowValue = !this.showGiveNumbers;
|
|
try {
|
|
await this.$saveSettings({
|
|
showContactGivesInline: newShowValue,
|
|
});
|
|
} catch (err) {
|
|
const fullError =
|
|
"Error updating contact-amounts setting: " + errorStringForLog(err);
|
|
this.$logAndConsole(fullError, true);
|
|
// Use notification helper and constant
|
|
this.notify.error(
|
|
NOTIFY_CONTACT_SETTING_SAVE_ERROR.message,
|
|
TIMEOUTS.LONG,
|
|
);
|
|
}
|
|
this.showGiveNumbers = newShowValue;
|
|
if (
|
|
newShowValue &&
|
|
Object.keys(this.givenByMeDescriptions).length === 0 &&
|
|
Object.keys(this.givenByMeConfirmed).length === 0 &&
|
|
Object.keys(this.givenByMeUnconfirmed).length === 0 &&
|
|
Object.keys(this.givenToMeDescriptions).length === 0 &&
|
|
Object.keys(this.givenToMeConfirmed).length === 0 &&
|
|
Object.keys(this.givenToMeUnconfirmed).length === 0
|
|
) {
|
|
// assume we should load it all
|
|
this.loadGives();
|
|
}
|
|
}
|
|
private toggleShowGiveTotals() {
|
|
if (this.showGiveTotals) {
|
|
this.showGiveTotals = false;
|
|
this.showGiveConfirmed = true;
|
|
} else if (this.showGiveConfirmed) {
|
|
this.showGiveTotals = false; // stays the same
|
|
this.showGiveConfirmed = false;
|
|
} else {
|
|
this.showGiveTotals = true;
|
|
this.showGiveConfirmed = true;
|
|
}
|
|
}
|
|
|
|
private showGiveAmountsClassNames() {
|
|
return {
|
|
"from-slate-400": this.showGiveTotals,
|
|
"to-slate-700": this.showGiveTotals,
|
|
"from-green-400": !this.showGiveTotals && this.showGiveConfirmed,
|
|
"to-green-700": !this.showGiveTotals && this.showGiveConfirmed,
|
|
"from-yellow-400": !this.showGiveTotals && !this.showGiveConfirmed,
|
|
"to-yellow-700": !this.showGiveTotals && !this.showGiveConfirmed,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Copy selected contacts as a shareable JWT URL
|
|
* Creates a JWT containing selected contact data and copies to clipboard
|
|
*/
|
|
private async copySelectedContacts() {
|
|
if (this.contactsSelected.length === 0) {
|
|
// Use notification helper and constant
|
|
this.notify.error(NOTIFY_CONTACTS_SELECT_TO_COPY.message);
|
|
return;
|
|
}
|
|
const selectedContactsFull = this.contacts.filter((c) =>
|
|
this.contactsSelected.includes(c.did),
|
|
);
|
|
const selectedContacts: Array<Contact> = selectedContactsFull.map((c) => {
|
|
const contact: Contact = {
|
|
did: c.did,
|
|
name: c.name,
|
|
};
|
|
if (c.nextPubKeyHashB64) {
|
|
contact.nextPubKeyHashB64 = c.nextPubKeyHashB64;
|
|
}
|
|
if (c.profileImageUrl) {
|
|
contact.profileImageUrl = c.profileImageUrl;
|
|
}
|
|
if (c.publicKeyBase64) {
|
|
contact.publicKeyBase64 = c.publicKeyBase64;
|
|
}
|
|
return contact;
|
|
});
|
|
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
|
contacts: selectedContacts,
|
|
});
|
|
// Use production URL for sharing to avoid localhost issues in development
|
|
const contactsJwtUrl = `${PROD_SHARE_DOMAIN}/deep-link/contact-import/${contactsJwt}`;
|
|
useClipboard()
|
|
.copy(contactsJwtUrl)
|
|
.then(() => {
|
|
// Use notification helper
|
|
this.notify.copied(NOTIFY_CONTACT_LINK_COPIED.message);
|
|
});
|
|
}
|
|
|
|
private showCopySelectionsInfo() {
|
|
// Use notification helper and constant
|
|
this.notify.info(NOTIFY_CONTACT_INFO_COPY.message, TIMEOUTS.LONG);
|
|
}
|
|
|
|
/**
|
|
* Show onboarding meeting dialog based on user's meeting status
|
|
* Checks if user is in a meeting and whether they are the host
|
|
*/
|
|
private async showOnboardMeetingDialog() {
|
|
try {
|
|
// First check if they're in a meeting
|
|
const headers = await getHeaders(this.activeDid);
|
|
const memberResponse = await this.axios.get(
|
|
this.apiServer + "/api/partner/groupOnboardMember",
|
|
{ headers },
|
|
);
|
|
|
|
if (memberResponse.data.data) {
|
|
// They're in a meeting, check if they're the host
|
|
const hostResponse = await this.axios.get(
|
|
this.apiServer + "/api/partner/groupOnboard",
|
|
{ headers },
|
|
);
|
|
|
|
if (hostResponse.data.data) {
|
|
// They're the host, take them to setup
|
|
this.$router.push({ name: "onboard-meeting-setup" });
|
|
} else {
|
|
// They're not the host, take them to list
|
|
this.$router.push({ name: "onboard-meeting-list" });
|
|
}
|
|
} else {
|
|
// They're not in a meeting, show the dialog
|
|
this.$notify(
|
|
{
|
|
group: "modal",
|
|
type: "confirm",
|
|
title: "Onboarding Meeting",
|
|
text: "Would you like to start a new meeting?",
|
|
onYes: async () => {
|
|
this.$router.push({ name: "onboard-meeting-setup" });
|
|
},
|
|
yesText: "Start New Meeting",
|
|
onNo: async () => {
|
|
this.$router.push({ name: "onboard-meeting-list" });
|
|
},
|
|
noText: "Join Existing Meeting",
|
|
},
|
|
-1,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
this.$logAndConsole(
|
|
"Error checking meeting status:" + errorStringForLog(error),
|
|
);
|
|
// Use notification helper
|
|
this.notify.error(NOTIFY_MEETING_STATUS_ERROR.message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle QR code button click - route to appropriate scanner
|
|
* Uses native scanner on mobile platforms, web scanner otherwise
|
|
*/
|
|
|
|
public handleQRCodeClick() {
|
|
console.log("[ContactsView] handleQRCodeClick method called");
|
|
this.$logAndConsole(
|
|
"[ContactsView] handleQRCodeClick method called",
|
|
false,
|
|
);
|
|
|
|
if (Capacitor.isNativePlatform()) {
|
|
console.log("[ContactsView] Navigating to contact-qr-scan-full");
|
|
this.$router.push({ name: "contact-qr-scan-full" });
|
|
} else {
|
|
console.log("[ContactsView] Navigating to contact-qr");
|
|
this.$router.push({ name: "contact-qr" });
|
|
}
|
|
}
|
|
}
|
|
</script>
|