You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1265 lines
41 KiB
1265 lines
41 KiB
<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 message
|
|
const addedMessage = await this.handleContactVisibility(newContact);
|
|
|
|
// Clear input field
|
|
this.contactInput = "";
|
|
|
|
// Handle registration prompt if needed
|
|
await this.handleRegistrationPrompt(newContact);
|
|
|
|
// Show success notification
|
|
this.notify.success(addedMessage);
|
|
} 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 message
|
|
*/
|
|
private async handleContactVisibility(newContact: Contact): Promise<string> {
|
|
if (this.activeDid) {
|
|
await this.setVisibility(newContact, true, false);
|
|
newContact.seesMe = true;
|
|
return NOTIFY_CONTACTS_ADDED_VISIBLE.message;
|
|
} else {
|
|
return 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>
|
|
|