+ );
+};
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/constants.js b/src/applications/_mock-form-ae-design-patterns/vadx/constants.js
new file mode 100644
index 000000000000..15e96f2c5906
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/constants.js
@@ -0,0 +1,7 @@
+const constants = {
+ API_BASE_URL: 'http://localhost:1337',
+ FRONTEND_PROCESS_NAME: 'frontend-server',
+ MOCK_SERVER_PROCESS_NAME: 'mock-server',
+};
+
+module.exports = constants;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/context/processManager.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/context/processManager.jsx
new file mode 100644
index 000000000000..5f9a9c87cf66
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/context/processManager.jsx
@@ -0,0 +1,239 @@
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useRef,
+ useCallback,
+ useMemo,
+} from 'react';
+import PropTypes from 'prop-types';
+import { formatDate } from '../utils/dates';
+import { API_BASE_URL } from '../constants';
+
+const ProcessManagerContext = createContext(null);
+
+export const ProcessManagerProvider = ({ children }) => {
+ const [processes, setProcesses] = useState({});
+ const [activeApps, setActiveApps] = useState({});
+ const [output, setOutput] = useState({});
+ const eventSourcesRef = useRef({});
+ const [manifests, setManifests] = useState([]);
+
+ const handleSSEMessage = useCallback((processName, event) => {
+ const parsedEvent = JSON.parse(event.data);
+
+ if (parsedEvent.type === 'status') {
+ if (parsedEvent.data === 'stopped') {
+ // remove the process from the output
+ setOutput(prev => {
+ // eslint-disable-next-line no-unused-vars
+ const { [processName]: _, ...rest } = prev;
+ return rest;
+ });
+ return;
+ }
+ setProcesses(prev => ({
+ ...prev,
+ [processName]: {
+ ...prev[processName],
+ status: parsedEvent.data.status,
+ lastUpdate: parsedEvent.data.timestamp,
+ metadata: parsedEvent.data.metadata,
+ },
+ }));
+ return;
+ }
+
+ if (parsedEvent.type === 'stdout' || parsedEvent.type === 'stderr') {
+ setOutput(prev => ({
+ ...prev,
+ [processName]: [
+ {
+ id: Date.now(),
+ friendlyDate: formatDate(new Date().toISOString()),
+ ...parsedEvent,
+ },
+ ...(prev[processName] || []),
+ ],
+ }));
+ }
+
+ if (parsedEvent.type === 'cache') {
+ setOutput(prev => ({
+ ...prev,
+ [processName]: [
+ ...(prev[processName] || []),
+ ...parsedEvent.data.map((line, index) => ({
+ id: `${processName}-cache-${index}`,
+ friendlyDate: formatDate(new Date().toISOString()),
+ data: line,
+ })),
+ ],
+ }));
+ }
+ }, []);
+
+ // Move all the event source and process management logic here
+ const setupEventSource = useCallback(
+ processName => {
+ const eventSource = new EventSource(
+ `${API_BASE_URL}/events/${processName}`,
+ );
+
+ eventSource.onmessage = event => {
+ handleSSEMessage(processName, event);
+ };
+
+ eventSource.onerror = () => {
+ eventSource.close();
+ delete eventSourcesRef.current[processName];
+ };
+
+ return eventSource;
+ },
+ [handleSSEMessage],
+ );
+
+ const fetchStatus = useCallback(
+ async () => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/status`);
+ const { processes: processStatus, apps } = await response.json();
+ setProcesses(processStatus);
+ setActiveApps(apps);
+
+ // Setup or tear down event sources based on process status
+ Object.keys(processStatus).forEach(processName => {
+ if (
+ processStatus[processName] &&
+ !eventSourcesRef.current[processName]
+ ) {
+ eventSourcesRef.current[processName] = setupEventSource(
+ processName,
+ );
+ } else if (
+ !processStatus[processName] &&
+ eventSourcesRef.current[processName]
+ ) {
+ eventSourcesRef.current[processName].close();
+ delete eventSourcesRef.current[processName];
+ }
+ });
+ } catch (error) {
+ setProcesses({});
+ setActiveApps([]);
+ }
+ },
+ [setupEventSource],
+ ); // Empty dependency array since it only uses stable references
+
+ const fetchManifests = useCallback(async () => {
+ try {
+ const response = await fetch(`${API_BASE_URL}/manifests`);
+ const data = await response.json();
+ setManifests(data.manifests);
+ return data.manifests;
+ } catch (error) {
+ return [];
+ }
+ }, []); // Empty dependency array since it only uses stable references
+
+ const startProcess = async (processName, processConfig) => {
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
+
+ const response = await fetch(`${API_BASE_URL}/start-${processName}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(processConfig),
+ signal: controller.signal,
+ });
+
+ clearTimeout(timeoutId);
+
+ if (response.ok) {
+ delete eventSourcesRef.current[processName];
+ fetchStatus();
+ return true;
+ }
+
+ throw new Error('Failed to start process');
+ } catch (error) {
+ if (error.name === 'AbortError') {
+ // remove process from event sources
+ delete eventSourcesRef.current[processName];
+ fetchStatus();
+ return true;
+ }
+ return false;
+ }
+ };
+
+ const stopProcess = async (processName, port) => {
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5s timeout
+
+ const response = await fetch(`${API_BASE_URL}/stop`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ port }),
+ signal: controller.signal,
+ });
+
+ clearTimeout(timeoutId);
+
+ if (response.ok) {
+ if (eventSourcesRef.current[processName]) {
+ eventSourcesRef.current[processName].close();
+ delete eventSourcesRef.current[processName];
+ }
+ fetchStatus();
+ return true;
+ }
+
+ throw new Error('Failed to stop process');
+ } catch (error) {
+ if (error.name === 'AbortError') {
+ // This is expected when server restarts
+ fetchStatus();
+ return true;
+ }
+ return false;
+ }
+ };
+
+ const value = {
+ processes,
+ output,
+ startProcess,
+ stopProcess,
+ fetchStatus,
+ setupEventSource,
+ manifests,
+ fetchManifests,
+ activeApps: useMemo(() => activeApps, [activeApps]),
+ setOutput,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+ProcessManagerProvider.propTypes = {
+ children: PropTypes.node.isRequired,
+};
+
+export const useProcessManager = () => {
+ const context = useContext(ProcessManagerContext);
+ if (!context) {
+ throw new Error(
+ 'useProcessManager must be used within a ProcessManagerProvider',
+ );
+ }
+ return context;
+};
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/context/vadx.js b/src/applications/_mock-form-ae-design-patterns/vadx/context/vadx.js
index 98bffcc69624..18b824fd7c37 100644
--- a/src/applications/_mock-form-ae-design-patterns/vadx/context/vadx.js
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/context/vadx.js
@@ -94,6 +94,12 @@ export const VADXProvider = ({ children }) => {
[broadcastChannel],
);
+ const createUpdateHandlerByKey = key => {
+ return update => {
+ setSyncedData({ ...preferences, [key]: update });
+ };
+ };
+
// update the loading state for the dev tools
const updateDevLoading = isLoading => {
setSyncedData({ ...preferences, isDevLoading: isLoading });
@@ -117,6 +123,9 @@ export const VADXProvider = ({ children }) => {
setSyncedData({ ...preferences, showVADX: show });
};
+ const updateFeApi = createUpdateHandlerByKey('feApiUrl');
+ const updateBeApi = createUpdateHandlerByKey('beApiUrl');
+
// update local toggles
const updateLocalToggles = useCallback(
async toggles => {
@@ -148,10 +157,12 @@ export const VADXProvider = ({ children }) => {
return (
{
updateShowVADX,
updateLocalToggles,
updateClearLocalToggles,
+ updateFeApi,
+ updateBeApi,
debouncedSetSearchQuery,
- togglesLoading,
- togglesState,
}}
>
{children}
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/index.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/index.jsx
index 6efb6c00ecd3..cf765d8cd241 100644
--- a/src/applications/_mock-form-ae-design-patterns/vadx/index.jsx
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/index.jsx
@@ -1,5 +1,6 @@
import React, { Suspense } from 'react';
import PropTypes from 'prop-types';
+import { Servers } from './app/pages/servers/Servers';
import { VADXProvider } from './context/vadx';
import { VADXPanelLoader } from './panel/VADXPanelLoader';
@@ -33,3 +34,5 @@ VADX.propTypes = {
featureToggleName: PropTypes.string,
plugin: PropTypes.object,
};
+
+export { Servers as VADXServersRoute };
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/panel/tabs/form/FormTab.jsx b/src/applications/_mock-form-ae-design-patterns/vadx/panel/tabs/form/FormTab.jsx
index 9830ee426e71..4634aff3f3ec 100644
--- a/src/applications/_mock-form-ae-design-patterns/vadx/panel/tabs/form/FormTab.jsx
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/panel/tabs/form/FormTab.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import { Link, withRouter } from 'react-router';
import { useSelector } from 'react-redux';
+import PropTypes from 'prop-types';
import ChapterAnalyzer from './ChapterAnalyzer';
import { FormDataViewer } from './FormDataViewer';
@@ -69,4 +70,8 @@ const FormTabBase = props => {
);
};
+FormTabBase.propTypes = {
+ router: PropTypes.object.isRequired,
+};
+
export const FormTab = withRouter(FormTabBase);
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/constants/mockServerPaths.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/constants/mockServerPaths.js
new file mode 100644
index 000000000000..82bfed18a4c2
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/constants/mockServerPaths.js
@@ -0,0 +1,44 @@
+const MOCK_SERVER_PATHS = [
+ 'src/applications/_mock-form-ae-design-patterns/mocks/server.js',
+ 'src/applications/appeals/shared/tests/mock-api.js',
+ 'src/applications/avs/api/mocks/index.js',
+ 'src/applications/check-in/api/local-mock-api/index.js',
+ 'src/applications/combined-debt-portal/combined/utils/mocks/mockServer.js',
+ 'src/applications/disability-benefits/2346/mocks/index.js',
+ 'src/applications/disability-benefits/all-claims/local-dev-mock-api/index.js',
+ 'src/applications/education-letters/testing/response.js',
+ 'src/applications/financial-status-report/mocks/responses.js',
+ 'src/applications/health-care-supply-reordering/mocks/index.js',
+ 'src/applications/ivc-champva/10-10D/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/ivc-champva/10-7959C/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/ivc-champva/10-7959f-1/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/mhv-landing-page/mocks/api/index.js',
+ 'src/applications/mhv-medications/mocks/api/index.js',
+ 'src/applications/mhv-secure-messaging/api/mocks/index.js',
+ 'src/applications/mhv-supply-reordering/mocks/index.js',
+ 'src/applications/my-education-benefits/testing/responses.js',
+ 'src/applications/personalization/dashboard/mocks/server.js',
+ 'src/applications/personalization/notification-center/mocks/server.js',
+ 'src/applications/personalization/profile/mocks/server.js',
+ 'src/applications/personalization/review-information/tests/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/post-911-gib-status/mocks/server.js',
+ 'src/applications/representative-appoint/mocks/server.js',
+ 'src/applications/simple-forms/20-10206/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/simple-forms/20-10207/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/simple-forms/21-0845/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/simple-forms/21-0966/tests/e2e/fixtures/mocks/local-mock-api-responses.js',
+ 'src/applications/simple-forms/21-0972/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/simple-forms/21-10210/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/simple-forms/21-4138/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/simple-forms/21-4142/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/simple-forms/40-0247/tests/e2e/fixtures/mocks/local-mock-api-reponses.js',
+ 'src/applications/simple-forms/form-upload/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/simple-forms/mock-simple-forms-patterns/tests/e2e/fixtures/mocks/local-mock-responses.js',
+ 'src/applications/travel-pay/services/mocks/index.js',
+ 'src/applications/vaos/services/mocks/index.js',
+ 'src/platform/mhv/api/mocks/index.js',
+ 'src/platform/mhv/downtime/mocks/api/index.js',
+ 'src/platform/testing/local-dev-mock-api/common.js',
+];
+
+module.exports = { MOCK_SERVER_PATHS };
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/index.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/index.js
new file mode 100644
index 000000000000..1c7ccebc8b5c
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/index.js
@@ -0,0 +1,34 @@
+/* eslint-disable no-console */
+const express = require('express');
+
+const cors = require('./utils/cors');
+const { initializeManifests } = require('./utils/manifests');
+const { autoStartServers } = require('./utils/processes');
+const parseArgs = require('./utils/parseArgs');
+
+const app = express();
+const port = 1337;
+const args = parseArgs();
+
+app.use(express.json());
+
+// Allow CORS
+app.use(cors);
+
+const router = express.Router();
+
+router.use(require('./routes/events'));
+router.use(require('./routes/manifests'));
+router.use(require('./routes/output'));
+router.use(require('./routes/start-mock-server'));
+router.use(require('./routes/start-frontend-server'));
+router.use(require('./routes/status'));
+router.use(require('./routes/stop-on-port'));
+
+app.use(router);
+
+app.listen(port, async () => {
+ await initializeManifests();
+ await autoStartServers(args);
+ console.log(`Process manager server listening at http://localhost:${port}`);
+});
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/events.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/events.js
new file mode 100644
index 000000000000..3dc223a0ea15
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/events.js
@@ -0,0 +1,36 @@
+const express = require('express');
+const { outputCache, clients, sendSSE } = require('../utils/processes');
+
+const router = express.Router();
+
+router.get('/events/:name', (req, res) => {
+ const { name } = req.params;
+
+ res.writeHead(200, {
+ 'Content-Type': 'text/event-stream',
+ 'Cache-Control': 'no-cache',
+ Connection: 'keep-alive',
+ });
+
+ // Send the current cache immediately
+ if (outputCache[name]) {
+ sendSSE(res, { type: 'cache', data: outputCache[name] });
+ }
+
+ // Add this client to the list of clients for this process
+ if (!clients.has(name)) {
+ clients.set(name, []);
+ }
+ clients.get(name).push(res);
+
+ // Remove the client when the connection is closed
+ req.on('close', () => {
+ const clientsForProcess = clients.get(name) || [];
+ const index = clientsForProcess.indexOf(res);
+ if (index !== -1) {
+ clientsForProcess.splice(index, 1);
+ }
+ });
+});
+
+module.exports = router;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/manifests.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/manifests.js
new file mode 100644
index 000000000000..529a411380fe
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/manifests.js
@@ -0,0 +1,15 @@
+const express = require('express');
+const { getCachedManifests } = require('../utils/manifests');
+
+const router = express.Router();
+
+router.get('/manifests', (req, res) => {
+ const manifests = getCachedManifests();
+ res.json({
+ success: true,
+ count: manifests.length,
+ manifests,
+ });
+});
+
+module.exports = router;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/output.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/output.js
new file mode 100644
index 000000000000..d267b03066c5
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/output.js
@@ -0,0 +1,21 @@
+const express = require('express');
+const { outputCache } = require('../utils/processes');
+
+const router = express.Router();
+
+router.get('/output/:name', (req, res) => {
+ const { name } = req.params;
+ if (outputCache[name]) {
+ res.json(outputCache[name]);
+ } else {
+ res
+ .status(404)
+ .json({ error: `No output cache found for process ${name}` });
+ }
+});
+
+router.get('/output', (req, res) => {
+ res.json({ all: outputCache });
+});
+
+module.exports = router;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-frontend-server.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-frontend-server.js
new file mode 100644
index 000000000000..b4e3f4a042fd
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-frontend-server.js
@@ -0,0 +1,85 @@
+const express = require('express');
+const { killProcessOnPort } = require('../utils/processes');
+const { startProcess } = require('../utils/processes');
+const paths = require('../utils/paths');
+const logger = require('../utils/logger');
+const { getCachedManifests } = require('../utils/manifests');
+const { FRONTEND_PROCESS_NAME } = require('../../constants');
+
+const router = express.Router();
+
+router.post('/start-frontend-server', async (req, res) => {
+ const { entries = [] } = req.body;
+
+ if (!Array.isArray(entries) || entries.length === 0) {
+ return res.status(400).json({
+ success: false,
+ message: 'Request body must include an array of entry strings',
+ });
+ }
+
+ try {
+ const manifests = getCachedManifests();
+
+ // Find manifests that match the requested entries
+ const validManifests = entries
+ .map(entry => manifests.find(manifest => manifest.entryName === entry))
+ .filter(Boolean);
+
+ // check if we found all requested entries
+ if (validManifests.length !== entries.length) {
+ const foundEntries = validManifests.map(m => m.entryName);
+ const invalidEntries = entries.filter(
+ entry => !foundEntries.includes(entry),
+ );
+
+ // provides the invalid entries and the available entries
+ // might remove the available entries in the future just to be more consistent
+ // but this way we can provide the user with the available entries
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid entry names provided',
+ invalidEntries,
+ availableEntries: manifests.map(m => m.entryName).filter(Boolean),
+ });
+ }
+
+ // Use only the validated entry names from the manifests
+ // this way user input is not used to start the server
+ const validatedEntries = validManifests.map(m => m.entryName);
+
+ await killProcessOnPort('3001');
+
+ const result = await startProcess(
+ FRONTEND_PROCESS_NAME,
+ 'yarn',
+ [
+ '--cwd',
+ paths.root,
+ 'watch',
+ '--env',
+ `entry=${validatedEntries.join(',')}`,
+ 'api=http://localhost:3000',
+ ],
+ {
+ forceRestart: true,
+ metadata: {
+ entries: validatedEntries,
+ },
+ },
+ );
+
+ logger.debug('result', result);
+
+ return res.status(200).json(result);
+ } catch (error) {
+ logger.error('Error in /start-frontend-server:', error);
+ return res.status(500).json({
+ success: false,
+ message: 'Failed to validate entries',
+ error: error.message,
+ });
+ }
+});
+
+module.exports = router;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-mock-server.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-mock-server.js
new file mode 100644
index 000000000000..e2db13f5c1d0
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/start-mock-server.js
@@ -0,0 +1,57 @@
+const express = require('express');
+const path = require('path');
+const { killProcessOnPort, startProcess } = require('../utils/processes');
+const paths = require('../utils/paths');
+const { MOCK_SERVER_PATHS } = require('../constants/mockServerPaths');
+const { MOCK_SERVER_PROCESS_NAME } = require('../../constants');
+
+const router = express.Router();
+
+router.post('/start-mock-server', async (req, res) => {
+ const { responsesPath } = req.body;
+
+ if (!responsesPath) {
+ return res.status(400).json({
+ success: false,
+ message: 'responsesPath is required',
+ });
+ }
+
+ // Normalize the path for comparison just in case there are any issues with the path string
+ const normalizedPath = path.normalize(responsesPath).replace(/\\/g, '/');
+
+ const matchingPathIndex = MOCK_SERVER_PATHS.findIndex(
+ allowedPath =>
+ path.normalize(allowedPath).replace(/\\/g, '/') === normalizedPath,
+ );
+
+ if (matchingPathIndex === -1) {
+ return res.status(403).json({
+ success: false,
+ message: 'Invalid responses path',
+ allowedPaths: MOCK_SERVER_PATHS, // I might remove this in the future
+ });
+ }
+
+ // Use the validated path from our array
+ // this way we are not using user input to start the server
+ const validatedPath = path.join(
+ paths.root,
+ MOCK_SERVER_PATHS[matchingPathIndex],
+ );
+
+ await killProcessOnPort('3000');
+
+ const result = await startProcess(
+ MOCK_SERVER_PROCESS_NAME,
+ 'node',
+ [paths.mockApi, '--responses', validatedPath],
+ {
+ forceRestart: true,
+ },
+ );
+
+ return res.json(result);
+});
+
+module.exports = router;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/status.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/status.js
new file mode 100644
index 000000000000..a4f896d1df7a
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/status.js
@@ -0,0 +1,82 @@
+// server/routes/status.js
+const express = require('express');
+const { processes } = require('../utils/processes');
+const { getCachedManifests } = require('../utils/manifests');
+const logger = require('../utils/logger');
+const { FRONTEND_PROCESS_NAME } = require('../../constants');
+
+const router = express.Router();
+
+/**
+ * Extracts entry names from frontend process args
+ * @param {string[]} args - Process spawn arguments
+ * @returns {string[]} Array of entry names
+ */
+const getEntryNamesFromArgs = args => {
+ const entryArg = args.find(arg => arg.startsWith('entry='));
+ if (!entryArg) return [];
+
+ // Split on comma to handle multiple entries
+ return entryArg.replace('entry=', '').split(',');
+};
+
+/**
+ * Gets app information from manifests for given entry names
+ * @param {string[]} entryNames - Array of entry names to look up
+ * @returns {Object[]} Array of app information objects
+ */
+const getAppInfo = async entryNames => {
+ const apps = [];
+ const manifestFiles = getCachedManifests();
+
+ entryNames.forEach(entryName => {
+ const manifest = manifestFiles.find(m => m.entryName === entryName);
+ if (manifest) {
+ apps.push({
+ entryName,
+ rootUrl: manifest.rootUrl,
+ appName: manifest.appName || '',
+ });
+ }
+ });
+
+ return apps;
+};
+
+router.get('/status', async (req, res) => {
+ try {
+ // Get basic process status info
+ const processStatus = Object.keys(processes).reduce((acc, name) => {
+ acc[name] = {
+ pid: processes[name].pid,
+ killed: processes[name].killed,
+ exitCode: processes[name].exitCode,
+ signalCode: processes[name].signalCode,
+ args: processes[name].spawnargs,
+ };
+ return acc;
+ }, {});
+
+ // Get route information for frontend process if running
+ let apps = [];
+ if (processStatus[FRONTEND_PROCESS_NAME]) {
+ const entryNames = getEntryNamesFromArgs(
+ processStatus[FRONTEND_PROCESS_NAME].args,
+ );
+ apps = await getAppInfo(entryNames);
+ }
+
+ res.json({
+ processes: processStatus,
+ apps,
+ });
+ } catch (error) {
+ logger.error('Error getting status:', error);
+ res.status(500).json({
+ error: 'Failed to get server status',
+ message: error.message,
+ });
+ }
+});
+
+module.exports = router;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/stop-on-port.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/stop-on-port.js
new file mode 100644
index 000000000000..836dbe6a98ca
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/routes/stop-on-port.js
@@ -0,0 +1,45 @@
+const express = require('express');
+const { killProcessOnPort } = require('../utils/processes');
+const logger = require('../utils/logger');
+
+const router = express.Router();
+
+router.post('/stop', async (req, res) => {
+ const { port: portToStop } = req.body;
+
+ // Validate port is a number and within allowed range
+ // adds a bit of protection from invalid port numbers
+ const port = parseInt(portToStop, 10);
+ if (Number.isNaN(port) || port < 1024 || port > 65535) {
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid port number. Must be between 1024 and 65535',
+ });
+ }
+
+ // Only allow stopping known development ports
+ // if 1337 is in the list, we are stopping the whole server manager aka this server
+ const allowedPorts = [3000, 3001, 3002, 1337];
+ if (!allowedPorts.includes(port)) {
+ return res.status(403).json({
+ success: false,
+ message: 'Not allowed to stop processes on this port',
+ });
+ }
+
+ try {
+ await killProcessOnPort(port);
+ return res.json({
+ success: true,
+ message: `Process on port ${port} stopped`,
+ });
+ } catch (error) {
+ logger.error(`Error stopping process on port ${port}:`, error);
+ return res.status(500).json({
+ success: false,
+ message: `Error stopping process on port ${port}: ${error.message}`,
+ });
+ }
+});
+
+module.exports = router;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/cors.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/cors.js
new file mode 100644
index 000000000000..8eb7f45e6b30
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/cors.js
@@ -0,0 +1,42 @@
+/**
+ * _LOCAL USE ONLY_ - middleware to enable cross-origin requests (CORS).
+ * Sets necessary CORS headers and handles preflight requests.
+ *
+ * @middleware
+ * @param {import('express').Request} req - Express request object
+ * @param {import('express').Response} res - Express response object
+ * @param {import('express').NextFunction} next - Express next middleware function
+ * @returns {void|Response} Returns 200 for preflight requests, otherwise calls next()
+ *
+ * @example
+ * // Apply as middleware to Express app or router
+ * app.use(cors);
+ *
+ * @security
+ * - Only for use in local server
+ * - Allows all origins ('*')
+ * - Allows all common request methods for REST
+ * - Allows X-Requested-With and content-type headers
+ * - Enables credentials
+ */
+const cors = (req, res, next) => {
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader(
+ 'Access-Control-Allow-Methods',
+ 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
+ );
+ res.setHeader(
+ 'Access-Control-Allow-Headers',
+ 'X-Requested-With,content-type',
+ );
+ res.setHeader('Access-Control-Allow-Credentials', true);
+
+ // Handle preflight requests by immediately responding with 200
+ if (req.method === 'OPTIONS') {
+ return res.sendStatus(200);
+ }
+
+ return next();
+};
+
+module.exports = cors;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/logger.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/logger.js
new file mode 100644
index 000000000000..184256ed6a1e
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/logger.js
@@ -0,0 +1,73 @@
+/* eslint-disable no-console */
+const chalk = require('chalk');
+
+/**
+ * @typedef {(name: string, type: string, message: string|Buffer) => void} ProcessFn
+ * Process-specific logging function
+ */
+
+/**
+ * @typedef {(...args: any[]) => void} LoggerFn
+ * Generic logger function
+ */
+
+/**
+ * @typedef {Object} Logger
+ * @property {LoggerFn} info - log blue message with `[INFO]` prefix
+ * @property {LoggerFn} success - log green message with `[SUCCESS]` prefix
+ * @property {LoggerFn} warn - log yellow message with `[WARN]` prefix
+ * @property {LoggerFn} error - log red message with `[ERROR]` prefix
+ * @property {LoggerFn} debug - log gray message with `[DEBUG]` prefix
+ * @property {ProcessFn} process - log process-specific messages with name and type
+ */
+
+/**
+ * Logger for server-side logging with colored output.
+ * Only creates this logger when running in Node.js environment.
+ * Returns a no-op logger in non-Node environments to prevent accidental logging.
+ *
+ * @type {Logger}
+ *
+ * @example
+ * logger.info('Server started on port 3000');
+ * logger.success('Database connected successfully');
+ * logger.warn('Rate limit approaching');
+ * logger.error('Failed to connect to database');
+ * logger.debug(data);
+ *
+ * // Log process-specific messages, red for stderr type, blue for stdout or other types
+ * logger.process('Server', 'stdout', 'Server initialized');
+ * logger.process('Worker', 'stderr', 'Memory limit exceeded');
+ *
+ * // Handling Buffer messages
+ * const buf = Buffer.from('Process output');
+ * logger.process('Process', 'stdout', buf);
+ */
+const logger =
+ typeof process !== 'undefined' && process.versions?.node
+ ? {
+ info: (...args) => console.log(chalk.blue('[INFO]'), ...args),
+ success: (...args) => console.log(chalk.green('[SUCCESS]'), ...args),
+ warn: (...args) => console.log(chalk.yellow('[WARN]'), ...args),
+ error: (...args) => console.log(chalk.red('[ERROR]'), ...args),
+ debug: (...args) => console.log(chalk.gray('[DEBUG]'), ...args),
+ /**
+ * Log process-specific messages
+ * @type {ProcessFn}
+ */
+ process: (name, type, message) => {
+ const color = type === 'stderr' ? 'red' : 'blue';
+ const text = Buffer.isBuffer(message) ? message.toString() : message;
+ console.log(chalk[color](`[${name}] ${type}:`), text);
+ },
+ }
+ : {
+ info: () => {},
+ success: () => {},
+ warn: () => {},
+ error: () => {},
+ debug: () => {},
+ process: () => {},
+ };
+
+module.exports = logger;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/manifests.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/manifests.js
new file mode 100644
index 000000000000..1ecfbb7dce02
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/manifests.js
@@ -0,0 +1,105 @@
+const fs = require('fs').promises;
+const path = require('path');
+const logger = require('./logger');
+const paths = require('./paths');
+
+/**
+ * @typedef {Object} ManifestFile
+ * @property {string} path - file path to the manifest.json
+ * @property {string} entryName - name of the entry file
+ * @property {string} rootUrl - root url of the app
+ * @property {string} appName - name of the app
+ * @property {string} [productId] - product id of the app
+ */
+
+/** @type {ManifestFile[]} */
+let _cachedManifests = [];
+
+/**
+ * Searches a directory for manifest.json files
+ * @param {string} dir - Dir path to search
+ * @returns {Promise} Array of manifest objects
+ */
+async function findManifestFiles(dir) {
+ const manifests = [];
+
+ /**
+ * Recursively searches directories for manifest files
+ * @param {string} currentDir - Current directory being searched
+ * @returns {Promise}
+ */
+ async function searchDir(currentDir) {
+ try {
+ const files = await fs.readdir(currentDir);
+ const fileStats = await Promise.all(
+ files.map(file => {
+ const filePath = path.join(currentDir, file);
+ return fs.stat(filePath).then(stat => ({ file, filePath, stat }));
+ }),
+ );
+
+ const dirPromises = [];
+ const manifestPromises = [];
+
+ for (const { file, filePath, stat } of fileStats) {
+ // there were a few files called manifest.json in the node_modules folder
+ // so we need to filter them out
+ if (file !== 'node_modules') {
+ if (stat.isDirectory()) {
+ dirPromises.push(searchDir(filePath));
+ } else if (file === 'manifest.json') {
+ try {
+ manifestPromises.push(
+ fs.readFile(filePath, 'utf8').then(content => ({
+ path: filePath,
+ ...JSON.parse(content),
+ })),
+ );
+ } catch (err) {
+ logger.error(`Error reading manifest at ${filePath}:`, err);
+ }
+ }
+ }
+ }
+
+ // using Promise.all to run all promises in parallel and not wait for each one
+ await Promise.all(dirPromises);
+ manifests.push(...(await Promise.all(manifestPromises)));
+ } catch (err) {
+ logger.error(`Error reading directory ${currentDir}:`, err);
+ }
+ }
+
+ await searchDir(dir);
+ return manifests;
+}
+
+/**
+ * Returns the currently cached manifests.
+ * Used because just accessing the `_cachedManifests` variable directly
+ * will only return the initial value `[]`
+ * @returns {ManifestFile[]} Array of cached manifest objects
+ */
+const getCachedManifests = () => _cachedManifests;
+
+/**
+ * Creates the manifest cache.
+ * Used during vadx startup
+ * @returns {Promise}
+ * @throws {Error} If there is an error reading the manifests
+ */
+async function initializeManifests() {
+ try {
+ logger.debug('Scanning for manifests in:', paths.applications);
+ _cachedManifests = await findManifestFiles(paths.applications);
+ logger.info(`Loaded ${_cachedManifests.length} manifests at startup`);
+ } catch (error) {
+ logger.error('Error loading manifests at startup:', error);
+ _cachedManifests = [];
+ }
+}
+
+module.exports = {
+ initializeManifests,
+ getCachedManifests,
+};
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/parseArgs.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/parseArgs.js
new file mode 100644
index 000000000000..8a76055ca1b0
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/parseArgs.js
@@ -0,0 +1,20 @@
+const commandLineArgs = require('command-line-args');
+
+const optionDefinitions = [
+ { name: 'entry', type: String, defaultValue: 'mock-form-ae-design-patterns' },
+ { name: 'api', type: String, defaultValue: 'http://localhost:3000' },
+ { name: 'responses', type: String },
+];
+
+function parseArgs() {
+ const options = commandLineArgs(optionDefinitions);
+ return {
+ entry: options.entry,
+ api: options.api,
+ responses:
+ options.responses ||
+ `src/applications/_mock-form-ae-design-patterns/mocks/server.js`,
+ };
+}
+
+module.exports = parseArgs;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/paths.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/paths.js
new file mode 100644
index 000000000000..6a8ec44f452e
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/paths.js
@@ -0,0 +1,40 @@
+const path = require('path');
+const fs = require('fs');
+
+const findRoot = startDir => {
+ let currentDir = startDir;
+
+ // Walk up the directory tree until we find package.json or hit the root
+ while (currentDir !== path.parse(currentDir).root) {
+ const pkgPath = path.join(currentDir, 'package.json');
+
+ if (fs.existsSync(pkgPath)) {
+ try {
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
+ // Verify this is the right package.json
+ if (pkg.name === 'vets-website') {
+ return currentDir;
+ }
+ } catch (e) {
+ // Continue if package.json is invalid
+ }
+ }
+
+ currentDir = path.dirname(currentDir);
+ }
+
+ throw new Error('Could not find vets-website root directory');
+};
+
+// Get absolute paths that can be used anywhere
+const paths = {
+ root: findRoot(__dirname),
+ get applications() {
+ return path.join(this.root, 'src/applications');
+ },
+ get mockApi() {
+ return path.join(this.root, 'src/platform/testing/e2e/mockapi.js');
+ },
+};
+
+module.exports = paths;
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/processes.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/processes.js
new file mode 100644
index 000000000000..ee2c9750b6a0
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/processes.js
@@ -0,0 +1,201 @@
+const { spawn, exec } = require('child_process');
+const { stripAnsi } = require('./strings');
+const logger = require('./logger');
+const paths = require('./paths');
+const { FRONTEND_PROCESS_NAME } = require('../../constants');
+
+const processes = {};
+const outputCache = {};
+const MAX_CACHE_LINES = 100;
+const clients = new Map();
+
+function killProcessOnPort(portToKill) {
+ return new Promise((resolve, reject) => {
+ const isWin = process.platform === 'win32';
+
+ let command;
+ if (isWin) {
+ command = `FOR /F "tokens=5" %a in ('netstat -aon ^| find ":${portToKill}" ^| find "LISTENING"') do taskkill /F /PID %a`;
+ } else {
+ command = `lsof -ti :${portToKill} | xargs kill -9`;
+ }
+
+ exec(command, error => {
+ if (error) {
+ logger.error(`Error killing process on port ${portToKill}: ${error}`);
+ reject(error);
+ } else {
+ logger.debug(`Process on port ${portToKill} killed`);
+ resolve();
+ }
+ });
+ });
+}
+
+function sendSSE(res, data) {
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
+}
+
+function addToCache(name, type, data) {
+ if (!outputCache[name]) {
+ outputCache[name] = [];
+ }
+
+ const strippedData = stripAnsi(data.toString().trim());
+
+ // Send SSE to all connected clients for this process
+ const clientsForProcess = clients.get(name) || [];
+ clientsForProcess.forEach(client => {
+ sendSSE(client, { type, data: strippedData });
+ });
+
+ if (type === 'status') {
+ return;
+ }
+
+ outputCache[name].unshift(strippedData);
+ if (outputCache[name].length > MAX_CACHE_LINES) {
+ outputCache[name].pop();
+ }
+}
+
+// Modify setupProcessHandlers to include status events
+function setupProcessHandlers(childProcess, procName, metadata) {
+ addToCache(procName, 'status', {
+ status: 'started',
+ timestamp: Date.now(),
+ metadata,
+ });
+
+ const statusInterval = setInterval(() => {
+ if (processes[procName]) {
+ const clientsForProcess = clients.get(procName) || [];
+ clientsForProcess.forEach(client => {
+ sendSSE(client, {
+ type: 'status',
+ data: { status: 'running', metadata },
+ });
+ });
+ } else {
+ clearInterval(statusInterval);
+ }
+ }, 5000);
+
+ childProcess.stdout.on('data', data => {
+ logger.process(procName, 'stdout', data);
+ addToCache(procName, 'stdout', data);
+ });
+
+ childProcess.stderr.on('data', data => {
+ logger.process(procName, 'stderr', data);
+ addToCache(procName, 'stderr', data);
+ });
+
+ childProcess.on('close', code => {
+ logger.process(procName, 'close', code);
+ addToCache(procName, 'status', 'stopped');
+ clearInterval(statusInterval);
+ delete processes[procName];
+ });
+}
+
+function startProcess(procName, command, args, options = {}) {
+ const { forceRestart = false } = options;
+
+ const { metadata } = options;
+
+ logger.debug(`Starting process: ${procName}`);
+ logger.debug(`Command: ${command}`);
+ logger.debug(`Args: ${args}`);
+ logger.debug({ metadata });
+ return new Promise(resolve => {
+ if (processes[procName]) {
+ if (!forceRestart) {
+ return resolve({
+ success: false,
+ message: `Process ${procName} is already running`,
+ });
+ }
+
+ logger.debug(`Force stopping existing process: ${procName}`);
+ const oldProcess = processes[procName];
+
+ // Clean up the old process
+ oldProcess.on('close', () => {
+ delete processes[procName];
+ if (outputCache[procName]) {
+ outputCache[procName] = [];
+ }
+
+ // Start new process after old one is fully cleaned up
+ const childProcess = spawn(command, args, {
+ env: process.env,
+ });
+ processes[procName] = childProcess;
+
+ setupProcessHandlers(childProcess, procName, metadata);
+
+ resolve({
+ success: true,
+ message: `Process ${procName} restarted`,
+ });
+ });
+
+ return oldProcess.kill();
+ }
+ // No existing process, just start a new one
+ const childProcess = spawn(command, args, {
+ env: process.env,
+ });
+ processes[procName] = childProcess;
+
+ setupProcessHandlers(childProcess, procName, metadata);
+
+ return resolve({
+ success: true,
+ message: `Process ${procName} started`,
+ });
+ });
+}
+
+async function autoStartServers(options = {}) {
+ const { entry, api, responses } = options;
+
+ await killProcessOnPort('3000');
+ await killProcessOnPort('3001');
+
+ await startProcess(
+ FRONTEND_PROCESS_NAME,
+ 'yarn',
+ ['--cwd', paths.root, 'watch', '--env', `entry=${entry}`, `api=${api}`],
+ {
+ forceRestart: true,
+ metadata: {
+ entries: [entry],
+ },
+ },
+ );
+
+ await startProcess(
+ 'mock-server',
+ 'yarn',
+ ['--cwd', paths.root, 'mock-api', '--responses', responses],
+ {
+ forceRestart: true,
+ metadata: {
+ responses,
+ },
+ },
+ );
+}
+
+module.exports = {
+ processes,
+ outputCache,
+ clients,
+ sendSSE,
+ addToCache,
+ startProcess,
+ autoStartServers,
+ killProcessOnPort,
+};
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/strings.js b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/strings.js
new file mode 100644
index 000000000000..74634c574925
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/server/utils/strings.js
@@ -0,0 +1,19 @@
+/**
+ * Strip ANSI escape codes from a string
+ * this makes the output more readable in the UI for terminal output
+ * @param {string} str - The string to strip ANSI escape codes from
+ * @returns {string} - The string with ANSI escape codes removed
+ */
+function stripAnsi(str) {
+ // couldn't figure out a good way to strip ansi codes
+ // from the output without using a regex that contained control characters
+ // this eslint rule also is created to avoid mistakes as these control characters are 'rarely used'
+ // but in our case we need to strip them
+ return str.replace(
+ // eslint-disable-next-line no-control-regex
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
+ '',
+ );
+}
+
+module.exports = { stripAnsi };
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/utils/HeadingHierarchyAnalyzer.js b/src/applications/_mock-form-ae-design-patterns/vadx/utils/HeadingHierarchyAnalyzer.js
index d403e248f375..da5381aae8a2 100644
--- a/src/applications/_mock-form-ae-design-patterns/vadx/utils/HeadingHierarchyAnalyzer.js
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/utils/HeadingHierarchyAnalyzer.js
@@ -123,7 +123,7 @@ class HeadingHierarchyAnalyzer {
if (h1Count > 1) {
issues.push({
type: 'multiple-h1',
- message: `Page has ${h1Count} h1 headings (should have exactly 1)`,
+ message: `Page has multiple (${h1Count}) top level H1 headings and should have exactly 1`,
});
}
diff --git a/src/applications/_mock-form-ae-design-patterns/vadx/utils/dates.js b/src/applications/_mock-form-ae-design-patterns/vadx/utils/dates.js
new file mode 100644
index 000000000000..15787e517808
--- /dev/null
+++ b/src/applications/_mock-form-ae-design-patterns/vadx/utils/dates.js
@@ -0,0 +1,10 @@
+import { format, parseISO } from 'date-fns';
+
+export const formatDate = dateString => {
+ try {
+ const date = parseISO(dateString);
+ return format(date, 'HH:mm:ss:aaaaa');
+ } catch (error) {
+ return dateString; // Return original string if parsing fails
+ }
+};
From 6d0cabeab456d84c7fa7d581331360110d50b870 Mon Sep 17 00:00:00 2001
From: Nick Sayre
Date: Tue, 14 Jan 2025 16:17:38 -0600
Subject: [PATCH 02/39] 1324 - remove save-in-progress from the My VA review
contact information form (#34084)
---
.../personalization/review-information/config/form.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/applications/personalization/review-information/config/form.js b/src/applications/personalization/review-information/config/form.js
index d39f2fae43df..1826351acd95 100644
--- a/src/applications/personalization/review-information/config/form.js
+++ b/src/applications/personalization/review-information/config/form.js
@@ -70,6 +70,7 @@ const formConfig = {
collapsibleNavLinks: true,
},
formId: VA_FORM_IDS.FORM_WELCOME_VA_SETUP_REVIEW_INFORMATION,
+ disableSave: true,
saveInProgress: {
// messages: {
// inProgress: 'Your welcome va setup review information form application (00-0000) is in progress.',
From 3154baff7c10866731de7611c86b53490172e2ec Mon Sep 17 00:00:00 2001
From: Afia Caruso <108290062+acaruso-oddball@users.noreply.github.com>
Date: Tue, 14 Jan 2025 17:24:39 -0500
Subject: [PATCH 03/39] Verify update 916 (#33982)
* - Redirected unauthenticated users to . Enforced and added IAM query parameters. Simplified rendering and button logic.
* first pass, refactor in process
* made updates to simplify workflow, created unified verification page, streamlined verify handler and reverted changes
* set default oauth param to false
---
.../verify/components/AuthenticatedVerify.jsx | 90 -------
.../components/UnauthenticatedVerify.jsx | 42 ----
.../verify/components/UnifiedVerify.jsx | 110 +++++++++
.../verify/components/verifyButton.jsx | 46 ----
.../verify/containers/VerifyApp.jsx | 28 +--
.../AuthenticatedVerify.unit.spec.js | 94 --------
.../components/Unauthenticated.unit.spec.js | 42 ----
.../components/UnifiedVerify.unit.spec.js | 74 ++++++
.../tests/containers/VerifyApp.unit.spec.js | 46 +---
.../components/VerifyButton.jsx | 42 +++-
.../components/VerifyButton.unit.spec.js | 221 +++++++++++-------
11 files changed, 363 insertions(+), 472 deletions(-)
delete mode 100644 src/applications/verify/components/AuthenticatedVerify.jsx
delete mode 100644 src/applications/verify/components/UnauthenticatedVerify.jsx
create mode 100644 src/applications/verify/components/UnifiedVerify.jsx
delete mode 100644 src/applications/verify/components/verifyButton.jsx
delete mode 100644 src/applications/verify/tests/components/AuthenticatedVerify.unit.spec.js
delete mode 100644 src/applications/verify/tests/components/Unauthenticated.unit.spec.js
create mode 100644 src/applications/verify/tests/components/UnifiedVerify.unit.spec.js
diff --git a/src/applications/verify/components/AuthenticatedVerify.jsx b/src/applications/verify/components/AuthenticatedVerify.jsx
deleted file mode 100644
index 9d4246aff556..000000000000
--- a/src/applications/verify/components/AuthenticatedVerify.jsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import React, { useEffect } from 'react';
-import { SERVICE_PROVIDERS } from 'platform/user/authentication/constants';
-import { focusElement } from '~/platform/utilities/ui';
-import { useSelector } from 'react-redux';
-import { selectProfile } from 'platform/user/selectors';
-import {
- VerifyIdmeButton,
- VerifyLogingovButton,
-} from 'platform/user/authentication/components/VerifyButton';
-
-export default function Authentication() {
- const profile = useSelector(selectProfile);
- useEffect(
- () => {
- if (!profile?.loading) {
- focusElement('h1');
- }
- },
- [profile.loading, profile.verified],
- );
-
- if (profile?.loading) {
- return (
-
- );
- }
- const { idme, logingov } = SERVICE_PROVIDERS;
- const signInMethod = profile?.signIn?.serviceName;
- const singleVerifyButton =
- signInMethod === 'logingov' ? (
-
- ) : (
-
- );
-
- const deprecationDates = `${
- signInMethod === 'mhv' ? `January 31,` : `September 30,`
- } 2025.`;
- const { label } = SERVICE_PROVIDERS[signInMethod];
- const deprecationDatesContent = (
-
- You’ll need to sign in with a different account after{' '}
- {deprecationDates}. After this date, we’ll remove the{' '}
- {label} sign-in option. You’ll need to sign in using a{' '}
- Login.gov or ID.me account.
-
- We need you to verify your identity for your{' '}
- {label} account. This step helps us protect
- all Veterans’ information and prevent scammers from stealing
- your benefits.
-
-
- This one-time process often takes about 10 minutes. You’ll
- need to provide certain personal information and
- identification.
-
- We need you to verify your identity for your{' '}
- Login.gov or ID.me account.
- This step helps us protect all Veterans’ information and prevent
- scammers from stealing your benefits.
-
-
- This one-time process often takes about 10 minutes. You’ll need
- to provide certain personal information and identification.
-
-
- );
-}
diff --git a/src/applications/verify/components/UnifiedVerify.jsx b/src/applications/verify/components/UnifiedVerify.jsx
new file mode 100644
index 000000000000..bddf1505425b
--- /dev/null
+++ b/src/applications/verify/components/UnifiedVerify.jsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { useSelector } from 'react-redux';
+import {
+ isAuthenticatedWithOAuth,
+ signInServiceName,
+} from 'platform/user/authentication/selectors';
+import {
+ VerifyIdmeButton,
+ VerifyLogingovButton,
+} from 'platform/user/authentication/components/VerifyButton';
+import { hasSession } from 'platform/user/profile/utilities';
+
+const Verify = () => {
+ const isAuthenticated = hasSession();
+ const isAuthenticatedOAuth = useSelector(isAuthenticatedWithOAuth);
+ const loginServiceName = useSelector(signInServiceName); // Get the current SIS (e.g., idme or logingov)
+
+ let buttonContent;
+
+ if (isAuthenticated) {
+ <>
+
+
+ >;
+ } else if (isAuthenticatedOAuth) {
+ // Use the loginServiceName to determine which button to show
+ if (loginServiceName === 'idme') {
+ buttonContent = (
+
+ );
+ } else if (loginServiceName === 'logingov') {
+ buttonContent = (
+
+ );
+ }
+ } else {
+ buttonContent = (
+ <>
+
+
+ >
+ );
+ }
+
+ const renderServiceNames = () => {
+ if (isAuthenticated) {
+ return (
+ {loginServiceName === 'idme' ? 'ID.me' : 'Login.gov'}
+ );
+ }
+ return (
+ <>
+ Login.gov or ID.me
+ >
+ );
+ };
+
+ return (
+
+
+
+
+
Verify your identity
+
+ We need you to verify your identity for your{' '}
+ {renderServiceNames()} account. This step helps us protect all
+ Veterans’ information and prevent scammers from stealing your
+ benefits.
+
+
+ This one-time process often takes about 10 minutes. You’ll need to
+ provide certain personal information and identification.
+