diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 0b80972..ab9c4fc 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -690,6 +690,7 @@ export function serverMessageForUser(error: any) { /** * Helpful for server errors, to get all the info -- including stuff skipped by toString & JSON.stringify + * It works with AxiosError, eg handling an error.response intelligently. * * @param error */ diff --git a/src/router/index.ts b/src/router/index.ts index 4e6e354..3bb330e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -180,9 +180,19 @@ const routes: Array = [ component: () => import("../views/OfferDetailsView.vue"), }, { - path: '/onboard-meeting', - name: 'onboard-meeting', - component: () => import('../views/OnboardMeetingView.vue'), + path: '/onboard-meeting-list', + name: 'onboard-meeting-list', + component: () => import('../views/OnboardMeetingListView.vue'), + }, + { + path: '/onboard-meeting-members/:groupId', + name: 'onboard-meeting-members', + component: () => import('../views/OnboardMeetingMembersView.vue'), + }, + { + path: '/onboard-meeting-setup', + name: 'onboard-meeting-setup', + component: () => import('../views/OnboardMeetingSetupView.vue'), }, { path: "/project/:id?", diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 3dab9fe..9ce3679 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -31,7 +31,7 @@ diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 3f5f46d..2db6c18 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -960,7 +960,7 @@ export default class HomeView extends Vue { option2Text: "We are nearby with cameras", option3Text: "We will share some other way", onOption1: () => { - (this.$router as Router).push({ name: "onboarding-meeting" }); + (this.$router as Router).push({ name: "onboard-meeting-list" }); }, onOption2: () => { (this.$router as Router).push({ name: "contact-qr" }); diff --git a/src/views/OnboardMeetingListView.vue b/src/views/OnboardMeetingListView.vue new file mode 100644 index 0000000..78903ed --- /dev/null +++ b/src/views/OnboardMeetingListView.vue @@ -0,0 +1,197 @@ + + + \ No newline at end of file diff --git a/src/views/OnboardMeetingMembersView.vue b/src/views/OnboardMeetingMembersView.vue new file mode 100644 index 0000000..6296ed6 --- /dev/null +++ b/src/views/OnboardMeetingMembersView.vue @@ -0,0 +1,157 @@ + + + \ No newline at end of file diff --git a/src/views/OnboardMeetingView.vue b/src/views/OnboardMeetingSetupView.vue similarity index 51% rename from src/views/OnboardMeetingView.vue rename to src/views/OnboardMeetingSetupView.vue index ec9769a..b181b3d 100644 --- a/src/views/OnboardMeetingView.vue +++ b/src/views/OnboardMeetingSetupView.vue @@ -9,14 +9,33 @@ -
+

Current Meeting

-

Name: {{ existingMeeting.name }}

-

Expires: {{ formatExpirationTime(existingMeeting.expiresAt) }}

-

Share the the password with the people you want to onboard.

+

Name: {{ currentMeeting.name }}

+

Expires: {{ formatExpirationTime(currentMeeting.expiresAt) }}

+
+

Share the password with the people you want to onboard.

+ + Go to Meeting Members + +
+ + The meeting password has been lost. Edit it, or delete and create a new meeting. +
-
+
+
- -
-

Create New Meeting

-
+ +
+

{{ isEditing ? 'Edit Meeting' : 'Create New Meeting' }}

+ +
Meeting Expiration Time Meeting Password Your Name - {{ isLoading ? 'Creating...' : 'Create Meeting' }} + {{ isLoading ? (isEditing ? 'Updating...' : 'Creating...') : (isEditing ? 'Update Meeting' : 'Create Meeting') }} + +
+
@@ -125,18 +154,23 @@ import { Component, Vue } from 'vue-facing-decorator'; import QuickNav from '@/components/QuickNav.vue'; import TopMessage from '@/components/TopMessage.vue'; -import { retrieveSettingsForActiveAccount } from '@/db/index'; -import { getHeaders, serverMessageForUser } from '@/libs/endorserServer'; +import { logConsoleAndDb, retrieveSettingsForActiveAccount } from '@/db/index'; +import { errorStringForLog, getHeaders, serverMessageForUser } from '@/libs/endorserServer'; import { encryptMessage } from '@/libs/crypto'; -interface Meeting { - groupId?: string; - name: string; - expiresAt: string; +interface ServerMeeting { + groupId: string; // from the server + name: string; // from the server + expiresAt: string; // from the server + userFullName?: string; // from the user's session + password?: string; // from the user's session } -interface NewMeeting extends Meeting { +interface MeetingSetupInfo { + name: string; + expiresAt: string; userFullName: string; + password: string; } @Component({ @@ -148,19 +182,15 @@ interface NewMeeting extends Meeting { export default class OnboardMeetingView extends Vue { $notify!: (notification: { group: string; type: string; title: string; text: string }, timeout?: number) => void; - existingMeeting: Meeting | null = null; - currentMeeting: NewMeeting = { - name: '', - expiresAt: this.getDefaultExpirationTime(), - userFullName: '', - }; - password = ''; - isLoading = false; + currentMeeting: ServerMeeting | null = null; + newOrUpdatedMeeting: MeetingSetupInfo | null = null; activeDid = ''; apiServer = ''; isDeleting = false; + isEditing = false; + isLoading = true; showDeleteConfirm = false; - + fullName = ''; get minDateTime() { const now = new Date(); now.setMinutes(now.getMinutes() + 5); // Set minimum 5 minutes in the future @@ -171,9 +201,10 @@ export default class OnboardMeetingView extends Vue { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ''; this.apiServer = settings.apiServer || ''; - this.currentMeeting.userFullName = settings.firstName || ''; + this.fullName = settings.firstName || ''; - await this.fetchExistingMeeting(); + await this.fetchCurrentMeeting(); + this.isLoading = false; } getDefaultExpirationTime(): string { @@ -198,7 +229,17 @@ export default class OnboardMeetingView extends Vue { return `${year}-${month}-${day}T${hours}:${minutes}`; } - async fetchExistingMeeting() { + blankMeeting(): MeetingSetupInfo { + return { + // no groupId yet + name: '', + expiresAt: this.getDefaultExpirationTime(), + userFullName: this.fullName, + password: this.currentMeeting?.password || "", + }; + } + + async fetchCurrentMeeting() { try { const headers = await getHeaders(this.activeDid); const response = await this.axios.get( @@ -206,19 +247,21 @@ export default class OnboardMeetingView extends Vue { { headers } ); - if (response.data && response.data.data) { - this.existingMeeting = response.data.data; + if (response?.data?.data) { + console.log('Response data', response.data.data); + this.currentMeeting = { + ...response.data.data, + userFullName: this.fullName, + password: this.currentMeeting?.password || "", + }; + console.log('Current meeting', this.currentMeeting); + } else { + // no meeting found + this.newOrUpdatedMeeting = this.blankMeeting(); } } catch (error) { - console.log('Error fetching existing meeting:', error); - this.$notify( - { - group: 'alert', - type: 'danger', - title: 'Error', - text: serverMessageForUser(error) || 'Failed to fetch existing meeting.', - }, - ); + // no meeting found + this.newOrUpdatedMeeting = this.blankMeeting(); } } @@ -226,8 +269,12 @@ export default class OnboardMeetingView extends Vue { this.isLoading = true; try { + if (!this.newOrUpdatedMeeting) { + throw Error('There was no meeting data to create. We should never get here.'); + } + // Convert local time to UTC for comparison and server submission - const localExpiresAt = new Date(this.currentMeeting.expiresAt); + const localExpiresAt = new Date(this.newOrUpdatedMeeting.expiresAt); const now = new Date(); if (localExpiresAt <= now) { this.$notify( @@ -241,19 +288,44 @@ export default class OnboardMeetingView extends Vue { ); return; } + if (!this.newOrUpdatedMeeting.userFullName) { + this.$notify( + { + group: 'alert', + type: 'warning', + title: 'Invalid Name', + text: 'Please enter your name.', + }, + 5000 + ); + return; + } + if (!this.newOrUpdatedMeeting.password) { + this.$notify( + { + group: 'alert', + type: 'warning', + title: 'Invalid Password', + text: 'Please enter a password.', + }, + 5000 + ); + return; + } + // create content with user's name and DID encrypted with password const content = { - name: this.currentMeeting.userFullName, + name: this.newOrUpdatedMeeting.userFullName, did: this.activeDid, }; - const encryptedContent = await encryptMessage(JSON.stringify(content), this.password); + const encryptedContent = await encryptMessage(JSON.stringify(content), this.newOrUpdatedMeeting.password); const headers = await getHeaders(this.activeDid); const response = await this.axios.post( this.apiServer + '/api/partner/groupOnboard', { - name: this.currentMeeting.name, + name: this.newOrUpdatedMeeting.name, expiresAt: localExpiresAt.toISOString(), content: encryptedContent, }, @@ -261,11 +333,11 @@ export default class OnboardMeetingView extends Vue { ); if (response.data && response.data.success) { - this.existingMeeting = { - groupId: response.data.groupId, - name: this.currentMeeting.name, - expiresAt: localExpiresAt.toISOString(), + this.currentMeeting = { + ...this.newOrUpdatedMeeting, + groupId: response.data.success.groupId, }; + this.newOrUpdatedMeeting = null; this.$notify( { group: 'alert', @@ -276,10 +348,10 @@ export default class OnboardMeetingView extends Vue { 3000 ); } else { - throw new Error('Failed to create meeting due to unexpected response data: ' + JSON.stringify(response.data)); + throw { response: response }; } } catch (error) { - console.error('Error creating meeting:', error); + logConsoleAndDb('Error creating meeting: ' + errorStringForLog(error), true); const errorMessage = serverMessageForUser(error); this.$notify( { @@ -324,7 +396,8 @@ export default class OnboardMeetingView extends Vue { { headers } ); - this.existingMeeting = null; + this.currentMeeting = null; + this.newOrUpdatedMeeting = this.blankMeeting(); this.showDeleteConfirm = false; this.$notify( @@ -351,5 +424,123 @@ export default class OnboardMeetingView extends Vue { this.isDeleting = false; } } + + startEditing() { + this.isEditing = true; + // Populate form with existing meeting data + if (this.currentMeeting) { + console.log('Current meeting', this.currentMeeting); + const localExpiresAt = new Date(this.currentMeeting.expiresAt); + this.newOrUpdatedMeeting = { + name: this.currentMeeting.name, + expiresAt: this.formatDateForInput(localExpiresAt), + userFullName: this.currentMeeting.userFullName || '', + password: this.currentMeeting.password || '', + }; + } else { + console.error('There is no current meeting to edit. We should never get here.'); + } + } + + cancelEditing() { + this.isEditing = false; + // Reset form data + this.newOrUpdatedMeeting = null; + } + + async updateMeeting() { + this.isLoading = true; + if (!this.newOrUpdatedMeeting) { + throw Error('There was no meeting data to update.'); + } + + try { + // Convert local time to UTC for comparison and server submission + const localExpiresAt = new Date(this.newOrUpdatedMeeting.expiresAt); + const now = new Date(); + if (localExpiresAt <= now) { + this.$notify( + { + group: 'alert', + type: 'warning', + title: 'Invalid Time', + text: 'Select a future time for the meeting expiration.', + }, + 5000 + ); + return; + } + if (!this.newOrUpdatedMeeting.userFullName) { + this.$notify( + { + group: 'alert', + type: 'warning', + title: 'Invalid Name', + text: 'Please enter your name.', + }, + 5000 + ); + return; + } + if (!this.newOrUpdatedMeeting.password) { + this.$notify( + { + group: 'alert', + type: 'warning', + title: 'Invalid Password', + text: 'Please enter a password.', + }, + 5000 + ); + return; + } + // create content with user's name and DID encrypted with password + const content = { + name: this.newOrUpdatedMeeting.userFullName, + did: this.activeDid, + }; + const encryptedContent = await encryptMessage(JSON.stringify(content), this.newOrUpdatedMeeting.password); + + const headers = await getHeaders(this.activeDid); + const response = await this.axios.put( + this.apiServer + '/api/partner/groupOnboard', + { + // the groupId is in the currentMeeting but it's not necessary while users only have one meeting + name: this.newOrUpdatedMeeting.name, + expiresAt: localExpiresAt.toISOString(), + content: encryptedContent, + }, + { headers } + ); + + if (response.data && response.data.success) { + console.log('Updated meeting', response.data); + // Update the current meeting with only the necessary fields + this.currentMeeting = { + ...this.newOrUpdatedMeeting, + groupId: this.currentMeeting?.groupId || "", + }; + this.newOrUpdatedMeeting = null; + console.log('Updated meeting now', this.currentMeeting); + this.isEditing = false; + } else { + throw { response: response }; + } + } catch (error) { + logConsoleAndDb('Error updating meeting: ' + errorStringForLog(error), true); + const errorMessage = serverMessageForUser(error); + this.$notify( + { + group: 'alert', + type: 'danger', + title: 'Error', + text: errorMessage || 'Failed to update meeting. Try reloading or submitting again.', + }, + 5000 + ); + } finally { + this.isLoading = false; + } + } } \ No newline at end of file