Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tricky-toys-destroy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tokens-studio/figma-plugin": patch
---

Add launch darkly feature flag to control bypassing of license checks
6 changes: 4 additions & 2 deletions packages/tokens-studio-for-figma/cypress/e2e/branches.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions packages/tokens-studio-for-figma/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
})
1 change: 1 addition & 0 deletions packages/tokens-studio-for-figma/flags.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ declare module 'launchdarkly-js-sdk-common' {
secondScreen?: boolean;
colorModifier?: boolean;
idStorage?: boolean;
bypassLicenseCheck?: boolean;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type { LDClient } from 'launchdarkly-js-client-sdk';
import { RootState } from '@/app/store';
import { entitlementsSelector } from '@/selectors/getEntitlements';
import { licenseKeySelector } from '@/selectors/licenseKeySelector';

Check failure on line 6 in packages/tokens-studio-for-figma/src/app/components/AppContainer/startupProcessSteps/getLdFlagsFactory.ts

View workflow job for this annotation

GitHub Actions / ESLint

'licenseKeySelector' is defined but never used. Allowed unused vars must match /^_/u
import { planSelector } from '@/selectors/planSelector';
import type { StartupMessage } from '@/types/AsyncMessages';
import { setUserData } from '@/utils/analytics';
Expand All @@ -16,10 +16,9 @@
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 (
<LDProvider
deferInitialization
clientSideID={ldClientSideId}
user={userId && licenseKey ? { key: userId } : undefined}
user={userId ? { key: userId } : undefined}
>
{children}
</LDProvider>
);
};

export function withLDProviderWrapper<P>(Component: React.ComponentType<React.PropsWithChildren<React.PropsWithChildren<P>>>) {
export function withLDProviderWrapper<P extends Record<string, any>>(Component: React.ComponentType<P>) {
return (props: P) => (
<LDProviderWrapper>
<Component {...props} />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, any>;
}
}

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();
};
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ProBadge campaign="test" />, { store });
expect(getByText('getPro')).toBeInTheDocument();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import {
import Navbar from '../Navbar';

describe('Navbar', () => {
beforeAll(() => {
process.env.LAUNCHDARKLY_FLAGS = 'tokenFlowButton';
});

it('should work', async () => {
const mockStore = createMockStore({});
Expand All @@ -26,52 +23,66 @@ 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(
<Provider store={mockStore}>
<Navbar />
</Provider>,
);

// 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(
<Provider store={mockStore}>
<Navbar />
</Provider>,
);

// 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');
expect(tokenFlowButton).toBeInTheDocument();
});
});

// 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(
<Provider store={mockStore}>
<Navbar />
</Provider>,
);

// 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');
expect(tokenFlowButton).toBeInTheDocument();
});
});

// 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(
<Provider store={mockStore}>
<Navbar />
Expand All @@ -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(
<Provider store={mockStore}>
<Navbar />
</Provider>,
);

// 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(
Expand All @@ -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(
Expand Down
16 changes: 9 additions & 7 deletions packages/tokens-studio-for-figma/src/app/hooks/useIsProUser.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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]);
*/
}
8 changes: 8 additions & 0 deletions packages/tokens-studio-for-figma/tests/config/setupTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
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;

Check failure on line 21 in packages/tokens-studio-for-figma/tests/config/setupTest.tsx

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected require()

export const AllTheProviders: FC = ({ children }) => (
<Provider store={store}>
<TooltipProvider>
Expand Down
Loading