diff --git a/.changeset/tricky-toys-destroy.md b/.changeset/tricky-toys-destroy.md new file mode 100644 index 0000000000..fd99e8b90e --- /dev/null +++ b/.changeset/tricky-toys-destroy.md @@ -0,0 +1,5 @@ +--- +"@tokens-studio/figma-plugin": patch +--- + +Add launch darkly feature flag to control bypassing of license checks diff --git a/packages/tokens-studio-for-figma/cypress/e2e/branches.cy.js b/packages/tokens-studio-for-figma/cypress/e2e/branches.cy.js index 09fc6e1d7c..5d0e75b4d7 100644 --- a/packages/tokens-studio-for-figma/cypress/e2e/branches.cy.js +++ b/packages/tokens-studio-for-figma/cypress/e2e/branches.cy.js @@ -172,8 +172,10 @@ describe('Branch switcher', () => { cy.get('[data-testid=push-dialog-success-heading]').should('have.length', 1); }); - // TEMPORARY: Skipped while license check is bypassed - it.skip('shows pro upgrade modal for non-pro users', () => { + it('shows pro upgrade modal for non-pro users', () => { + // Set feature flags to disable license bypass + cy.setFeatureFlags({ bypassLicenseCheck: false }); + // Create a non-pro user setup const nonProUserParams = { ...mockStartupParams, diff --git a/packages/tokens-studio-for-figma/cypress/support/commands.js b/packages/tokens-studio-for-figma/cypress/support/commands.js index 307dc1fa71..360ed9d966 100644 --- a/packages/tokens-studio-for-figma/cypress/support/commands.js +++ b/packages/tokens-studio-for-figma/cypress/support/commands.js @@ -78,4 +78,11 @@ Cypress.Commands.add('receiveSelectionValues', (values) => { }; $window.postMessage(message, '*'); }); +}) + +Cypress.Commands.add('setFeatureFlags', (flags) => { + cy.window().then(($window) => { + // Store flags in window object so they can be accessed by the app + $window.__CYPRESS_FEATURE_FLAGS__ = flags; + }); }) \ No newline at end of file diff --git a/packages/tokens-studio-for-figma/flags.d.ts b/packages/tokens-studio-for-figma/flags.d.ts index d7a07d45d3..1d559db82f 100644 --- a/packages/tokens-studio-for-figma/flags.d.ts +++ b/packages/tokens-studio-for-figma/flags.d.ts @@ -9,5 +9,6 @@ declare module 'launchdarkly-js-sdk-common' { secondScreen?: boolean; colorModifier?: boolean; idStorage?: boolean; + bypassLicenseCheck?: boolean; } } diff --git a/packages/tokens-studio-for-figma/src/app/components/AppContainer/startupProcessSteps/getLdFlagsFactory.ts b/packages/tokens-studio-for-figma/src/app/components/AppContainer/startupProcessSteps/getLdFlagsFactory.ts index 9d332909e9..53b5e9796c 100644 --- a/packages/tokens-studio-for-figma/src/app/components/AppContainer/startupProcessSteps/getLdFlagsFactory.ts +++ b/packages/tokens-studio-for-figma/src/app/components/AppContainer/startupProcessSteps/getLdFlagsFactory.ts @@ -16,10 +16,9 @@ export function getLdFlagsFactory(store: Store, ldClientPromise: Prom const state = store.getState(); const plan = planSelector(state); const entitlements = entitlementsSelector(state); - const licenseKey = licenseKeySelector(state); const clientEmail = clientEmailSelector(state); - if (user?.userId && licenseKey) { + if (user?.userId) { setUserData({ plan: plan ? 'pro' : 'free' }); try { await (await ldClientPromise)?.identify(ldUserFactory( diff --git a/packages/tokens-studio-for-figma/src/app/components/LaunchDarkly/LDProvider.tsx b/packages/tokens-studio-for-figma/src/app/components/LaunchDarkly/LDProvider.tsx index ca172e7468..2962630da5 100644 --- a/packages/tokens-studio-for-figma/src/app/components/LaunchDarkly/LDProvider.tsx +++ b/packages/tokens-studio-for-figma/src/app/components/LaunchDarkly/LDProvider.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { LDProvider } from 'launchdarkly-react-client-sdk'; import { useSelector } from 'react-redux'; import { userIdSelector } from '@/selectors/userIdSelector'; -import { licenseKeySelector } from '@/selectors/licenseKeySelector'; interface LDProviderProps { children: JSX.Element; @@ -12,21 +11,19 @@ const ldClientSideId = process.env.LAUNCHDARKLY_SDK_CLIENT || ''; export const LDProviderWrapper = ({ children }: LDProviderProps) => { const userId = useSelector(userIdSelector); - // @README we only want to set-up LD if there is a license key to reduce the amount of API calls - const licenseKey = useSelector(licenseKeySelector); return ( {children} ); }; -export function withLDProviderWrapper

