diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index c4136a10ddc7..d794f4d2fa58 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -5026,6 +5026,12 @@ "slideFundWalletTitle": { "message": "Fund your wallet" }, + "slideRemoteModeDescription": { + "message": "Access your hardware wallet without plugging it in" + }, + "slideRemoteModeTitle": { + "message": "Remote mode is here!" + }, "slideSweepStakeDescription": { "message": "Mint an NFT now for a chance to win" }, diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 9bef187a06cc..9794db5c167f 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -5026,6 +5026,12 @@ "slideFundWalletTitle": { "message": "Fund your wallet" }, + "slideRemoteModeDescription": { + "message": "Access your hardware wallet without plugging it in" + }, + "slideRemoteModeTitle": { + "message": "Remote mode is here!" + }, "slideSweepStakeDescription": { "message": "Mint an NFT now for a chance to win" }, diff --git a/app/scripts/controllers/app-state-controller.ts b/app/scripts/controllers/app-state-controller.ts index 075397dca721..7106edc64bc7 100644 --- a/app/scripts/controllers/app-state-controller.ts +++ b/app/scripts/controllers/app-state-controller.ts @@ -563,34 +563,27 @@ export class AppStateController extends BaseController< } /** - * Updates slides by adding new slides that don't already exist in state + * Replaces slides in state with new slides. If a slide with the same id + * already exists, it will be merged with the new slide. * - * @param slides - Array of new slides to add + * @param slides - Array of new slides */ updateSlides(slides: CarouselSlide[]): void { this.update((state) => { const currentSlides = state.slides || []; - // Updates the undismissable property for slides that already exist in state - const updatedCurrentSlides = currentSlides.map((currentSlide) => { - const matchingNewSlide = slides.find((s) => s.id === currentSlide.id); - if (matchingNewSlide) { + const newSlides = slides.map((slide) => { + const existingSlide = currentSlides.find((s) => s.id === slide.id); + if (existingSlide) { return { - ...currentSlide, - undismissable: matchingNewSlide.undismissable, + ...existingSlide, + ...slide, }; } - return currentSlide; - }); - - // Adds new slides that don't already exist in state - const newSlides = slides.filter((newSlide) => { - return !currentSlides.some( - (currentSlide) => currentSlide.id === newSlide.id, - ); + return slide; }); - state.slides = [...newSlides, ...updatedCurrentSlides]; + state.slides = [...newSlides]; }); } diff --git a/ui/components/multichain/account-overview/account-overview-layout.tsx b/ui/components/multichain/account-overview/account-overview-layout.tsx index 1018cb3e93eb..7f5e8731fdc6 100644 --- a/ui/components/multichain/account-overview/account-overview-layout.tsx +++ b/ui/components/multichain/account-overview/account-overview-layout.tsx @@ -6,7 +6,6 @@ import { isEqual } from 'lodash'; import { removeSlide } from '../../../store/actions'; import { Carousel } from '..'; import { - getSelectedAccountCachedBalance, getAppIsLoading, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) getSwapsDefaultToken, @@ -21,10 +20,7 @@ import { MetaMetricsEventCategory, } from '../../../../shared/constants/metametrics'; import type { CarouselSlide } from '../../../../shared/constants/app-state'; -import { - useCarouselManagement, - ZERO_BALANCE, -} from '../../../hooks/useCarouselManagement'; +import { useCarouselManagement } from '../../../hooks/useCarouselManagement'; import { AccountOverviewTabsProps, AccountOverviewTabs, @@ -39,7 +35,6 @@ export const AccountOverviewLayout = ({ ...tabsProps }: AccountOverviewLayoutProps) => { const dispatch = useDispatch(); - const totalBalance = useSelector(getSelectedAccountCachedBalance); const isLoading = useSelector(getAppIsLoading); const trackEvent = useContext(MetaMetricsContext); const [hasRendered, setHasRendered] = useState(false); @@ -48,10 +43,7 @@ export const AccountOverviewLayout = ({ const defaultSwapsToken = useSelector(getSwapsDefaultToken, isEqual); ///: END:ONLY_INCLUDE_IF - const hasZeroBalance = totalBalance === ZERO_BALANCE; - const { slides } = useCarouselManagement({ - hasZeroBalance, - }); + const { slides } = useCarouselManagement(); ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) const { openBridgeExperience } = useBridging(); @@ -78,9 +70,6 @@ export const AccountOverviewLayout = ({ }; const handleRemoveSlide = (isLastSlide: boolean, id: string) => { - if (id === 'fund' && hasZeroBalance) { - return; - } if (isLastSlide) { trackEvent({ event: MetaMetricsEventName.BannerCloseAll, diff --git a/ui/hooks/useCarouselManagement/constants.ts b/ui/hooks/useCarouselManagement/constants.ts index 26ca25199442..0d5f427c24bd 100644 --- a/ui/hooks/useCarouselManagement/constants.ts +++ b/ui/hooks/useCarouselManagement/constants.ts @@ -1,3 +1,12 @@ +export const REMOTE_MODE_SLIDE = { + id: 'remoteMode', + title: 'slideRemoteModeTitle', + description: 'slideRemoteModeDescription', + // TODO: Update image once we have a remote mode icon + image: './images/slide-fund-icon.svg', + href: '/home.html#remote', +}; + export const SWEEPSTAKES_SLIDE = { id: 'sweepStake', title: 'slideSweepStakeTitle', @@ -39,7 +48,7 @@ export const CASH_SLIDE = { href: 'https://portfolio.metamask.io/sell', }; -export const ZERO_BALANCE = '0x00'; +export const ZERO_BALANCE = '0x0'; export const SWEEPSTAKES_START = new Date('2025-04-09T00:00:00Z'); export const SWEEPSTAKES_END = new Date('2025-04-15T23:59:59Z'); diff --git a/ui/hooks/useCarouselManagement/useCarouselManagement.test.ts b/ui/hooks/useCarouselManagement/useCarouselManagement.test.ts index 17f585244b38..eccb3a48e4d5 100644 --- a/ui/hooks/useCarouselManagement/useCarouselManagement.test.ts +++ b/ui/hooks/useCarouselManagement/useCarouselManagement.test.ts @@ -1,10 +1,12 @@ import { renderHook } from '@testing-library/react-hooks'; import { useDispatch, useSelector } from 'react-redux'; -import { removeSlide, updateSlides } from '../../store/actions'; +import { updateSlides } from '../../store/actions'; +import { getSelectedAccountCachedBalance, getSlides } from '../../selectors'; +import { getIsRemoteModeEnabled } from '../../selectors/remote-mode'; import { CarouselSlide } from '../../../shared/constants/app-state'; import { - useCarouselManagement, getSweepstakesCampaignActive, + useCarouselManagement, } from './useCarouselManagement'; import { FUND_SLIDE, @@ -14,39 +16,146 @@ import { SWEEPSTAKES_SLIDE, SWEEPSTAKES_START, SWEEPSTAKES_END, + ZERO_BALANCE, + REMOTE_MODE_SLIDE, } from './constants'; +const SLIDES_ZERO_FUNDS_REMOTE_OFF_SWEEPSTAKES_OFF = [ + { ...FUND_SLIDE, undismissable: true }, + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + BRIDGE_SLIDE, + ///: END:ONLY_INCLUDE_IF + CARD_SLIDE, + CASH_SLIDE, +]; + +const SLIDES_POSITIVE_FUNDS_REMOTE_OFF_SWEEPSTAKES_OFF = [ + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + BRIDGE_SLIDE, + ///: END:ONLY_INCLUDE_IF + CARD_SLIDE, + { ...FUND_SLIDE, undismissable: false }, + CASH_SLIDE, +]; + +const SLIDES_ZERO_FUNDS_REMOTE_ON_SWEEPSTAKES_OFF = [ + REMOTE_MODE_SLIDE, + { ...FUND_SLIDE, undismissable: true }, + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + BRIDGE_SLIDE, + ///: END:ONLY_INCLUDE_IF + CARD_SLIDE, + CASH_SLIDE, +]; + +const SLIDES_POSITIVE_FUNDS_REMOTE_ON_SWEEPSTAKES_OFF = [ + REMOTE_MODE_SLIDE, + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + BRIDGE_SLIDE, + ///: END:ONLY_INCLUDE_IF + CARD_SLIDE, + { ...FUND_SLIDE, undismissable: false }, + CASH_SLIDE, +]; + +const SLIDES_ZERO_FUNDS_REMOTE_OFF_SWEEPSTAKES_ON = [ + { ...SWEEPSTAKES_SLIDE, dismissed: false }, + { ...FUND_SLIDE, undismissable: true }, + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + BRIDGE_SLIDE, + ///: END:ONLY_INCLUDE_IF + CARD_SLIDE, + CASH_SLIDE, +]; + +const SLIDES_POSITIVE_FUNDS_REMOTE_OFF_SWEEPSTAKES_ON = [ + { ...SWEEPSTAKES_SLIDE, dismissed: false }, + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + BRIDGE_SLIDE, + ///: END:ONLY_INCLUDE_IF + CARD_SLIDE, + { ...FUND_SLIDE, undismissable: false }, + CASH_SLIDE, +]; + +const SLIDES_ZERO_FUNDS_REMOTE_ON_SWEEPSTAKES_ON = [ + { ...SWEEPSTAKES_SLIDE, dismissed: false }, + REMOTE_MODE_SLIDE, + { ...FUND_SLIDE, undismissable: true }, + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + BRIDGE_SLIDE, + ///: END:ONLY_INCLUDE_IF + CARD_SLIDE, + CASH_SLIDE, +]; + +const SLIDES_POSITIVE_FUNDS_REMOTE_ON_SWEEPSTAKES_ON = [ + { ...SWEEPSTAKES_SLIDE, dismissed: false }, + REMOTE_MODE_SLIDE, + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + BRIDGE_SLIDE, + ///: END:ONLY_INCLUDE_IF + CARD_SLIDE, + { ...FUND_SLIDE, undismissable: false }, + CASH_SLIDE, +]; + jest.mock('react-redux', () => ({ useDispatch: jest.fn(), - useSelector: jest.fn(), + useSelector: jest.fn((selector) => selector()), })); jest.mock('../../store/actions', () => ({ - removeSlide: jest.fn((id) => ({ type: 'REMOVE_SLIDE', payload: id })), - updateSlides: jest.fn((slides) => ({ - type: 'UPDATE_SLIDES', - payload: slides, - })), + updateSlides: jest.fn(), })); -const mockUseDispatch = useDispatch as jest.MockedFunction; -const mockUseSelector = useSelector as jest.MockedFunction; -const mockUpdateSlides = updateSlides as jest.MockedFunction< - typeof updateSlides ->; -const mockRemoveSlide = removeSlide as jest.MockedFunction; +jest.mock('../../selectors/selectors.js', () => ({ + ...jest.requireActual('../../selectors/selectors.js'), + getSelectedAccountCachedBalance: jest.fn(), + getSlides: jest.fn(), +})); -describe('useCarouselManagement', () => { - let mockDispatch: jest.Mock; - let slides: CarouselSlide[]; +jest.mock('../../selectors/remote-feature-flags', () => ({ + getIsRemoteModeEnabled: jest.fn(), +})); - beforeEach(() => { - mockDispatch = jest.fn(); - mockUseDispatch.mockReturnValue(mockDispatch); +const mockUpdateSlides = jest.mocked(updateSlides); +const mockUseSelector = jest.mocked(useSelector); +const mockUseDispatch = jest.mocked(useDispatch); - slides = []; - mockUseSelector.mockImplementation(() => slides); +const mockGetSlides = jest.fn(); +const mockGetSelectedAccountCachedBalance = jest.fn(); +const mockGetIsRemoteModeEnabled = jest.fn(); +describe('useCarouselManagement', () => { + let validTestDate: string; + let invalidTestDate: string; + + beforeEach(() => { + // Test dates + validTestDate = new Date(SWEEPSTAKES_START.getTime() + 1000).toISOString(); // 1 day after + invalidTestDate = new Date( + SWEEPSTAKES_START.getTime() - 1000, + ).toISOString(); // 1 day before + // Mocks + mockUseDispatch.mockReturnValue(jest.fn()); + mockUseSelector.mockImplementation((selector) => { + if (selector === getSlides) { + return mockGetSlides(); + } + if (selector === getSelectedAccountCachedBalance) { + return mockGetSelectedAccountCachedBalance(); + } + if (selector === getIsRemoteModeEnabled) { + return mockGetIsRemoteModeEnabled(); + } + return undefined; + }); + // Default values + mockGetSlides.mockReturnValue([]); + mockGetSelectedAccountCachedBalance.mockReturnValue(ZERO_BALANCE); + mockGetIsRemoteModeEnabled.mockReturnValue(false); + // Reset mocks jest.clearAllMocks(); }); @@ -67,273 +176,185 @@ describe('useCarouselManagement', () => { }); }); - describe('hook behavior', () => { - it('should build slides correctly when sweepstakes is not active', () => { - const testDate = new Date( - SWEEPSTAKES_START.getTime() - 86400000, - ).toISOString(); // 1 day before - - renderHook(() => - useCarouselManagement({ - hasZeroBalance: false, - testDate, - }), - ); - - expect(mockUpdateSlides).toHaveBeenCalled(); + describe('zero funds, remote off, sweepstakes off', () => { + it('should have correct slide order', () => { + renderHook(() => useCarouselManagement({ testDate: invalidTestDate })); const updatedSlides = mockUpdateSlides.mock.calls[0][0]; - const fundSlide = updatedSlides.find( - (slide: CarouselSlide) => slide.id === FUND_SLIDE.id, - ); - expect(fundSlide).toBeDefined(); - expect(fundSlide?.undismissable).toBe(false); - - expect( - updatedSlides.some( - (slide: CarouselSlide) => slide.id === BRIDGE_SLIDE.id, - ), - ).toBe(true); - expect( - updatedSlides.some( - (slide: CarouselSlide) => slide.id === CARD_SLIDE.id, - ), - ).toBe(true); - expect( - updatedSlides.some( - (slide: CarouselSlide) => slide.id === CASH_SLIDE.id, - ), - ).toBe(true); - - const hasSweepstakesSlide = updatedSlides.some( - (slide: CarouselSlide) => slide.id === SWEEPSTAKES_SLIDE.id, + expect(updatedSlides).toStrictEqual( + SLIDES_ZERO_FUNDS_REMOTE_OFF_SWEEPSTAKES_OFF, ); - expect(hasSweepstakesSlide).toBe(false); }); - it('should build slides correctly when sweepstakes is active', () => { - const testDate = new Date( - SWEEPSTAKES_START.getTime() + 1000, - ).toISOString(); - - renderHook(() => - useCarouselManagement({ - hasZeroBalance: false, - testDate, - }), - ); + it('should mark fund slide as undismissable', () => { + renderHook(() => useCarouselManagement({ testDate: invalidTestDate })); const updatedSlides = mockUpdateSlides.mock.calls[0][0]; - const sweepstakesSlide = updatedSlides.find( - (slide: CarouselSlide) => slide.id === SWEEPSTAKES_SLIDE.id, - ); - expect(sweepstakesSlide).toBeDefined(); - expect(sweepstakesSlide?.dismissed).toBe(false); - - expect(updatedSlides[0].id).toBe(SWEEPSTAKES_SLIDE.id); - expect(updatedSlides[1].id).toBe(FUND_SLIDE.id); + expect(updatedSlides[0].undismissable).toBe(true); }); + }); - it('should mark fund slide as undismissable when hasZeroBalance is true', () => { - const testDate = new Date( - SWEEPSTAKES_START.getTime() - 86400000, - ).toISOString(); + describe('zero funds, remote on, sweepstakes off', () => { + beforeEach(() => { + mockGetIsRemoteModeEnabled.mockReturnValue(true); + }); - renderHook(() => - useCarouselManagement({ - hasZeroBalance: true, - testDate, - }), - ); + it('should have correct slide order', () => { + renderHook(() => useCarouselManagement({ testDate: invalidTestDate })); const updatedSlides = mockUpdateSlides.mock.calls[0][0]; - const fundSlide = updatedSlides.find( - (slide: CarouselSlide) => slide.id === FUND_SLIDE.id, + expect(updatedSlides).toStrictEqual( + SLIDES_ZERO_FUNDS_REMOTE_ON_SWEEPSTAKES_OFF, ); - expect(fundSlide).toBeDefined(); - expect(fundSlide?.undismissable).toBe(true); }); + }); - it('should remove sweepstakes slide if it exists and sweepstakes is not active', () => { - slides = [{ ...SWEEPSTAKES_SLIDE }]; - const testDate = new Date( - SWEEPSTAKES_END.getTime() + 86400000, - ).toISOString(); + describe('zero funds, remote off, sweepstakes on', () => { + it('should have correct slide order', () => { + renderHook(() => useCarouselManagement({ testDate: validTestDate })); - renderHook(() => - useCarouselManagement({ - hasZeroBalance: false, - testDate, - }), + const updatedSlides = mockUpdateSlides.mock.calls[0][0]; + + expect(updatedSlides).toStrictEqual( + SLIDES_ZERO_FUNDS_REMOTE_OFF_SWEEPSTAKES_ON, ); + }); + }); - expect(mockRemoveSlide).toHaveBeenCalledWith(SWEEPSTAKES_SLIDE.id); + describe('zero funds, remote on, sweepstakes on', () => { + beforeEach(() => { + mockGetIsRemoteModeEnabled.mockReturnValue(true); }); - it('should update slides when hasZeroBalance changes', () => { - const testDate = new Date().toISOString(); - const { rerender } = renderHook((props) => useCarouselManagement(props), { - initialProps: { hasZeroBalance: false, testDate }, - }); + it('should have correct slide order', () => { + renderHook(() => useCarouselManagement({ testDate: validTestDate })); - mockUpdateSlides.mockClear(); + const updatedSlides = mockUpdateSlides.mock.calls[0][0]; - rerender({ hasZeroBalance: true, testDate }); + expect(updatedSlides).toStrictEqual( + SLIDES_ZERO_FUNDS_REMOTE_ON_SWEEPSTAKES_ON, + ); + }); + }); - expect(mockUpdateSlides).toHaveBeenCalled(); + describe('positive funds, remote off, sweepstakes off', () => { + beforeEach(() => { + mockGetSelectedAccountCachedBalance.mockReturnValue('0x1'); + }); + + it('should have correct slide order', () => { + renderHook(() => useCarouselManagement({ testDate: invalidTestDate })); const updatedSlides = mockUpdateSlides.mock.calls[0][0]; - const fundSlide = updatedSlides.find( - (slide: CarouselSlide) => slide.id === FUND_SLIDE.id, + expect(updatedSlides).toStrictEqual( + SLIDES_POSITIVE_FUNDS_REMOTE_OFF_SWEEPSTAKES_OFF, ); - expect(fundSlide).toBeDefined(); - expect(fundSlide?.undismissable).toBe(true); }); + }); - it('should update slides when testDate changes', () => { - const initialTestDate = new Date( - SWEEPSTAKES_START.getTime() - 86400000, - ).toISOString(); + describe('positive funds, remote on, sweepstakes off', () => { + beforeEach(() => { + mockGetSelectedAccountCachedBalance.mockReturnValue('0x1'); + mockGetIsRemoteModeEnabled.mockReturnValue(true); + }); - const { rerender } = renderHook((props) => useCarouselManagement(props), { - initialProps: { hasZeroBalance: false, testDate: initialTestDate }, - }); + it('should have correct slide order', () => { + renderHook(() => useCarouselManagement({ testDate: invalidTestDate })); - const initialSlides = mockUpdateSlides.mock.calls[0][0]; + const updatedSlides = mockUpdateSlides.mock.calls[0][0]; - const initialHasSweepstakesSlide = initialSlides.some( - (slide: CarouselSlide) => slide.id === SWEEPSTAKES_SLIDE.id, + expect(updatedSlides).toStrictEqual( + SLIDES_POSITIVE_FUNDS_REMOTE_ON_SWEEPSTAKES_OFF, ); - expect(initialHasSweepstakesSlide).toBe(false); + }); + }); - mockUpdateSlides.mockClear(); + describe('positive funds, remote off, sweepstakes on', () => { + beforeEach(() => { + mockGetSelectedAccountCachedBalance.mockReturnValue('0x1'); + }); - const newTestDate = new Date( - SWEEPSTAKES_START.getTime() + 1000, - ).toISOString(); - rerender({ hasZeroBalance: false, testDate: newTestDate }); + it('should have correct slide order', () => { + renderHook(() => useCarouselManagement({ testDate: validTestDate })); - const newSlides = mockUpdateSlides.mock.calls[0][0]; + const updatedSlides = mockUpdateSlides.mock.calls[0][0]; - const newHasSweepstakesSlide = newSlides.some( - (slide: CarouselSlide) => slide.id === SWEEPSTAKES_SLIDE.id, + expect(updatedSlides).toStrictEqual( + SLIDES_POSITIVE_FUNDS_REMOTE_OFF_SWEEPSTAKES_ON, ); - expect(newHasSweepstakesSlide).toBe(true); }); }); - describe('return value', () => { - it('should return the current slides', () => { - const mockSlides: CarouselSlide[] = [ - { - id: 'test-slide', - title: 'Test Slide', - description: 'Test Description', - image: 'test-image', - }, - ]; - slides = mockSlides; - - const { result } = renderHook(() => - useCarouselManagement({ - hasZeroBalance: false, - }), - ); + describe('positive funds, remote on, sweepstakes on', () => { + beforeEach(() => { + mockGetSelectedAccountCachedBalance.mockReturnValue('0x1'); + mockGetIsRemoteModeEnabled.mockReturnValue(true); + }); - expect(result.current.slides).toBe(mockSlides); + it('should have correct slide order', () => { + renderHook(() => useCarouselManagement({ testDate: validTestDate })); + + const updatedSlides = mockUpdateSlides.mock.calls[0][0]; + + expect(updatedSlides).toStrictEqual( + SLIDES_POSITIVE_FUNDS_REMOTE_ON_SWEEPSTAKES_ON, + ); }); }); - describe('slide order', () => { - it('should maintain correct order when slides are updated multiple times', () => { - const testDate = new Date( - SWEEPSTAKES_START.getTime() + 1000, - ).toISOString(); + describe('state changes', () => { + it('should update slides when balance changes', () => { + mockGetSelectedAccountCachedBalance.mockReturnValue('0x1'); const { rerender } = renderHook((props) => useCarouselManagement(props), { - initialProps: { - hasZeroBalance: false, - testDate, - }, + initialProps: { testDate: invalidTestDate }, }); - let updatedSlides = mockUpdateSlides.mock.calls[0][0] as CarouselSlide[]; - expect(updatedSlides[0].id).toBe(SWEEPSTAKES_SLIDE.id); - expect(updatedSlides[1].id).toBe(FUND_SLIDE.id); - expect(updatedSlides[1].undismissable).toBe(false); + expect(mockUpdateSlides).toHaveBeenCalled(); + let updatedSlides = mockUpdateSlides.mock.calls[0][0]; + expect(updatedSlides).toStrictEqual( + SLIDES_POSITIVE_FUNDS_REMOTE_OFF_SWEEPSTAKES_OFF, + ); + mockGetSelectedAccountCachedBalance.mockReturnValue(ZERO_BALANCE); mockUpdateSlides.mockClear(); - mockDispatch.mockClear(); - rerender({ - hasZeroBalance: true, - testDate, - }); + rerender({ testDate: invalidTestDate }); expect(mockUpdateSlides).toHaveBeenCalled(); - updatedSlides = mockUpdateSlides.mock.calls[0][0] as CarouselSlide[]; - - expect(updatedSlides[0].id).toBe(SWEEPSTAKES_SLIDE.id); - expect(updatedSlides[1].id).toBe(FUND_SLIDE.id); - const fundSlide = updatedSlides.find( - (slide) => slide.id === FUND_SLIDE.id, - ); - expect(fundSlide?.undismissable).toBe(true); - }); - it('should handle empty slides array', () => { - mockUseSelector.mockImplementation(() => []); - const testDate = new Date( - SWEEPSTAKES_START.getTime() + 1000, - ).toISOString(); + updatedSlides = mockUpdateSlides.mock.calls[0][0]; - renderHook(() => - useCarouselManagement({ - hasZeroBalance: false, - testDate, - }), + expect(updatedSlides).toStrictEqual( + SLIDES_ZERO_FUNDS_REMOTE_OFF_SWEEPSTAKES_OFF, ); - - const updatedSlides = mockUpdateSlides.mock - .calls[0][0] as CarouselSlide[]; - expect(updatedSlides.length).toBeGreaterThan(0); - expect(updatedSlides[0].id).toBe(SWEEPSTAKES_SLIDE.id); }); - it('should maintain all required slide properties', () => { - const testDate = new Date( - SWEEPSTAKES_START.getTime() + 1000, - ).toISOString(); + it('should update slides when testDate changes', () => { + const { rerender } = renderHook((props) => useCarouselManagement(props), { + initialProps: { hasZeroBalance: false, testDate: invalidTestDate }, + }); - renderHook(() => - useCarouselManagement({ - hasZeroBalance: true, - testDate, - }), + expect(mockUpdateSlides).toHaveBeenCalled(); + let updatedSlides = mockUpdateSlides.mock.calls[0][0]; + + expect(updatedSlides).toStrictEqual( + SLIDES_ZERO_FUNDS_REMOTE_OFF_SWEEPSTAKES_OFF, ); - const updatedSlides = mockUpdateSlides.mock - .calls[0][0] as CarouselSlide[]; + mockUpdateSlides.mockClear(); - const sweepstakesSlide = updatedSlides.find( - (slide) => slide.id === SWEEPSTAKES_SLIDE.id, - ); - expect(sweepstakesSlide).toEqual({ - ...SWEEPSTAKES_SLIDE, - dismissed: false, - }); + rerender({ hasZeroBalance: false, testDate: validTestDate }); - const fundSlide = updatedSlides.find( - (slide) => slide.id === FUND_SLIDE.id, + expect(mockUpdateSlides).toHaveBeenCalled(); + updatedSlides = mockUpdateSlides.mock.calls[0][0]; + expect(updatedSlides).toStrictEqual( + SLIDES_ZERO_FUNDS_REMOTE_OFF_SWEEPSTAKES_ON, ); - expect(fundSlide).toEqual({ - ...FUND_SLIDE, - undismissable: true, - }); }); }); @@ -343,7 +364,6 @@ describe('useCarouselManagement', () => { renderHook(() => useCarouselManagement({ - hasZeroBalance: false, testDate, }), ); @@ -358,7 +378,6 @@ describe('useCarouselManagement', () => { renderHook(() => useCarouselManagement({ - hasZeroBalance: false, testDate, }), ); @@ -374,7 +393,6 @@ describe('useCarouselManagement', () => { expect(() => renderHook(() => useCarouselManagement({ - hasZeroBalance: false, testDate, }), ), diff --git a/ui/hooks/useCarouselManagement/useCarouselManagement.ts b/ui/hooks/useCarouselManagement/useCarouselManagement.ts index 6d946162e5ed..5eb3b2ed89ba 100644 --- a/ui/hooks/useCarouselManagement/useCarouselManagement.ts +++ b/ui/hooks/useCarouselManagement/useCarouselManagement.ts @@ -1,8 +1,9 @@ import { useEffect, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { removeSlide, updateSlides } from '../../store/actions'; -import { getSlides } from '../../selectors'; +import { updateSlides } from '../../store/actions'; +import { getSelectedAccountCachedBalance, getSlides } from '../../selectors'; import type { CarouselSlide } from '../../../shared/constants/app-state'; +import { getIsRemoteModeEnabled } from '../../selectors/remote-mode'; import { FUND_SLIDE, ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) @@ -10,13 +11,14 @@ import { ///: END:ONLY_INCLUDE_IF CARD_SLIDE, CASH_SLIDE, + REMOTE_MODE_SLIDE, SWEEPSTAKES_SLIDE, SWEEPSTAKES_START, SWEEPSTAKES_END, + ZERO_BALANCE, } from './constants'; type UseSlideManagementProps = { - hasZeroBalance: boolean; testDate?: string; // Only used in unit/e2e tests to simulate dates for sweepstakes campaign }; @@ -25,65 +27,62 @@ export function getSweepstakesCampaignActive(currentDate: Date) { } export const useCarouselManagement = ({ - hasZeroBalance, testDate, -}: UseSlideManagementProps) => { +}: UseSlideManagementProps = {}) => { const dispatch = useDispatch(); const slides = useSelector(getSlides); + const totalBalance = useSelector(getSelectedAccountCachedBalance); + const isRemoteModeEnabled = useSelector(getIsRemoteModeEnabled); - const buildSlideArray = useCallback( - (isSweepstakesActive: boolean) => { - const defaultSlides: CarouselSlide[] = []; - const fundSlide = { - ...FUND_SLIDE, - undismissable: hasZeroBalance, - }; + const hasZeroBalance = totalBalance === ZERO_BALANCE; - if (isSweepstakesActive) { - defaultSlides.push({ - ...SWEEPSTAKES_SLIDE, - dismissed: false, - }); - } + const checkSweepstakesActive = useCallback((currentDate: Date) => { + return getSweepstakesCampaignActive(currentDate); + }, []); - ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) - defaultSlides.push(BRIDGE_SLIDE); - ///: END:ONLY_INCLUDE_IF - defaultSlides.push(CARD_SLIDE); - defaultSlides.push(CASH_SLIDE); + useEffect(() => { + const defaultSlides: CarouselSlide[] = []; - const fundPosition = isSweepstakesActive ? 1 : 0; - defaultSlides.splice(fundPosition, 0, fundSlide); + const fundSlide = { + ...FUND_SLIDE, + undismissable: hasZeroBalance, + }; - return defaultSlides; - }, - [hasZeroBalance], - ); + ///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask) + defaultSlides.push(BRIDGE_SLIDE); + ///: END:ONLY_INCLUDE_IF + defaultSlides.push(CARD_SLIDE); + defaultSlides.push(CASH_SLIDE); - const checkSweepstakesActive = useCallback((currentDate: Date) => { - const isActive = getSweepstakesCampaignActive(currentDate); + defaultSlides.splice(hasZeroBalance ? 0 : 2, 0, fundSlide); - return isActive; - }, []); + // If enabled, insert remote mode slide at the beginning + if (isRemoteModeEnabled) { + defaultSlides.unshift(REMOTE_MODE_SLIDE); + } - useEffect(() => { + // If enabled, insert sweepstakes slide at the beginning const currentDate = testDate ? new Date(testDate) : new Date(new Date().toISOString()); + const isSweepstakesActive = checkSweepstakesActive(currentDate); - if (!isSweepstakesActive) { - const existingSweepstakes = slides.find( - (s: CarouselSlide) => s.id === SWEEPSTAKES_SLIDE.id, - ); - if (existingSweepstakes) { - dispatch(removeSlide(SWEEPSTAKES_SLIDE.id)); - } + if (isSweepstakesActive) { + defaultSlides.unshift({ + ...SWEEPSTAKES_SLIDE, + dismissed: false, + }); } - const newSlides = buildSlideArray(isSweepstakesActive); - dispatch(updateSlides(newSlides)); - }, [hasZeroBalance, testDate, buildSlideArray, checkSweepstakesActive]); + dispatch(updateSlides(defaultSlides)); + }, [ + checkSweepstakesActive, + dispatch, + hasZeroBalance, + isRemoteModeEnabled, + testDate, + ]); return { slides }; };