chore: useScopedPersistentState custom hook

This commit is contained in:
mazzucchelli 2025-06-03 18:50:39 +02:00
parent 8ecdda9ea1
commit 83c9d185c6
8 changed files with 70 additions and 17 deletions

View File

@ -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<string | undefined>(
const [cachedTrialEndsAt, setCachedTrialEndsAt] = useScopedPersistentState<string | undefined>(
'STRAPI_FREE_TRIAL_ENDS_AT',
undefined
);

View File

@ -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(<UpsellBanner />);
@ -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(<UpsellBanner />);

View File

@ -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);
});
});

View File

@ -1,5 +1,7 @@
import { useEffect, useState } from 'react';
import { useInitQuery } from '../services/admin';
const usePersistentState = <T>(key: string, defaultValue: T) => {
const [value, setValue] = useState<T>(() => {
const stickyValue = window.localStorage.getItem(key);
@ -23,4 +25,14 @@ const usePersistentState = <T>(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 = <T>(key: string, defaultValue: T) => {
const { data: initData } = useInitQuery();
const { uuid } = initData ?? {};
const namespacedKey = `${key}:${uuid}`;
return usePersistentState<T>(namespacedKey, defaultValue);
};
export { usePersistentState, useScopedPersistentState };

View File

@ -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<string | undefined>(
const [cachedTrialEndsAt] = useScopedPersistentState<string | undefined>(
'STRAPI_FREE_TRIAL_ENDS_AT',
undefined
);

View File

@ -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
);

View File

@ -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(() => ({

View File

@ -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: {