diff --git a/.gitignore b/.gitignore
index 81b02a92e..e437ac444 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ __pycache__/
*.py[cod]
*$py.class
docs/
+.DS_Store
# C extensions
*.so
diff --git a/frontend/public/config/config.js b/frontend/public/config/config.js
index 01cbed624..a53256ce3 100644
--- a/frontend/public/config/config.js
+++ b/frontend/public/config/config.js
@@ -7,6 +7,10 @@ export const config = {
POST_LOGOUT_URL: 'http://localhost:3000/',
SM_LOGOUT_URL:
'https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl='
+ },
+ feature_flags: {
+ supplementalReporting: true,
+ notifications: false
}
}
diff --git a/frontend/src/constants/config.js b/frontend/src/constants/config.js
index 2763bbcf2..8dfec5e6c 100644
--- a/frontend/src/constants/config.js
+++ b/frontend/src/constants/config.js
@@ -28,6 +28,15 @@ export function getApiBaseUrl() {
return window.lcfs_config.api_base ?? baseUrl
}
+export const isFeatureEnabled = (featureFlag) => {
+ return CONFIG.feature_flags[featureFlag]
+}
+
+export const FEATURE_FLAGS = {
+ SUPPLEMENTAL_REPORTING: 'supplementalReporting',
+ NOTIFICATIONS: 'notifications'
+}
+
export const CONFIG = {
API_BASE: getApiBaseUrl(),
KEYCLOAK: {
@@ -42,5 +51,10 @@ export const CONFIG = {
SM_LOGOUT_URL:
window.lcfs_config.keycloak.SM_LOGOUT_URL ??
'https://logontest7.gov.bc.ca/clp-cgi/logoff.cgi?retnow=1&returl='
+ },
+ feature_flags: {
+ supplementalReporting:
+ window.lcfs_config.feature_flags.supplementalReporting ?? true,
+ notifications: window.lcfs_config.feature_flags.notifications ?? false
}
}
diff --git a/frontend/src/utils/__tests__/withFeatureFlag.test.jsx b/frontend/src/utils/__tests__/withFeatureFlag.test.jsx
new file mode 100644
index 000000000..b619a1f25
--- /dev/null
+++ b/frontend/src/utils/__tests__/withFeatureFlag.test.jsx
@@ -0,0 +1,109 @@
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import withFeatureFlag from '../withFeatureFlag.jsx' // Adjust the import path as necessary
+import { isFeatureEnabled } from '@/constants/config.js'
+
+// Mock the isFeatureEnabled function
+vi.mock('@/constants/config.js', () => ({
+ isFeatureEnabled: vi.fn()
+}))
+
+// Mock Navigate component
+vi.mock('react-router-dom', () => ({
+ ...vi.importActual('react-router-dom'),
+ Navigate: ({ to }) =>
Navigate to {to}
+}))
+
+// Define a mock component to be wrapped
+const MockComponent = () => Feature Enabled Content
+
+describe('withFeatureFlag HOC', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('renders the wrapped component when the feature flag is enabled', () => {
+ isFeatureEnabled.mockReturnValue(true)
+
+ const WrappedComponent = withFeatureFlag(
+ MockComponent,
+ 'new-feature',
+ '/fallback'
+ )
+
+ render()
+
+ expect(screen.getByText('Feature Enabled Content')).toBeInTheDocument()
+ })
+
+ it('redirects to the specified path when the feature flag is disabled and redirect is provided', () => {
+ isFeatureEnabled.mockReturnValue(false)
+
+ const WrappedComponent = withFeatureFlag(
+ MockComponent,
+ 'new-feature',
+ '/fallback'
+ )
+
+ render()
+
+ const navigateElement = screen.getByTestId('navigate')
+ expect(navigateElement).toBeInTheDocument()
+ expect(navigateElement).toHaveTextContent('Navigate to /fallback')
+ })
+
+ it('renders null when the feature flag is disabled and no redirect is provided', () => {
+ isFeatureEnabled.mockReturnValue(false)
+
+ const WrappedComponent = withFeatureFlag(MockComponent, 'new-feature')
+
+ const { container } = render()
+
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('sets the correct display name for the wrapped component', () => {
+ isFeatureEnabled.mockReturnValue(true)
+
+ const WrappedComponent = withFeatureFlag(
+ MockComponent,
+ 'new-feature',
+ '/fallback'
+ )
+
+ render()
+
+ expect(WrappedComponent.displayName).toBe('WithFeatureFlag(MockComponent)')
+ })
+
+ it('handles undefined featureFlag gracefully by rendering the wrapped component', () => {
+ isFeatureEnabled.mockReturnValue(false)
+
+ const WrappedComponent = withFeatureFlag(
+ MockComponent,
+ undefined,
+ '/fallback'
+ )
+
+ render()
+
+ const navigateElement = screen.getByTestId('navigate')
+ expect(navigateElement).toBeInTheDocument()
+ expect(navigateElement).toHaveTextContent('Navigate to /fallback')
+ })
+
+ it('handles null props correctly by passing them to the wrapped component', () => {
+ isFeatureEnabled.mockReturnValue(true)
+
+ const WrappedComponent = withFeatureFlag(
+ MockComponent,
+ 'new-feature',
+ '/fallback'
+ )
+
+ render()
+
+ expect(screen.getByText('Feature Enabled Content')).toBeInTheDocument()
+ })
+})
diff --git a/frontend/src/utils/__tests__/withRole.test.jsx b/frontend/src/utils/__tests__/withRole.test.jsx
index 1d74ce903..350efd3ca 100644
--- a/frontend/src/utils/__tests__/withRole.test.jsx
+++ b/frontend/src/utils/__tests__/withRole.test.jsx
@@ -1 +1,137 @@
-describe.todo()
+import React from 'react'
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import withRole from '../withRole.jsx'
+import { useCurrentUser } from '@/hooks/useCurrentUser'
+
+// Mock the useCurrentUser hook
+vi.mock('@/hooks/useCurrentUser')
+
+// Mock Navigate component
+vi.mock('react-router-dom', () => ({
+ ...vi.importActual('react-router-dom'),
+ Navigate: ({ to }) => Navigate to {to}
+}))
+
+// Define a mock component to be wrapped
+const MockComponent = () => Protected Content
+
+describe('withRole HOC', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('renders Loading... when currentUser is undefined', () => {
+ useCurrentUser.mockReturnValue({
+ data: undefined
+ })
+
+ const WrappedComponent = withRole(
+ MockComponent,
+ ['admin', 'user'],
+ '/login'
+ )
+
+ render()
+
+ expect(screen.getByText('Loading...')).toBeInTheDocument()
+ })
+
+ it('renders the wrapped component when user has an allowed role', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ roles: [{ name: 'user' }, { name: 'editor' }]
+ }
+ })
+
+ const WrappedComponent = withRole(
+ MockComponent,
+ ['admin', 'user'],
+ '/login'
+ )
+
+ render()
+
+ expect(screen.getByText('Protected Content')).toBeInTheDocument()
+ })
+
+ it('redirects to the specified path when user does not have an allowed role and redirect is provided', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ roles: [{ name: 'guest' }]
+ }
+ })
+
+ const WrappedComponent = withRole(
+ MockComponent,
+ ['admin', 'user'],
+ '/login'
+ )
+
+ render()
+
+ const navigateElement = screen.getByTestId('navigate')
+ expect(navigateElement).toBeInTheDocument()
+ expect(navigateElement).toHaveTextContent('Navigate to /login')
+ })
+
+ it('renders null when user does not have an allowed role and no redirect is provided', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ roles: [{ name: 'guest' }]
+ }
+ })
+
+ const WrappedComponent = withRole(MockComponent, ['admin', 'user'])
+
+ const { container } = render()
+
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('sets the correct display name for the wrapped component', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ roles: [{ name: 'admin' }]
+ }
+ })
+
+ const WrappedComponent = withRole(MockComponent, ['admin'], '/login')
+
+ render()
+
+ expect(WrappedComponent.displayName).toBe('WithRole(MockComponent)')
+ })
+
+ it('handles currentUser with no roles gracefully', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ roles: []
+ }
+ })
+
+ const WrappedComponent = withRole(MockComponent, ['admin'], '/login')
+
+ render()
+
+ const navigateElement = screen.getByTestId('navigate')
+ expect(navigateElement).toBeInTheDocument()
+ expect(navigateElement).toHaveTextContent('Navigate to /login')
+ })
+
+ it('handles currentUser.roles being undefined gracefully', () => {
+ useCurrentUser.mockReturnValue({
+ data: {
+ // roles is undefined
+ }
+ })
+
+ const WrappedComponent = withRole(MockComponent, ['admin'], '/login')
+
+ render()
+
+ const navigateElement = screen.getByTestId('navigate')
+ expect(navigateElement).toBeInTheDocument()
+ expect(navigateElement).toHaveTextContent('Navigate to /login')
+ })
+})
diff --git a/frontend/src/utils/withFeatureFlag.jsx b/frontend/src/utils/withFeatureFlag.jsx
new file mode 100644
index 000000000..55b536f17
--- /dev/null
+++ b/frontend/src/utils/withFeatureFlag.jsx
@@ -0,0 +1,26 @@
+import { Navigate } from 'react-router-dom'
+import { isFeatureEnabled } from '@/constants/config.js'
+
+export const withFeatureFlag = (WrappedComponent, featureFlag, redirect) => {
+ const WithFeatureFlag = (props) => {
+ const isEnabled = isFeatureEnabled(featureFlag)
+
+ if (!isEnabled && redirect) {
+ return
+ }
+ if (!isEnabled && !redirect) {
+ return null
+ }
+
+ return
+ }
+
+ // Display name for the wrapped component
+ WithFeatureFlag.displayName = `WithFeatureFlag(${
+ WrappedComponent.displayName || WrappedComponent.name || 'Component'
+ })`
+
+ return WithFeatureFlag
+}
+
+export default withFeatureFlag
diff --git a/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx b/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx
index 65be99cc1..339b22133 100644
--- a/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx
+++ b/frontend/src/views/ComplianceReports/components/AssessmentCard.jsx
@@ -13,6 +13,7 @@ import { useNavigate } from 'react-router-dom'
import { StyledListItem } from '@/components/StyledListItem'
import { roles } from '@/constants/roles'
import { Role } from '@/components/Role'
+import { FEATURE_FLAGS, isFeatureEnabled } from '@/constants/config.js'
export const AssessmentCard = ({
orgData,
@@ -173,7 +174,7 @@ export const AssessmentCard = ({
variant="h6"
color="primary"
>
- {t(`report:reportHistory`)}
+ {t('report:reportHistory')}
{filteredHistory.map((item, index) => (
@@ -202,33 +203,34 @@ export const AssessmentCard = ({
>
)}
- {currentStatus === COMPLIANCE_REPORT_STATUSES.ASSESSED && (
- <>
-
- {t('report:supplementalWarning')}
-
-
- {
- createSupplementalReport()
- }}
- startIcon={}
- sx={{ mt: 2 }}
- disabled={isLoading}
+ {isFeatureEnabled(FEATURE_FLAGS.SUPPLEMENTAL_REPORTING) &&
+ currentStatus === COMPLIANCE_REPORT_STATUSES.ASSESSED && (
+ <>
+
- {t('report:createSupplementalRptBtn')}
-
-
- >
- )}
+ {t('report:supplementalWarning')}
+
+
+ {
+ createSupplementalReport()
+ }}
+ startIcon={}
+ sx={{ mt: 2 }}
+ disabled={isLoading}
+ >
+ {t('report:createSupplementalRptBtn')}
+
+
+ >
+ )}
diff --git a/frontend/src/views/Notifications/Notifications.jsx b/frontend/src/views/Notifications/Notifications.jsx
index 5841ee3c7..da555141f 100644
--- a/frontend/src/views/Notifications/Notifications.jsx
+++ b/frontend/src/views/Notifications/Notifications.jsx
@@ -1,3 +1,14 @@
-export const Notifications = () => {
+import * as ROUTES from '@/constants/routes/routes.js'
+import withFeatureFlag from '@/utils/withFeatureFlag.jsx'
+import { FEATURE_FLAGS } from '@/constants/config.js'
+
+export const NotificationsBase = () => {
return Notifications
}
+
+export const Notifications = withFeatureFlag(
+ NotificationsBase,
+ FEATURE_FLAGS.NOTIFICATIONS,
+ ROUTES.DASHBOARD
+)
+Notifications.displayName = 'Notifications'