diff --git a/packages/core/admin/admin/src/components/UpsellBanner.tsx b/packages/core/admin/admin/src/components/UpsellBanner.tsx index 16d19fe2a4..6bf2e2b06f 100644 --- a/packages/core/admin/admin/src/components/UpsellBanner.tsx +++ b/packages/core/admin/admin/src/components/UpsellBanner.tsx @@ -7,7 +7,7 @@ import { useIntl } from 'react-intl'; import { styled } from 'styled-components'; import { useGetLicenseTrialTimeLeftQuery } from '../../src/services/admin'; -import { usePersistentState } from '../hooks/usePersistentState'; +import { useScopedPersistentState } from '../hooks/usePersistentState'; const BannerBackground = styled(Flex)` background: linear-gradient( @@ -101,7 +101,7 @@ const Banner = ({ isTrialEndedRecently }: { isTrialEndedRecently: boolean }) => const UpsellBanner = () => { const { license } = useLicenseLimits(); - const [cachedTrialEndsAt, setCachedTrialEndsAt] = usePersistentState( + const [cachedTrialEndsAt, setCachedTrialEndsAt] = useScopedPersistentState( 'STRAPI_FREE_TRIAL_ENDS_AT', undefined ); diff --git a/packages/core/admin/admin/src/components/tests/UpsellBanner.test.tsx b/packages/core/admin/admin/src/components/tests/UpsellBanner.test.tsx index 3a5dc84443..826ff4889b 100644 --- a/packages/core/admin/admin/src/components/tests/UpsellBanner.test.tsx +++ b/packages/core/admin/admin/src/components/tests/UpsellBanner.test.tsx @@ -18,11 +18,16 @@ jest.mock('../../../src/services/admin', () => ({ trialEndsAt: '2025-05-15T00:00:00.000Z', }, })), + useInitQuery: jest.fn(() => ({ + data: { + uuid: 'test-uuid', + }, + })), })); describe('UpsellBanner', () => { beforeEach(() => { - localStorage.removeItem('STRAPI_FREE_TRIAL_ENDS_AT'); + localStorage.removeItem('STRAPI_FREE_TRIAL_ENDS_AT:test-uuid'); }); beforeAll(() => { @@ -82,7 +87,7 @@ describe('UpsellBanner', () => { data: {}, })); - localStorage.setItem('STRAPI_FREE_TRIAL_ENDS_AT', '2025-05-21T09:50:00.000Z'); + localStorage.setItem('STRAPI_FREE_TRIAL_ENDS_AT:test-uuid', '2025-05-21T09:50:00.000Z'); jest.setSystemTime(new Date(2025, 4, 22)); render(); @@ -114,7 +119,7 @@ describe('UpsellBanner', () => { data: {}, })); - localStorage.setItem('STRAPI_FREE_TRIAL_ENDS_AT', '2025-05-10T09:50:00.000Z'); + localStorage.setItem('STRAPI_FREE_TRIAL_ENDS_AT:test-uuid', '2025-05-10T09:50:00.000Z'); jest.setSystemTime(new Date(2025, 4, 22)); render(); diff --git a/packages/core/admin/admin/src/hooks/tests/usePersistentState.test.ts b/packages/core/admin/admin/src/hooks/tests/usePersistentState.test.ts index 23b4a9f146..84e37e237f 100644 --- a/packages/core/admin/admin/src/hooks/tests/usePersistentState.test.ts +++ b/packages/core/admin/admin/src/hooks/tests/usePersistentState.test.ts @@ -1,6 +1,14 @@ import { renderHook, act } from '@testing-library/react'; -import { usePersistentState } from '../usePersistentState'; +import { usePersistentState, useScopedPersistentState } from '../usePersistentState'; + +jest.mock('../../services/admin', () => ({ + useInitQuery: jest.fn(() => ({ + data: { + uuid: 'test-uuid', + }, + })), +})); describe('usePersistentState', () => { it('should return the value passed to set in the local storage', async () => { @@ -15,3 +23,18 @@ describe('usePersistentState', () => { expect(updatedValue).toBe(1); }); }); + +describe('useScopedPersistentState', () => { + it('should return the value passed to set in the local storage with a scoped key', async () => { + const { result } = renderHook(() => useScopedPersistentState('key', 0)); + const [value, setValue] = result.current; + expect(value).toBe(0); + + act(() => { + setValue(1); + }); + const [updatedValue] = result.current; + expect(localStorage.getItem('key:test-uuid')).toBeDefined(); + expect(updatedValue).toBe(1); + }); +}); diff --git a/packages/core/admin/admin/src/hooks/usePersistentState.ts b/packages/core/admin/admin/src/hooks/usePersistentState.ts index b70bb13d3e..156147dfaa 100644 --- a/packages/core/admin/admin/src/hooks/usePersistentState.ts +++ b/packages/core/admin/admin/src/hooks/usePersistentState.ts @@ -1,5 +1,7 @@ import { useEffect, useState } from 'react'; +import { useInitQuery } from '../services/admin'; + const usePersistentState = (key: string, defaultValue: T) => { const [value, setValue] = useState(() => { const stickyValue = window.localStorage.getItem(key); @@ -23,4 +25,14 @@ const usePersistentState = (key: string, defaultValue: T) => { return [value, setValue] as const; }; -export { usePersistentState }; +// Same as usePersistentState, but scoped to the current instance of Strapi +// useful for storing state that should not be shared across different instances of Strapi running on localhost +const useScopedPersistentState = (key: string, defaultValue: T) => { + const { data: initData } = useInitQuery(); + const { uuid } = initData ?? {}; + + const namespacedKey = `${key}:${uuid}`; + return usePersistentState(namespacedKey, defaultValue); +}; + +export { usePersistentState, useScopedPersistentState }; diff --git a/packages/core/admin/admin/src/pages/Home/components/FreeTrialEndedModal.tsx b/packages/core/admin/admin/src/pages/Home/components/FreeTrialEndedModal.tsx index 849429c06e..84186996b4 100644 --- a/packages/core/admin/admin/src/pages/Home/components/FreeTrialEndedModal.tsx +++ b/packages/core/admin/admin/src/pages/Home/components/FreeTrialEndedModal.tsx @@ -7,7 +7,7 @@ import { useIntl } from 'react-intl'; import styled from 'styled-components'; import { useLicenseLimits } from '../../../../../ee/admin/src/hooks/useLicenseLimits'; -import { usePersistentState } from '../../../hooks/usePersistentState'; +import { useScopedPersistentState } from '../../../hooks/usePersistentState'; const StyledModalContent = styled(Modal.Content)` max-width: 51.6rem; @@ -30,11 +30,11 @@ const StyledButton = styled(Button)` export const FreeTrialEndedModal = () => { const { formatMessage } = useIntl(); const [open, setOpen] = useState(true); - const [previouslyOpen, setPreviouslyOpen] = usePersistentState( + const [previouslyOpen, setPreviouslyOpen] = useScopedPersistentState( 'STRAPI_FREE_TRIAL_ENDED_MODAL', false ); - const [cachedTrialEndsAt] = usePersistentState( + const [cachedTrialEndsAt] = useScopedPersistentState( 'STRAPI_FREE_TRIAL_ENDS_AT', undefined ); diff --git a/packages/core/admin/admin/src/pages/Home/components/FreeTrialWelcomeModal.tsx b/packages/core/admin/admin/src/pages/Home/components/FreeTrialWelcomeModal.tsx index dfb35b4a62..fecb6ff8b4 100644 --- a/packages/core/admin/admin/src/pages/Home/components/FreeTrialWelcomeModal.tsx +++ b/packages/core/admin/admin/src/pages/Home/components/FreeTrialWelcomeModal.tsx @@ -7,7 +7,7 @@ import styled from 'styled-components'; import { useLicenseLimits } from '../../../../../ee/admin/src/hooks/useLicenseLimits'; import lightIllustration from '../../../assets/images/free-trial.png'; -import { usePersistentState } from '../../../hooks/usePersistentState'; +import { useScopedPersistentState } from '../../../hooks/usePersistentState'; const StyledModalContent = styled(Modal.Content)` max-width: 51.6rem; @@ -34,7 +34,7 @@ const StyledButton = styled(Button)` export const FreeTrialWelcomeModal = () => { const { formatMessage } = useIntl(); const [open, setOpen] = useState(true); - const [previouslyOpen, setPreviouslyOpen] = usePersistentState( + const [previouslyOpen, setPreviouslyOpen] = useScopedPersistentState( 'STRAPI_FREE_TRIAL_WELCOME_MODAL', false ); diff --git a/packages/core/admin/admin/src/pages/Home/components/tests/FreeTrialEndedModal.test.tsx b/packages/core/admin/admin/src/pages/Home/components/tests/FreeTrialEndedModal.test.tsx index 5a6ca636a7..3275ef3b53 100644 --- a/packages/core/admin/admin/src/pages/Home/components/tests/FreeTrialEndedModal.test.tsx +++ b/packages/core/admin/admin/src/pages/Home/components/tests/FreeTrialEndedModal.test.tsx @@ -18,12 +18,17 @@ jest.mock('../../../../../src/services/admin', () => ({ trialEndsAt: '2025-05-15T00:00:00.000Z', }, })), + useInitQuery: jest.fn(() => ({ + data: { + uuid: 'test-uuid', + }, + })), })); describe('FreeTrialEndedModal', () => { beforeEach(() => { - localStorage.removeItem('STRAPI_FREE_TRIAL_ENDS_AT'); - localStorage.removeItem('STRAPI_FREE_TRIAL_ENDED_MODAL'); + localStorage.removeItem('STRAPI_FREE_TRIAL_ENDS_AT:test-uuid'); + localStorage.removeItem('STRAPI_FREE_TRIAL_ENDED_MODAL:test-uuid'); }); beforeAll(() => { @@ -36,7 +41,7 @@ describe('FreeTrialEndedModal', () => { }); it('should render when trial ended less than 7 days ago and modal never appeared before', async () => { - localStorage.setItem('STRAPI_FREE_TRIAL_ENDS_AT', '2025-05-21T09:50:00.000Z'); + localStorage.setItem('STRAPI_FREE_TRIAL_ENDS_AT:test-uuid', '2025-05-21T09:50:00.000Z'); // @ts-expect-error – mock useLicenseLimits.mockImplementationOnce(() => ({ @@ -53,8 +58,8 @@ describe('FreeTrialEndedModal', () => { }); it('should not render when trial ended less than 7 days ago but modal already appeared before', async () => { - localStorage.setItem('STRAPI_FREE_TRIAL_ENDS_AT', '2025-05-21T09:50:00.000Z'); - localStorage.setItem('STRAPI_FREE_TRIAL_ENDED_MODAL', 'true'); + localStorage.setItem('STRAPI_FREE_TRIAL_ENDS_AT:test-uuid', '2025-05-21T09:50:00.000Z'); + localStorage.setItem('STRAPI_FREE_TRIAL_ENDED_MODAL:test-uuid', 'true'); // @ts-expect-error – mock useLicenseLimits.mockImplementationOnce(() => ({ diff --git a/packages/core/admin/admin/src/pages/Home/components/tests/FreeTrialWelcomeModal.test.tsx b/packages/core/admin/admin/src/pages/Home/components/tests/FreeTrialWelcomeModal.test.tsx index d4fa886cd1..4017fdc13f 100644 --- a/packages/core/admin/admin/src/pages/Home/components/tests/FreeTrialWelcomeModal.test.tsx +++ b/packages/core/admin/admin/src/pages/Home/components/tests/FreeTrialWelcomeModal.test.tsx @@ -3,6 +3,14 @@ import { render, screen, waitFor } from '@tests/utils'; import { useLicenseLimits } from '../../../../../../ee/admin/src/hooks/useLicenseLimits'; import { FreeTrialWelcomeModal } from '../FreeTrialWelcomeModal'; +jest.mock('../../../../services/admin', () => ({ + useInitQuery: jest.fn(() => ({ + data: { + uuid: 'test-uuid', + }, + })), +})); + jest.mock('../../../../../../ee/admin/src/hooks/useLicenseLimits', () => ({ useLicenseLimits: jest.fn(() => ({ license: {