From a9d00a9e81f3276c82f5e98c26e0b5648a4ad0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20De=20Freitas?= <6485562+adefreitas@users.noreply.github.com> Date: Thu, 7 Aug 2025 17:50:52 +0100 Subject: [PATCH 1/2] feat: feature flag context and hook --- src/Hub.tsx | 50 +++++++++++++++++++ src/StackOneHub.tsx | 24 ++++----- .../integration-picker/IntegrationPicker.tsx | 35 +++++++------ src/shared/contexts/featureFlagContext.tsx | 26 ++++++++++ src/shared/hooks/useFeatureFlags.ts | 24 +++++++++ src/shared/queries.ts | 12 +++++ src/shared/types/featureFlags.ts | 1 + 7 files changed, 146 insertions(+), 26 deletions(-) create mode 100644 src/Hub.tsx create mode 100644 src/shared/contexts/featureFlagContext.tsx create mode 100644 src/shared/hooks/useFeatureFlags.ts create mode 100644 src/shared/queries.ts create mode 100644 src/shared/types/featureFlags.ts diff --git a/src/Hub.tsx b/src/Hub.tsx new file mode 100644 index 0000000..fb56b0e --- /dev/null +++ b/src/Hub.tsx @@ -0,0 +1,50 @@ +import { useQuery } from '@tanstack/react-query'; +import { IntegrationPicker } from './modules/integration-picker/IntegrationPicker'; +import { FeatureFlagContext, FeatureFlagProvider } from './shared/contexts/featureFlagContext'; +import { getSettings } from './shared/queries'; +import { HubModes } from './types/types'; + +interface HubProps { + mode: HubModes; + token: string; + apiUrl: string; + dashboardUrl: string; + height: string; + onSuccess?: () => void; + onClose?: () => void; + onCancel?: () => void; + accountId?: string; +} +export const Hub = ({ + mode, + token, + apiUrl, + dashboardUrl, + height, + onSuccess, + onClose, + onCancel, + accountId, +}: HubProps) => { + const { data: settings } = useQuery({ + queryKey: ['settings'], + queryFn: () => getSettings(apiUrl, token), + }); + + return ( + + {mode === 'integration-picker' && ( + + )} + + ); +}; diff --git a/src/StackOneHub.tsx b/src/StackOneHub.tsx index 054e770..0dc6e2d 100644 --- a/src/StackOneHub.tsx +++ b/src/StackOneHub.tsx @@ -13,6 +13,7 @@ import { } from '@stackone/malachite'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useEffect } from 'react'; +import { Hub } from './Hub'; import { CsvImporter } from './modules/csv-importer.tsx/CsvImporter'; import { IntegrationPicker } from './modules/integration-picker/IntegrationPicker'; import ErrorContainer from './shared/components/error'; @@ -119,18 +120,17 @@ export const StackOneHub: React.FC = ({ } > - {mode === 'integration-picker' && ( - - )} + diff --git a/src/modules/integration-picker/IntegrationPicker.tsx b/src/modules/integration-picker/IntegrationPicker.tsx index f1fbf12..0a2e3a5 100644 --- a/src/modules/integration-picker/IntegrationPicker.tsx +++ b/src/modules/integration-picker/IntegrationPicker.tsx @@ -1,4 +1,7 @@ import { Card } from '@stackone/malachite'; +import { useContext } from 'react'; +import { FeatureFlagContext, FeatureFlagProvider } from '../../shared/contexts/featureFlagContext'; +import useFeatureFlags from '../../shared/hooks/useFeatureFlags'; import { IntegrationPickerContent } from './components/IntegrationPickerContent'; import CardFooter from './components/cardFooter'; import CardTitle from './components/cardTitle'; @@ -23,6 +26,8 @@ export const IntegrationPicker: React.FC = ({ onSuccess, dashboardUrl, }) => { + const isHubLinkAccountReleaseEnabled = useFeatureFlags('hub_link_account_release'); + const { // Data hubData, @@ -73,20 +78,22 @@ export const IntegrationPicker: React.FC = ({ } height={height} > - + {isHubLinkAccountReleaseEnabled && ( + + )} ); }; diff --git a/src/shared/contexts/featureFlagContext.tsx b/src/shared/contexts/featureFlagContext.tsx new file mode 100644 index 0000000..95fb896 --- /dev/null +++ b/src/shared/contexts/featureFlagContext.tsx @@ -0,0 +1,26 @@ +import { createContext, useMemo } from 'react'; +import { FeatureFlag } from '../types/featureFlags'; + +export const FeatureFlagContext = createContext<{ featureFlags: FeatureFlag[] }>({ + featureFlags: [], +}); + +export const FeatureFlagProvider = ({ + featureFlags, + children, +}: { + featureFlags: FeatureFlag[]; + children: React.ReactNode; +}) => { + const memoizedContextValue = useMemo( + () => ({ + featureFlags, + }), + [featureFlags], + ); + return ( + + {children} + + ); +}; diff --git a/src/shared/hooks/useFeatureFlags.ts b/src/shared/hooks/useFeatureFlags.ts new file mode 100644 index 0000000..8c49fbd --- /dev/null +++ b/src/shared/hooks/useFeatureFlags.ts @@ -0,0 +1,24 @@ +import { useContext } from 'react'; +import { FeatureFlagContext } from '../contexts/featureFlagContext'; +import { FeatureFlag } from '../types/featureFlags'; + +const isFeatureEnabled = ({ + featureFlags, + featureFlag, +}: { featureFlags: FeatureFlag[]; featureFlag: FeatureFlag }): boolean => { + return featureFlags.includes(featureFlag); +}; + +export const useFeatureFlags = (featureFlag: FeatureFlag): boolean => { + const { featureFlags } = useContext(FeatureFlagContext); + if (!featureFlags) { + throw new Error('useFeatureFlags must be used within a FeatureFlagProvider'); + } + + return isFeatureEnabled({ + featureFlags, + featureFlag, + }); +}; + +export default useFeatureFlags; diff --git a/src/shared/queries.ts b/src/shared/queries.ts new file mode 100644 index 0000000..ac13cc1 --- /dev/null +++ b/src/shared/queries.ts @@ -0,0 +1,12 @@ +import { getRequest } from './httpClient'; +import { FeatureFlag } from './types/featureFlags'; + +export const getSettings = async (baseUrl: string, token: string) => { + return await getRequest<{ enabled_features: FeatureFlag[] }>({ + url: `${baseUrl}/hub/settings`, + headers: { + 'Content-Type': 'application/json', + 'x-hub-session-token': token, + }, + }); +}; diff --git a/src/shared/types/featureFlags.ts b/src/shared/types/featureFlags.ts new file mode 100644 index 0000000..2b1df98 --- /dev/null +++ b/src/shared/types/featureFlags.ts @@ -0,0 +1 @@ +export type FeatureFlag = 'hub_link_account_release'; From 5cbe84877bd63a3c0d7ea525401cd97aa45bd158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20De=20Freitas?= <6485562+adefreitas@users.noreply.github.com> Date: Fri, 8 Aug 2025 10:09:56 +0100 Subject: [PATCH 2/2] chore: copilot --- src/Hub.tsx | 2 +- src/modules/integration-picker/IntegrationPicker.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Hub.tsx b/src/Hub.tsx index fb56b0e..17d12ac 100644 --- a/src/Hub.tsx +++ b/src/Hub.tsx @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { IntegrationPicker } from './modules/integration-picker/IntegrationPicker'; -import { FeatureFlagContext, FeatureFlagProvider } from './shared/contexts/featureFlagContext'; +import { FeatureFlagProvider } from './shared/contexts/featureFlagContext'; import { getSettings } from './shared/queries'; import { HubModes } from './types/types'; diff --git a/src/modules/integration-picker/IntegrationPicker.tsx b/src/modules/integration-picker/IntegrationPicker.tsx index 0a2e3a5..7ccc590 100644 --- a/src/modules/integration-picker/IntegrationPicker.tsx +++ b/src/modules/integration-picker/IntegrationPicker.tsx @@ -1,6 +1,4 @@ import { Card } from '@stackone/malachite'; -import { useContext } from 'react'; -import { FeatureFlagContext, FeatureFlagProvider } from '../../shared/contexts/featureFlagContext'; import useFeatureFlags from '../../shared/hooks/useFeatureFlags'; import { IntegrationPickerContent } from './components/IntegrationPickerContent'; import CardFooter from './components/cardFooter';