(Component: React.ComponentType>>) { +export function withLDProviderWrapper

>(Component: React.ComponentType

) { return (props: P) => ( diff --git a/packages/tokens-studio-for-figma/src/app/components/LaunchDarkly/useFlags.ts b/packages/tokens-studio-for-figma/src/app/components/LaunchDarkly/useFlags.ts index 93d0533007..6253b8a689 100644 --- a/packages/tokens-studio-for-figma/src/app/components/LaunchDarkly/useFlags.ts +++ b/packages/tokens-studio-for-figma/src/app/components/LaunchDarkly/useFlags.ts @@ -1,9 +1,36 @@ import { useFlags as useFlagsRaw } from 'launchdarkly-react-client-sdk'; +// eslint-disable-next-line no-underscore-dangle +declare global { + interface Window { + // eslint-disable-next-line no-underscore-dangle + __CYPRESS_FEATURE_FLAGS__?: Record; + } +} + export const useFlags = process.env.LAUNCHDARKLY_FLAGS - ? () => ( - Object.fromEntries(process.env.LAUNCHDARKLY_FLAGS!.split(',').map((flag) => ( - [flag, true] - ))) - ) - : useFlagsRaw; + ? () => { + // Get flags from environment variable + const envFlags = Object.fromEntries( + process.env.LAUNCHDARKLY_FLAGS!.split(',').map((flag) => [flag, true]), + ); + + // Check if Cypress has set additional feature flags and merge them + // eslint-disable-next-line no-underscore-dangle + if (typeof window !== 'undefined' && window.__CYPRESS_FEATURE_FLAGS__) { + // eslint-disable-next-line no-underscore-dangle + return { ...envFlags, ...window.__CYPRESS_FEATURE_FLAGS__ }; + } + + return envFlags; + } + : () => { + // Check if Cypress has set feature flags + // eslint-disable-next-line no-underscore-dangle + if (typeof window !== 'undefined' && window.__CYPRESS_FEATURE_FLAGS__) { + // eslint-disable-next-line no-underscore-dangle + return window.__CYPRESS_FEATURE_FLAGS__; + } + // Otherwise use the real LaunchDarkly SDK + return useFlagsRaw(); + }; diff --git a/packages/tokens-studio-for-figma/src/app/components/ProBadge.test.tsx b/packages/tokens-studio-for-figma/src/app/components/ProBadge.test.tsx index 55df5fb302..48ab6f6b32 100644 --- a/packages/tokens-studio-for-figma/src/app/components/ProBadge.test.tsx +++ b/packages/tokens-studio-for-figma/src/app/components/ProBadge.test.tsx @@ -4,8 +4,7 @@ import { store } from '../store'; import ProBadge from './ProBadge'; describe('ProBadge', () => { - // Skipping test temporarily - it.skip('displays get pro badge if user is on free plan', () => { + it('displays get pro badge if user is on free plan', () => { const { getByText } = render(, { store }); expect(getByText('getPro')).toBeInTheDocument(); }); diff --git a/packages/tokens-studio-for-figma/src/app/components/__tests__/Navbar.test.tsx b/packages/tokens-studio-for-figma/src/app/components/__tests__/Navbar.test.tsx index d9a1166f1e..0b59e52797 100644 --- a/packages/tokens-studio-for-figma/src/app/components/__tests__/Navbar.test.tsx +++ b/packages/tokens-studio-for-figma/src/app/components/__tests__/Navbar.test.tsx @@ -8,9 +8,6 @@ import { import Navbar from '../Navbar'; describe('Navbar', () => { - beforeAll(() => { - process.env.LAUNCHDARKLY_FLAGS = 'tokenFlowButton'; - }); it('should work', async () => { const mockStore = createMockStore({}); @@ -26,8 +23,21 @@ describe('Navbar', () => { expect(mockStore.getState().uiState.activeTab).toEqual(Tabs.SETTINGS); }); - // TEMPORARY: Skipped while license check is bypassed - it.skip('displays the token flow button if user has access to it via license key', () => { + it('hides token flow button for free users', () => { + const mockStore = createMockStore({}); + const result = render( + + + , + ); + + // Without license key, button should not be visible + expect(() => { + result.getByTestId('token-flow-button'); + }).toThrowError(); + }); + + it('displays the token flow button if user has access to it via license key', () => { const mockStore = createMockStore({}); const result = render( @@ -35,10 +45,12 @@ describe('Navbar', () => { , ); + // Without license key, button should not be visible expect(() => { result.getByTestId('token-flow-button'); }).toThrowError(); + // After setting license key, button should appear mockStore.dispatch.userState.setLicenseKey('test-key-123'); waitFor(() => { const tokenFlowButton = result.getByTestId('token-flow-button'); @@ -46,8 +58,7 @@ describe('Navbar', () => { }); }); - // TEMPORARY: Skipped while license check is bypassed - it.skip('displays the token flow button if user has access to it via Studio PAT', () => { + it('displays the token flow button if user has access to it via Studio PAT', () => { const mockStore = createMockStore({}); const result = render( @@ -55,10 +66,12 @@ describe('Navbar', () => { , ); + // Without Studio PAT, button should not be visible expect(() => { result.getByTestId('token-flow-button'); }).toThrowError(); + // After setting Studio PAT, button should appear mockStore.dispatch.userState.setTokensStudioPAT('studio-pat-token-123'); waitFor(() => { const tokenFlowButton = result.getByTestId('token-flow-button'); @@ -66,12 +79,10 @@ describe('Navbar', () => { }); }); - // TEMPORARY: Skipped while license check is bypassed it('should open the token flow page when the button is clicked', async () => { global.open = jest.fn(); const mockStore = createMockStore({}); - const result = render( @@ -86,7 +97,18 @@ describe('Navbar', () => { }); }); - // TEMPORARY: Skipped while license check is bypassed + it('hides second screen icon for free users', () => { + const mockStore = createMockStore({}); + const result = render( + + + , + ); + + // Without license, second screen should not be visible + expect(result.queryByLabelText('Second Screen')).not.toBeInTheDocument(); + }); + it('displays the second screen icon if user has access to it via license key', () => { const mockStore = createMockStore({}); const result = render( @@ -104,7 +126,6 @@ describe('Navbar', () => { }); }); - // TEMPORARY: Skipped while license check is bypassed it('displays the second screen icon if user has access to it via Studio PAT', () => { const mockStore = createMockStore({}); const result = render( diff --git a/packages/tokens-studio-for-figma/src/app/hooks/useIsProUser.ts b/packages/tokens-studio-for-figma/src/app/hooks/useIsProUser.ts index 35463179cf..36aa107bda 100644 --- a/packages/tokens-studio-for-figma/src/app/hooks/useIsProUser.ts +++ b/packages/tokens-studio-for-figma/src/app/hooks/useIsProUser.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { useSelector } from 'react-redux'; import { useMemo } from 'react'; import { licenseKeySelector } from '@/selectors/licenseKeySelector'; @@ -12,17 +11,20 @@ export function useIsProUser() { const validPAT = useSelector(tokensStudioPATSelector); const flags = useFlags(); - // TEMPORARY: License server and LaunchDarkly are down, always return true - return useMemo(() => true, []); - - /* Original implementation - restore when servers are back up return useMemo(() => { // Feature flag to bypass license check when server is down - if (flags.bypassLicenseCheck) { + // If LaunchDarkly is down, flags.bypassLicenseCheck will be undefined + // In that case, default to false to enforce normal license validation + if (flags.bypassLicenseCheck === undefined) { + return Boolean(existingKey && !licenseKeyError) || Boolean(validPAT); + } + + if (flags.bypassLicenseCheck === true) { + // Feature flag explicitly enabled, bypass license check return true; } + // Feature flag is false, use normal license validation return Boolean(existingKey && !licenseKeyError) || Boolean(validPAT); }, [existingKey, licenseKeyError, validPAT, flags.bypassLicenseCheck]); - */ } diff --git a/packages/tokens-studio-for-figma/tests/config/setupTest.tsx b/packages/tokens-studio-for-figma/tests/config/setupTest.tsx index 6d8e439f71..37882d76ef 100644 --- a/packages/tokens-studio-for-figma/tests/config/setupTest.tsx +++ b/packages/tokens-studio-for-figma/tests/config/setupTest.tsx @@ -12,6 +12,14 @@ import { RootState, store } from '../../src/app/store'; import { models } from '../../src/app/store/models'; import { undoableEnhancer } from '@/app/enhancers/undoableEnhancer'; +// Mock the useFlags hook from LaunchDarkly wrapper +// Set bypassLicenseCheck to false globally to test normal license validation +jest.mock('@/app/components/LaunchDarkly', () => ({ + useFlags: jest.fn(() => ({ bypassLicenseCheck: false })), +})); + +export const mockUseFlags = require('@/app/components/LaunchDarkly').useFlags; + export const AllTheProviders: FC = ({ children }) => (