forked from jsnbuchanan/crowd-funder-for-time-pwa
- Fix ShareMyContactInfoView.vue mounted() method to use Active Identity façade - Fix OnboardMeetingSetupView, OnboardMeetingMembersView, OnboardMeetingListView - Fix ContactAmountsView to use new Active Identity system - Replace all remaining settings?.activeDid usage with () These components were causing Playwright test failures due to routing issues when activeDid was not found in legacy settings.
509 lines
16 KiB
Vue
509 lines
16 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 mb-8">
|
|
Onboarding Meetings
|
|
</h1>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="flex justify-center items-center py-8">
|
|
<font-awesome icon="spinner" class="fa-spin-pulse" />
|
|
</div>
|
|
|
|
<div v-else-if="attendingMeeting">
|
|
<p>You are in this meeting.</p>
|
|
<div
|
|
class="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer"
|
|
@click="promptPassword(attendingMeeting)"
|
|
>
|
|
<div class="flex justify-between items-center">
|
|
<h2 class="text-xl font-medium">{{ attendingMeeting.name }}</h2>
|
|
<button
|
|
class="text-red-600 hover:text-red-700 p-2"
|
|
title="Leave Meeting"
|
|
@click.stop="leaveMeeting"
|
|
>
|
|
<font-awesome icon="right-from-bracket" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Meeting List -->
|
|
<div v-else class="space-y-4">
|
|
<div
|
|
v-for="meeting in meetings"
|
|
:key="meeting.groupId"
|
|
class="p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer"
|
|
@click="promptPassword(meeting)"
|
|
>
|
|
<h2 class="text-xl font-medium">{{ meeting.name }}</h2>
|
|
</div>
|
|
|
|
<!-- Create Meeting Button for registered users with no meetings -->
|
|
<div
|
|
v-if="meetings.length === 0 && isRegistered"
|
|
class="text-center py-8"
|
|
>
|
|
<p class="text-gray-500 mb-4">No onboarding meetings available</p>
|
|
<button
|
|
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-6 py-3 rounded-md hover:from-green-500 hover:to-green-800 transition-colors"
|
|
@click="createMeeting"
|
|
>
|
|
<font-awesome icon="plus" class="mr-2" />
|
|
Create Meeting
|
|
</button>
|
|
</div>
|
|
|
|
<!-- No meetings message for unregistered users -->
|
|
<p
|
|
v-if="meetings.length === 0 && !isRegistered"
|
|
class="text-center text-gray-500 py-8"
|
|
>
|
|
No onboarding meetings available
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Password Dialog -->
|
|
<div
|
|
v-if="showPasswordDialog"
|
|
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4"
|
|
>
|
|
<div class="bg-white rounded-lg p-6 max-w-sm w-full">
|
|
<h3 class="text-lg font-medium mb-4">Enter Meeting Password</h3>
|
|
<input
|
|
ref="passwordInput"
|
|
v-model="password"
|
|
type="text"
|
|
class="w-full px-3 py-2 border rounded-md mb-4"
|
|
placeholder="Enter password"
|
|
@keyup.enter="submitPassword"
|
|
/>
|
|
<div class="flex justify-end space-x-4">
|
|
<button
|
|
class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
|
|
@click="cancelPasswordDialog"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
@click="submitPassword"
|
|
>
|
|
Submit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { nextTick } from "vue";
|
|
import { Router } from "vue-router";
|
|
|
|
import QuickNav from "../components/QuickNav.vue";
|
|
import TopMessage from "../components/TopMessage.vue";
|
|
import { encryptMessage } from "../libs/crypto";
|
|
import {
|
|
errorStringForLog,
|
|
getHeaders,
|
|
serverMessageForUser,
|
|
} from "../libs/endorserServer";
|
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
|
import { NotificationIface } from "@/constants/app";
|
|
|
|
interface Meeting {
|
|
name: string;
|
|
groupId: number;
|
|
}
|
|
|
|
@Component({
|
|
components: {
|
|
QuickNav,
|
|
TopMessage,
|
|
},
|
|
mixins: [PlatformServiceMixin],
|
|
})
|
|
export default class OnboardMeetingListView extends Vue {
|
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
$router!: Router;
|
|
|
|
notify!: ReturnType<typeof createNotifyHelpers>;
|
|
|
|
activeDid = "";
|
|
apiServer = "";
|
|
attendingMeeting: Meeting | null = null;
|
|
firstName = "";
|
|
isLoading = false;
|
|
isRegistered = false;
|
|
meetings: Meeting[] = [];
|
|
password = "";
|
|
selectedMeeting: Meeting | null = null;
|
|
showPasswordDialog = false;
|
|
|
|
/**
|
|
* Vue lifecycle hook - component initialization
|
|
*
|
|
* Initializes the component by loading user settings and fetching available
|
|
* onboarding meetings. This method is called when the component is created
|
|
* and sets up all necessary data for the meeting list interface.
|
|
*
|
|
* Workflow:
|
|
* 1. Initialize notification system using createNotifyHelpers
|
|
* 2. Load user account settings (DID, API server, registration status)
|
|
* 3. Fetch available onboarding meetings from the server
|
|
*
|
|
* Dependencies:
|
|
* - PlatformServiceMixin for settings access ($accountSettings)
|
|
* - Server API for meeting data (fetchMeetings)
|
|
*
|
|
* Error Handling:
|
|
* - Server errors during meeting fetch are handled in fetchMeetings()
|
|
*
|
|
* @author Matthew Raymer
|
|
*/
|
|
async created() {
|
|
this.notify = createNotifyHelpers(this.$notify);
|
|
|
|
// Load user account settings
|
|
const settings = await this.$accountSettings();
|
|
|
|
// Use new Active Identity façade instead of settings.activeDid
|
|
this.activeDid = (await this.$getActiveDid()) || "";
|
|
this.apiServer = settings?.apiServer || "";
|
|
this.firstName = settings?.firstName || "";
|
|
this.isRegistered = !!settings?.isRegistered;
|
|
|
|
await this.fetchMeetings();
|
|
}
|
|
|
|
/**
|
|
* Fetches available onboarding meetings from the server
|
|
*
|
|
* This method retrieves the list of onboarding meetings that the user can join.
|
|
* It first checks if the user is already attending a meeting, and if so,
|
|
* displays that meeting instead of the full list.
|
|
*
|
|
* Workflow:
|
|
* 1. Check if user is already attending a meeting (groupOnboardMember endpoint)
|
|
* 2. If attending: Fetch meeting details and display single meeting view
|
|
* 3. If not attending: Fetch all available meetings (groupsOnboarding endpoint)
|
|
* 4. Handle loading states and error conditions
|
|
*
|
|
* API Endpoints Used:
|
|
* - GET /api/partner/groupOnboardMember - Check current attendance
|
|
* - GET /api/partner/groupOnboard/{id} - Get meeting details
|
|
* - GET /api/partner/groupsOnboarding - Get all available meetings
|
|
*
|
|
* State Management:
|
|
* - Sets isLoading flag during API calls
|
|
* - Updates attendingMeeting or meetings array
|
|
* - Handles error states with user notifications
|
|
*
|
|
* @author Matthew Raymer
|
|
*/
|
|
async fetchMeetings() {
|
|
this.isLoading = true;
|
|
try {
|
|
const headers = await getHeaders(this.activeDid);
|
|
|
|
const response = await this.axios.get(
|
|
this.apiServer + "/api/partner/groupOnboardMember",
|
|
{ headers },
|
|
);
|
|
|
|
if (response.data?.data) {
|
|
const attendingMeetingId = response.data.data.groupId;
|
|
|
|
const headers2 = await getHeaders(this.activeDid);
|
|
const response2 = await this.axios.get(
|
|
this.apiServer + "/api/partner/groupOnboard/" + attendingMeetingId,
|
|
{ headers: headers2 },
|
|
);
|
|
|
|
if (response2.data?.data) {
|
|
this.attendingMeeting = response2.data.data;
|
|
return;
|
|
} else {
|
|
this.$logAndConsole(
|
|
"Error fetching meeting for user after saying they are in one.",
|
|
true,
|
|
);
|
|
}
|
|
} else {
|
|
this.$logAndConsole(
|
|
"Error fetching meeting for user after saying they are in one.",
|
|
true,
|
|
);
|
|
}
|
|
|
|
const headers2 = await getHeaders(this.activeDid);
|
|
const response2 = await this.axios.get(
|
|
this.apiServer + "/api/partner/groupsOnboarding",
|
|
{ headers: headers2 },
|
|
);
|
|
|
|
if (response2.data?.data) {
|
|
this.meetings = response2.data.data;
|
|
}
|
|
} catch (error: unknown) {
|
|
this.$logAndConsole(
|
|
"Error fetching meetings: " + errorStringForLog(error),
|
|
true,
|
|
);
|
|
this.notify.error(
|
|
serverMessageForUser(error) || "There was a problem fetching meetings.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Opens the password dialog for joining a meeting
|
|
*
|
|
* This method initiates the process of joining an onboarding meeting by
|
|
* opening a modal dialog that prompts the user for the meeting password.
|
|
* The dialog is focused and ready for input when displayed.
|
|
*
|
|
* Workflow:
|
|
* 1. Clear any previous password input
|
|
* 2. Store the selected meeting for later use
|
|
* 3. Show the password dialog modal
|
|
* 4. Focus the password input field for immediate typing
|
|
*
|
|
* UI State Changes:
|
|
* - Sets showPasswordDialog to true (shows modal)
|
|
* - Clears password field for fresh input
|
|
* - Stores selectedMeeting for password submission
|
|
*
|
|
* @param meeting - The meeting object the user wants to join
|
|
* @author Matthew Raymer
|
|
*/
|
|
promptPassword(meeting: Meeting) {
|
|
this.password = "";
|
|
this.selectedMeeting = meeting;
|
|
this.showPasswordDialog = true;
|
|
nextTick(() => {
|
|
const input = this.$refs.passwordInput as HTMLInputElement;
|
|
if (input) {
|
|
input.focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Cancels the password dialog and resets state
|
|
*
|
|
* This method handles the cancellation of the meeting password dialog.
|
|
* It cleans up the dialog state and resets all related variables to
|
|
* their initial state, ensuring a clean slate for future dialog interactions.
|
|
*
|
|
* State Cleanup:
|
|
* - Clears password input field
|
|
* - Removes selected meeting reference
|
|
* - Hides password dialog modal
|
|
*
|
|
* This ensures that if the user reopens the dialog, they start with
|
|
* a fresh state without any leftover data from previous attempts.
|
|
*
|
|
* @author Matthew Raymer
|
|
*/
|
|
cancelPasswordDialog() {
|
|
this.password = "";
|
|
this.selectedMeeting = null;
|
|
this.showPasswordDialog = false;
|
|
}
|
|
|
|
/**
|
|
* Submits the password and joins the selected meeting
|
|
*
|
|
* This method handles the complete workflow of joining an onboarding meeting.
|
|
* It encrypts the user's member data with the provided password and sends
|
|
* it to the server to register the user as a meeting participant.
|
|
*
|
|
* Workflow:
|
|
* 1. Validate that a meeting is selected (safety check)
|
|
* 2. Create member data object with user information
|
|
* 3. Encrypt member data using the meeting password
|
|
* 4. Send encrypted data to server via groupOnboardMember endpoint
|
|
* 5. On success: Navigate to meeting members view with credentials
|
|
* 6. On failure: Show error notification to user
|
|
*
|
|
* Data Encryption:
|
|
* - Member data includes: name, DID, registration status
|
|
* - Data is encrypted using the meeting password for security
|
|
* - Encrypted data is sent to server for verification
|
|
*
|
|
* Navigation:
|
|
* - On successful join: Redirects to onboard-meeting-members view
|
|
* - Passes groupId, password, and memberId as route parameters
|
|
* - Allows user to see other meeting participants
|
|
*
|
|
* Error Handling:
|
|
* - Invalid passwords result in server rejection
|
|
* - Network errors are caught and displayed to user
|
|
* - All errors are logged for debugging purposes
|
|
*
|
|
* @author Matthew Raymer
|
|
*/
|
|
async submitPassword() {
|
|
if (!this.selectedMeeting) {
|
|
// this should never happen
|
|
this.$logAndConsole(
|
|
"No meeting selected when prompting for password, which should never happen.",
|
|
true,
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Create member data object
|
|
const memberData = {
|
|
name: this.firstName,
|
|
did: this.activeDid,
|
|
isRegistered: this.isRegistered,
|
|
};
|
|
const memberDataString = JSON.stringify(memberData);
|
|
const encryptedMemberData = await encryptMessage(
|
|
memberDataString,
|
|
this.password,
|
|
);
|
|
|
|
// Get headers for authentication
|
|
const headers = await getHeaders(this.activeDid);
|
|
|
|
// Encrypt the member data
|
|
const postResult = await this.axios.post(
|
|
this.apiServer + "/api/partner/groupOnboardMember",
|
|
{
|
|
groupId: this.selectedMeeting.groupId,
|
|
content: encryptedMemberData,
|
|
},
|
|
{ headers },
|
|
);
|
|
|
|
if (postResult.data && postResult.data.success) {
|
|
// Navigate to members view with password and groupId
|
|
this.$router.push({
|
|
name: "onboard-meeting-members",
|
|
params: {
|
|
groupId: this.selectedMeeting.groupId.toString(),
|
|
},
|
|
query: {
|
|
password: this.password,
|
|
memberId: postResult.data.memberId,
|
|
},
|
|
});
|
|
|
|
this.cancelPasswordDialog();
|
|
} else {
|
|
throw { response: postResult };
|
|
}
|
|
} catch (error) {
|
|
this.$logAndConsole(
|
|
"Error joining meeting: " + errorStringForLog(error),
|
|
true,
|
|
);
|
|
this.notify.error(
|
|
serverMessageForUser(error) ||
|
|
"There was a problem joining the meeting.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prompts user to confirm leaving the current meeting
|
|
*
|
|
* This method initiates the process of leaving an onboarding meeting.
|
|
* It shows a confirmation dialog to prevent accidental departures,
|
|
* then handles the server-side removal and UI updates.
|
|
*
|
|
* Workflow:
|
|
* 1. Display confirmation dialog asking user to confirm departure
|
|
* 2. On confirmation: Send DELETE request to groupOnboardMember endpoint
|
|
* 3. On success: Clear attending meeting state and refresh meeting list
|
|
* 4. Show success notification to user
|
|
* 5. On failure: Show error notification with details
|
|
*
|
|
* Server Interaction:
|
|
* - DELETE /api/partner/groupOnboardMember - Removes user from meeting
|
|
* - Requires authentication headers for user verification
|
|
* - Server handles the actual removal from meeting database
|
|
*
|
|
* State Management:
|
|
* - Clears attendingMeeting when successfully left
|
|
* - Refreshes meetings list to show updated availability
|
|
* - Updates UI to show meeting list instead of single meeting
|
|
*
|
|
* User Experience:
|
|
* - Confirmation prevents accidental departures
|
|
* - Clear feedback on success/failure
|
|
* - Seamless transition back to meeting list
|
|
*
|
|
* @author Matthew Raymer
|
|
*/
|
|
async leaveMeeting() {
|
|
this.notify.confirm(
|
|
"Are you sure you want to leave this meeting?",
|
|
async () => {
|
|
try {
|
|
const headers = await getHeaders(this.activeDid);
|
|
await this.axios.delete(
|
|
this.apiServer + "/api/partner/groupOnboardMember",
|
|
{ headers },
|
|
);
|
|
|
|
this.attendingMeeting = null;
|
|
await this.fetchMeetings();
|
|
|
|
this.notify.success("You left the meeting.", TIMEOUTS.LONG);
|
|
} catch (error) {
|
|
this.$logAndConsole(
|
|
"Error leaving meeting: " + errorStringForLog(error),
|
|
true,
|
|
);
|
|
this.notify.error(
|
|
serverMessageForUser(error) ||
|
|
"There was a problem leaving the meeting.",
|
|
TIMEOUTS.LONG,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Navigates to the meeting creation page
|
|
*
|
|
* This method handles the navigation to the meeting setup page where
|
|
* registered users can create new onboarding meetings. It's only
|
|
* available to users who are registered in the system.
|
|
*
|
|
* Navigation:
|
|
* - Routes to onboard-meeting-setup view
|
|
* - Allows user to configure new meeting settings
|
|
* - Only accessible to registered users (controlled by template)
|
|
*
|
|
* User Flow:
|
|
* - User clicks "Create Meeting" button
|
|
* - System navigates to setup page
|
|
* - User can configure meeting name, password, etc.
|
|
* - New meeting becomes available to other users
|
|
*
|
|
* @author Matthew Raymer
|
|
*/
|
|
createMeeting() {
|
|
this.$router.push({ name: "onboard-meeting-setup" });
|
|
}
|
|
}
|
|
</script>
|