diff --git a/e2e/src/Discover.spec.js b/e2e/src/Discover.spec.js index 80a23494dfe..93094a491c0 100644 --- a/e2e/src/Discover.spec.js +++ b/e2e/src/Discover.spec.js @@ -1,4 +1,4 @@ -import { quickOnboarding, waitForElementByIdAndTap } from './utils/utils' +import { quickOnboarding, waitForElementByIdAndTap, scrollIntoView } from './utils/utils' import { launchApp } from './utils/retries' import DappListDisplay from './usecases/DappListDisplay' @@ -12,6 +12,11 @@ describe('Discover tab', () => { permissions: { notifications: 'YES', contacts: 'YES', camera: 'YES' }, }) await waitForElementByIdAndTap('Tab/Discover') + + await scrollIntoView('Explore All', 'DappsExplorerScrollView') + await element(by.text('Explore All')).tap() + + await waitFor(element(by.id(`DappsScreen/DappsList`))) }) describe('Dapp List Display', DappListDisplay) diff --git a/e2e/src/usecases/DappListDisplay.js b/e2e/src/usecases/DappListDisplay.js index ca78461d81b..cc8b618e509 100644 --- a/e2e/src/usecases/DappListDisplay.js +++ b/e2e/src/usecases/DappListDisplay.js @@ -17,8 +17,9 @@ export default DappListDisplay = () => { }) it('should open internal webview with correct dapp when dapp opened', async () => { - await scrollIntoView(dappToTest.dapp.name, 'DAppsExplorerScreen/DappsList') + await scrollIntoView(dappToTest.dapp.name, 'DappsScreen/DappsList') await element(by.text(dappToTest.dapp.name)).tap() + await waitFor(element(by.id(`WebViewScreen/${dappToTest.dapp.name}`))) .toBeVisible() .withTimeout(10 * 1000) @@ -32,7 +33,7 @@ export default DappListDisplay = () => { it(':ios: should correctly filter dapp list based on user agent', async () => { const iOSDappList = await fetchDappList('Valora/1.0.0 (iOS 15.0; iPhone)') - const dappCards = await getElementTextList('DAppsExplorerScreen/AllSection/DappCard') + const dappCards = await getElementTextList('DappsScreen/AllSection/DappCard') jestExpect(dappCards.length).toEqual(iOSDappList.applications.length) }) } diff --git a/locales/base/translation.json b/locales/base/translation.json index 07ccedf856c..76588fb359b 100644 --- a/locales/base/translation.json +++ b/locales/base/translation.json @@ -1440,11 +1440,14 @@ "dappsScreen": { "title": "Dapps", "titleDiscover": "Discover", + "exploreDapps": "Explore Dapps", + "exploreAll": "Explore All", "message": "Explore ways to use your assets", "errorMessage": "There was an error loading dapps", "featuredDapp": "Featured", "allDapps": "All", "favoriteDapps": "My Favorites", + "mostPopularDapps": "Most Popular", "favoriteDappsAndAll": "My Favorites & All", "searchPlaceHolder": "Search dapps", "noFavorites": { diff --git a/src/analytics/Events.tsx b/src/analytics/Events.tsx index e1ce5e963b9..d8df38fb00b 100644 --- a/src/analytics/Events.tsx +++ b/src/analytics/Events.tsx @@ -543,6 +543,7 @@ export enum DappExplorerEvents { dapp_unfavorite = 'dapp_unfavorite', dapp_filter = 'dapp_filter', dapp_rankings_open = 'dapp_rankings_open', + dapp_explore_all = 'dapp_explore_all', } export enum WebViewEvents { diff --git a/src/analytics/Properties.tsx b/src/analytics/Properties.tsx index ea05617fa5c..f099b78febe 100644 --- a/src/analytics/Properties.tsx +++ b/src/analytics/Properties.tsx @@ -5,6 +5,7 @@ import { KycStatus as FiatConnectKycStatus, } from '@fiatconnect/fiatconnect-types' import { ShareAction } from 'react-native' +import { Screens } from 'src/navigator/Screens' import { PermissionStatus } from 'react-native-permissions' import { AppEvents, @@ -1129,6 +1130,7 @@ interface DappExplorerEventsProperties { activeFilter?: string activeSearchTerm?: string position?: number + fromScreen?: Screens } [DappExplorerEvents.dapp_close]: DappEventProperties [DappExplorerEvents.dapp_screen_open]: undefined @@ -1140,6 +1142,7 @@ interface DappExplorerEventsProperties { remove: boolean } [DappExplorerEvents.dapp_rankings_open]: undefined + [DappExplorerEvents.dapp_explore_all]: undefined } interface WebViewEventsProperties { diff --git a/src/analytics/docs.ts b/src/analytics/docs.ts index f66107d86ec..2b8edde1336 100644 --- a/src/analytics/docs.ts +++ b/src/analytics/docs.ts @@ -535,6 +535,7 @@ export const eventDocs: Record = { [DappExplorerEvents.dapp_unfavorite]: `when user unfavorites a dapp`, [DappExplorerEvents.dapp_filter]: `when a user taps on a filter`, [DappExplorerEvents.dapp_rankings_open]: `when a user taps on the dapp rankings card`, + [DappExplorerEvents.dapp_explore_all]: `when a user taps on the explore all button`, [WebViewEvents.webview_more_options]: `when user taps "triple dot icon" from the webview`, [WebViewEvents.webview_open_in_browser]: `when user taps "Open in External Browser" from the webview options`, [CoinbasePayEvents.coinbase_pay_flow_start]: `When user is navigated to Coinbase Pay experience`, diff --git a/src/dapps/DappsScreen.test.tsx b/src/dapps/DappsScreen.test.tsx new file mode 100644 index 00000000000..e56e7c90ae0 --- /dev/null +++ b/src/dapps/DappsScreen.test.tsx @@ -0,0 +1,718 @@ +import { fireEvent, render, within } from '@testing-library/react-native' +import * as React from 'react' +import { Provider } from 'react-redux' +import { DappExplorerEvents } from 'src/analytics/Events' +import ValoraAnalytics from 'src/analytics/ValoraAnalytics' +import { dappSelected, favoriteDapp, fetchDappsList, unfavoriteDapp } from 'src/dapps/slice' +import { DappCategory, DappSection } from 'src/dapps/types' +import DappsScreen from 'src/dapps/DappsScreen' +import { getFeatureGate } from 'src/statsig' +import MockedNavigator from 'test/MockedNavigator' +import { createMockStore } from 'test/utils' +import { mockDappListWithCategoryNames } from 'test/values' + +jest.mock('src/analytics/ValoraAnalytics') +jest.mock('src/statsig', () => ({ + getExperimentParams: jest.fn(() => ({ + dappsFilterEnabled: true, + dappsSearchEnabled: true, + })), + getFeatureGate: jest.fn(), +})) + +const dappsList = mockDappListWithCategoryNames + +const dappsCategories: DappCategory[] = [ + { + id: '1', + name: 'Swap', + backgroundColor: '#DEF8EA', + fontColor: '#1AB775', + }, + { + id: '2', + name: 'Lend, Borrow & Earn', + backgroundColor: '#DEF8F7', + fontColor: '#07A0AE', + }, +] + +const defaultStore = createMockStore({ + dapps: { dappListApiUrl: 'http://url.com', dappsList, dappsCategories }, +}) + +describe('DappsScreen', () => { + beforeEach(() => { + defaultStore.clearActions() + jest.clearAllMocks() + jest.mocked(getFeatureGate).mockReturnValue(false) + }) + + it('renders correctly and fires the correct actions on press dapp', () => { + const { getByText, queryByText } = render( + + + + ) + + expect(defaultStore.getActions()).toEqual([fetchDappsList()]) + expect(queryByText('featuredDapp')).toBeFalsy() + expect(getByText('Dapp 1')).toBeTruthy() + expect(getByText('Dapp 2')).toBeTruthy() + + fireEvent.press(getByText('Dapp 1')) + + expect(defaultStore.getActions()).toEqual([ + fetchDappsList(), + dappSelected({ dapp: { ...dappsList[0], openedFrom: DappSection.All } }), + ]) + }) + + it('renders correctly and fires the correct actions on press deep linked dapp', () => { + const { getByText, queryByText } = render( + + + + ) + + expect(defaultStore.getActions()).toEqual([fetchDappsList()]) + expect(getByText('Dapp 1')).toBeTruthy() + expect(getByText('Dapp 2')).toBeTruthy() + expect(queryByText('featuredDapp')).toBeFalsy() + + fireEvent.press(getByText('Dapp 2')) + + expect(defaultStore.getActions()).toEqual([ + fetchDappsList(), + dappSelected({ dapp: { ...dappsList[1], openedFrom: DappSection.All } }), + ]) + }) + + it('pens dapps directly', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + }, + }) + const { getByText } = render( + + + + ) + + expect(getByText('dappsDisclaimerAllDapps')).toBeTruthy() + + fireEvent.press(getByText('Dapp 1')) + + expect(store.getActions()).toEqual([ + fetchDappsList(), + dappSelected({ dapp: { ...dappsList[0], openedFrom: DappSection.All } }), + ]) + }) + + it('renders error message when fetching dapps fails', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList: [], + dappsCategories: [], + dappsListError: 'Error fetching dapps', + }, + }) + const { getByText, getByTestId } = render( + + + + ) + + expect(getByText('dappsScreen.errorMessage')).toBeTruthy() + // asserts whether categories.length (0) isn't rendered, which causes a + // crash in the app + expect(getByTestId('DappsScreen')).not.toHaveTextContent('0') + }) + + describe('favorite dapps', () => { + it('renders correctly when there are no favorite dapps', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: [], + }, + }) + const { getByText, queryByText } = render( + + + + ) + + expect(queryByText('dappsScreen.noFavorites.title')).toBeNull() + expect(queryByText('dappsScreen.noFavorites.description')).toBeNull() + expect(getByText('Dapp 1')).toBeTruthy() + expect(getByText('Dapp 2')).toBeTruthy() + }) + + it('renders correctly when there are favourited dapps', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp2'], + }, + }) + const { queryByText, getAllByTestId } = render( + + + + ) + + const favoritesSectionResults = getAllByTestId('DappsScreen/FavoritesSection/DappCard') + expect(favoritesSectionResults.length).toBe(1) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[1].name) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[1].description) + expect(queryByText('dappsScreen.favoritedDappToast.message')).toBeFalsy() + }) + + it('triggers the events when favoriting', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp1'], + }, + }) + const { getByTestId, getByText } = render( + + + + ) + + // don't include events dispatched on screen load + jest.clearAllMocks() + + const allDappsSection = getByTestId('DappsScreen/DappsList') + fireEvent.press(within(allDappsSection).getByTestId('Dapp/Favorite/dapp2')) + + // favorited dapp confirmation toast + expect(getByText('dappsScreen.favoritedDappToast.message')).toBeTruthy() + expect(getByText('dappsScreen.favoritedDappToast.labelCTA')).toBeTruthy() + + expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) + expect(ValoraAnalytics.track).toHaveBeenCalledWith('dapp_favorite', { + categories: ['2'], + dappId: 'dapp2', + dappName: 'Dapp 2', + section: 'all', + }) + expect(store.getActions()).toEqual([fetchDappsList(), favoriteDapp({ dappId: 'dapp2' })]) + }) + + it('triggers the events when unfavoriting', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp2'], + }, + }) + const { getByTestId, getAllByTestId, queryByText } = render( + + + + ) + + // don't include events dispatched on screen load + jest.clearAllMocks() + + const selectedDappCards = getAllByTestId('Dapp/dapp2') + // should only appear once, in the favorites section + expect(selectedDappCards).toHaveLength(1) + + fireEvent.press(getByTestId('Dapp/Favorite/dapp2')) + + expect(queryByText('dappsScreen.favoritedDappToast.message')).toBeFalsy() + + expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) + expect(ValoraAnalytics.track).toHaveBeenCalledWith('dapp_unfavorite', { + categories: ['2'], + dappId: 'dapp2', + dappName: 'Dapp 2', + section: 'all', + }) + expect(store.getActions()).toEqual([fetchDappsList(), unfavoriteDapp({ dappId: 'dapp2' })]) + }) + }) + + describe('searching dapps', () => { + it('renders correctly when there are no search results', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: [], + }, + }) + + const { getByTestId, queryByTestId } = render( + + + + ) + + fireEvent.changeText(getByTestId('SearchInput'), 'iDoNotExist') + + // Should the no results within the all section + expect(queryByTestId('DappsScreen/FavoritesSection/NoResults')).toBeNull() + expect(getByTestId('DappsScreen/AllSection/NoResults')).toBeTruthy() + }) + + it('renders correctly when there are search results in both sections', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp2'], + }, + }) + + const { getByTestId, queryByTestId, getAllByTestId } = render( + + + + ) + + fireEvent.changeText(getByTestId('SearchInput'), 'dapp') + + // Should display the correct sections + const favoritesSectionResults = getAllByTestId('DappsScreen/FavoritesSection/DappCard') + expect(favoritesSectionResults.length).toBe(1) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[1].name) + + const allSectionResults = getAllByTestId('DappsScreen/AllSection/DappCard') + expect(allSectionResults.length).toBe(2) + expect(allSectionResults[0]).toHaveTextContent(dappsList[0].name) + expect(allSectionResults[1]).toHaveTextContent(dappsList[2].name) + + // No results sections should not be displayed + expect(queryByTestId('DappsScreen/FavoritesSection/NoResults')).toBeNull() + expect(queryByTestId('DappsScreen/AllSection/NoResults')).toBeNull() + }) + + it('clearing search input should show all dapps', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp2'], + }, + }) + + const { getByTestId, getAllByTestId } = render( + + + + ) + + // Type in search that should have no results + fireEvent.press(getByTestId('SearchInput')) + fireEvent.changeText(getByTestId('SearchInput'), 'iDoNotExist') + + // Clear search field - onPress is tested in src/components/CircleButton.test.tsx + fireEvent.changeText(getByTestId('SearchInput'), '') + + // Dapps displayed in the correct sections + const favoritesSectionResults = getAllByTestId('DappsScreen/FavoritesSection/DappCard') + expect(favoritesSectionResults.length).toBe(1) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[1].name) + + const allSectionResults = getAllByTestId('DappsScreen/AllSection/DappCard') + expect(allSectionResults.length).toBe(2) + expect(allSectionResults[0]).toHaveTextContent(dappsList[0].name) + expect(allSectionResults[1]).toHaveTextContent(dappsList[2].name) + }) + }) + + describe('filter dapps', () => { + it('renders correctly when there are no filters applied', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp1'], + }, + }) + const { getByText, queryByText, getAllByTestId } = render( + + + + ) + + // All Filter Chips is not displayed + expect(queryByText('dappsScreen.allDapps')).toBeFalsy() + // Category Filter Chips displayed + expect(getByText(dappsCategories[0].name)).toBeTruthy() + expect(getByText(dappsCategories[1].name)).toBeTruthy() + + const favoritesSectionResults = getAllByTestId('DappsScreen/FavoritesSection/DappCard') + expect(favoritesSectionResults.length).toBe(1) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].name) + + const allSectionResults = getAllByTestId('DappsScreen/AllSection/DappCard') + expect(allSectionResults.length).toBe(2) + expect(allSectionResults[0]).toHaveTextContent(dappsList[1].name) + expect(allSectionResults[1]).toHaveTextContent(dappsList[2].name) + }) + + it('renders correctly when there are filters applied', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp1'], + }, + }) + const { getByTestId, getByText, queryByTestId, getAllByTestId } = render( + + + + ) + + // Tap on category 2 filter + fireEvent.press(getByText(dappsCategories[1].name)) + + // Favorite Section should not show results + expect(queryByTestId('DappsScreen/FavoritesSection/DappCard')).toBeNull() + + // All Section should show only 'dapp 2' + const allDappsSection = getByTestId('DappsScreen/DappsList') + // queryByText returns null if not found + expect(within(allDappsSection).queryByText(dappsList[0].name)).toBeFalsy() + expect(within(allDappsSection).queryByText(dappsList[0].description)).toBeFalsy() + expect(within(allDappsSection).getByText(dappsList[1].name)).toBeTruthy() + expect(within(allDappsSection).getByText(dappsList[1].description)).toBeTruthy() + + const allSectionResults = getAllByTestId('DappsScreen/AllSection/DappCard') + expect(allSectionResults.length).toBe(1) + expect(allSectionResults[0]).toHaveTextContent(dappsList[1].name) + }) + + it('triggers event when filtering', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp1'], + }, + }) + const { getByText } = render( + + + + ) + + // don't include events dispatched on screen load + jest.clearAllMocks() + + // Tap on category 2 filter + fireEvent.press(getByText(dappsCategories[1].name)) + + expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) + expect(ValoraAnalytics.track).toHaveBeenCalledWith(DappExplorerEvents.dapp_filter, { + filterId: '2', + remove: false, + }) + }) + + it('triggers events when toggling a category filter', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp1'], + }, + }) + const { getByText } = render( + + + + ) + + // don't include events dispatched on screen load + jest.clearAllMocks() + + // Tap on category 2 filter + fireEvent.press(getByText(dappsCategories[1].name)) + + // Tap on category 2 filter again to remove it + fireEvent.press(getByText(dappsCategories[1].name)) + + expect(ValoraAnalytics.track).toHaveBeenCalledTimes(2) + expect(ValoraAnalytics.track).toHaveBeenNthCalledWith(1, DappExplorerEvents.dapp_filter, { + filterId: '2', + remove: false, + }) + expect(ValoraAnalytics.track).toHaveBeenNthCalledWith(2, DappExplorerEvents.dapp_filter, { + filterId: '2', + remove: true, + }) + }) + + it('triggers event when clearing filters from category section', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp2'], + }, + }) + const { getByTestId, getByText } = render( + + + + ) + + // don't include events dispatched on screen load + jest.clearAllMocks() + + // Tap on category 2 filter + fireEvent.press(getByText(dappsCategories[1].name)) + + // Tap on remove filters from all section + fireEvent.press(getByTestId('DappsScreen/AllSection/NoResults/RemoveFilter')) + + // Assert correct analytics are fired + expect(ValoraAnalytics.track).toHaveBeenCalledTimes(2) + expect(ValoraAnalytics.track).toHaveBeenCalledWith(DappExplorerEvents.dapp_filter, { + filterId: '2', + remove: false, + }) + expect(ValoraAnalytics.track).toHaveBeenCalledWith(DappExplorerEvents.dapp_filter, { + filterId: '2', + remove: true, + }) + }) + + it('triggers event when clearing filters from favorite section', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp1'], + }, + }) + const { getByText, queryByTestId } = render( + + + + ) + + // don't include events dispatched on screen load + jest.clearAllMocks() + + // Tap on category 2 filter + fireEvent.press(getByText(dappsCategories[1].name)) + + // Should not render favorites section + expect(queryByTestId('DappsScreen/FavoritesSection/Title')).toBeNull() + expect(queryByTestId('DappsScreen/FavoritesSection/DappCard')).toBeNull() + + // Assert correct analytics are fired + expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) + expect(ValoraAnalytics.track).toHaveBeenCalledWith(DappExplorerEvents.dapp_filter, { + filterId: '2', + remove: false, + }) + }) + }) + + describe('searching and filtering', () => { + it('renders correctly when there are results in all and favorites sections', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp1'], + }, + }) + const { getByTestId, getAllByTestId } = render( + + + + ) + + // Search for 'tokens' + fireEvent.changeText(getByTestId('SearchInput'), 'tokens') + + // Favorites section displays correctly + const favoritesSectionResults = getAllByTestId('DappsScreen/FavoritesSection/DappCard') + expect(favoritesSectionResults.length).toBe(1) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].name) + + // All section displays correctly + const allSectionResults = getAllByTestId('DappsScreen/AllSection/DappCard') + expect(allSectionResults.length).toBe(1) + expect(allSectionResults[0]).toHaveTextContent(dappsList[1].name) + }) + + it('renders correctly when there are results in favorites and no results in all', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp1'], + }, + }) + const { getByTestId, getByText, getAllByTestId, queryByTestId } = render( + + + + ) + + // Searching for tokens should return both dapps + fireEvent.changeText(getByTestId('SearchInput'), 'tokens') + + // Filtering by category 1 should return only dapp 1 + fireEvent.press(getByText(dappsCategories[0].name)) + + // Favorite Section should show only 'dapp 1' + const favoritesSectionResults = getAllByTestId('DappsScreen/FavoritesSection/DappCard') + expect(favoritesSectionResults.length).toBe(1) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].name) + + // All Section should show no results + expect(queryByTestId('DappsScreen/AllSection/DappCard')).toBeFalsy() + expect(getByTestId('DappsScreen/AllSection/NoResults')).toBeTruthy() + }) + + it('renders correctly when there are no results in favorites and results in all', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: ['dapp2'], + }, + }) + const { getByTestId, getByText, queryByTestId } = render( + + + + ) + + // Searching for tokens should return both dapps + fireEvent.changeText(getByTestId('SearchInput'), 'tokens') + + // Filtering by category 1 should return only dapp 1 + fireEvent.press(getByText(dappsCategories[0].name)) + + // Favorite Section should not show + expect(queryByTestId('FavoriteDappsSection')).toBeNull() + + // All Section should show only 'dapp 1' + const allDappsSection = getByTestId('DappsScreen/DappsList') + expect(within(allDappsSection).getByText(dappsList[0].name)).toBeTruthy() + expect(within(allDappsSection).getByText(dappsList[0].description)).toBeTruthy() + expect(within(allDappsSection).queryByText(dappsList[1].name)).toBeFalsy() + expect(within(allDappsSection).queryByText(dappsList[1].description)).toBeFalsy() + }) + }) + + describe('dapp open analytics event properties', () => { + const defaultStoreProps = { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + favoriteDappIds: [], + } + const defaultExpectedDappOpenProps = { + activeFilter: 'all', + activeSearchTerm: '', + categories: ['1'], + dappId: 'dapp3', + dappName: 'Dapp 3', + position: 3, + section: 'all', + fromScreen: 'DappsScreen', + } + + it('should dispatch with no filter or search, from the normal list with no confirmation bottom sheet', () => { + const store = createMockStore({ + dapps: defaultStoreProps, + }) + const { getByText } = render( + + + + ) + + fireEvent.press(getByText('Dapp 3')) + + expect(ValoraAnalytics.track).toHaveBeenLastCalledWith( + DappExplorerEvents.dapp_open, + defaultExpectedDappOpenProps + ) + }) + + it('should dispatch with no filter or search, with favourites', () => { + const store = createMockStore({ + dapps: { + ...defaultStoreProps, + favoriteDappIds: ['dapp1'], + }, + }) + const { getByText } = render( + + + + ) + + fireEvent.press(getByText('Dapp 3')) + + expect(ValoraAnalytics.track).toHaveBeenLastCalledWith(DappExplorerEvents.dapp_open, { + ...defaultExpectedDappOpenProps, + position: 2, // note the position explicitly does not take into account the number of favorites + }) + }) + + it('should dispatch with filter and search', () => { + const store = createMockStore({ + dapps: defaultStoreProps, + }) + const { getByText, getByTestId, getAllByTestId } = render( + + + + ) + + fireEvent.changeText(getByTestId('SearchInput'), 'cool') + fireEvent.press(getByText(dappsCategories[0].name)) + + expect(getAllByTestId('DappsScreen/AllSection/DappCard')).toHaveLength(1) + fireEvent.press(getByText('Dapp 3')) + + expect(ValoraAnalytics.track).toHaveBeenLastCalledWith(DappExplorerEvents.dapp_open, { + ...defaultExpectedDappOpenProps, + activeFilter: '1', + activeSearchTerm: 'cool', + position: 1, + }) + }) + }) +}) diff --git a/src/dapps/DappsScreen.tsx b/src/dapps/DappsScreen.tsx new file mode 100644 index 00000000000..5122f14acec --- /dev/null +++ b/src/dapps/DappsScreen.tsx @@ -0,0 +1,367 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + LayoutChangeEvent, + RefreshControl, + SectionList, + SectionListProps, + StyleSheet, + Text, + View, +} from 'react-native' +import { ScrollView } from 'react-native-gesture-handler' +import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' +import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' +import { DappExplorerEvents } from 'src/analytics/Events' +import ValoraAnalytics from 'src/analytics/ValoraAnalytics' +import { BottomSheetRefType } from 'src/components/BottomSheet' +import FilterChipsCarousel, { BooleanFilterChip } from 'src/components/FilterChipsCarousel' +import SearchInput from 'src/components/SearchInput' +import { + dappsCategoriesAlphabeticalSelector, + dappsListErrorSelector, + dappsListLoadingSelector, + favoriteDappsWithCategoryNamesSelector, + nonFavoriteDappsWithCategoryNamesSelector, +} from 'src/dapps/selectors' +import { fetchDappsList } from 'src/dapps/slice' +import { ActiveDapp, Dapp, DappSection, DappWithCategoryNames } from 'src/dapps/types' +import DappCard from 'src/dappsExplorer/DappCard' +import { DappFeaturedActions } from 'src/dappsExplorer/DappFeaturedActions' +import { DappRankingsBottomSheet } from 'src/dappsExplorer/DappRankingsBottomSheet' +import NoResults from 'src/dappsExplorer/NoResults' +import { searchDappList } from 'src/dappsExplorer/searchDappList' +import useDappFavoritedToast from 'src/dappsExplorer/useDappFavoritedToast' +import useOpenDapp from 'src/dappsExplorer/useOpenDapp' +import { currentLanguageSelector } from 'src/i18n/selectors' +import { Screens } from 'src/navigator/Screens' +import useScrollAwareHeader from 'src/navigator/ScrollAwareHeader' +import { StackParamList } from 'src/navigator/types' +import { useDispatch, useSelector } from 'src/redux/hooks' +import { Colors } from 'src/styles/colors' +import fontStyles, { typeScale } from 'src/styles/fonts' +import { Spacing } from 'src/styles/styles' + +const AnimatedSectionList = + Animated.createAnimatedComponent>(SectionList) +interface SectionData { + data: DappWithCategoryNames[] + sectionName: string + dappSection: DappSection + testID: string +} + +type Props = NativeStackScreenProps + +function DappsScreen({ navigation }: Props) { + const { t } = useTranslation() + + const insets = useSafeAreaInsets() + + const sectionListRef = useRef(null) + + const horizontalScrollView = useRef(null) + const dappRankingsBottomSheetRef = useRef(null) + + const dispatch = useDispatch() + const loading = useSelector(dappsListLoadingSelector) + const error = useSelector(dappsListErrorSelector) + const categories = useSelector(dappsCategoriesAlphabeticalSelector) + const language = useSelector(currentLanguageSelector) + const nonFavoriteDappsWithCategoryNames = useSelector(nonFavoriteDappsWithCategoryNamesSelector) + const favoriteDappsWithCategoryNames = useSelector(favoriteDappsWithCategoryNamesSelector) + + const [filterChips, setFilterChips] = useState[]>(() => + categories.map((category) => ({ + id: category.id, + name: category.name, + filterFn: (dapp: DappWithCategoryNames) => dapp.categories.includes(category.id), + isSelected: false, + })) + ) + const selectedFilter = useMemo( + () => filterChips.find((filter) => filter.isSelected), + [filterChips] + ) + + const [searchTerm, setSearchTerm] = useState('') + + const { onSelectDapp } = useOpenDapp() + const { onFavoriteDapp, DappFavoritedToast } = useDappFavoritedToast(sectionListRef) + + const removeFilter = (filter: BooleanFilterChip) => { + ValoraAnalytics.track(DappExplorerEvents.dapp_filter, { + filterId: filter.id, + remove: true, + }) + setFilterChips((prev) => prev.map((filter) => ({ ...filter, isSelected: false }))) + horizontalScrollView.current?.scrollTo({ x: 0, animated: true }) + } + + const handleToggleFilterChip = (filter: BooleanFilterChip) => { + ValoraAnalytics.track(DappExplorerEvents.dapp_filter, { + filterId: filter.id, + remove: selectedFilter?.id === filter.id, + }) + + setFilterChips((prev) => + prev.map((prevFilter) => { + return { + ...prevFilter, + isSelected: prevFilter.id === filter.id ? !prevFilter.isSelected : false, + } + }) + ) + } + + const handleShowDappRankings = () => { + ValoraAnalytics.track(DappExplorerEvents.dapp_rankings_open) + dappRankingsBottomSheetRef.current?.snapToIndex(0) + } + + useEffect(() => { + dispatch(fetchDappsList()) + ValoraAnalytics.track(DappExplorerEvents.dapp_screen_open) + }, []) + + const onPressDapp = (dapp: ActiveDapp, index: number) => { + onSelectDapp(dapp, { + position: 1 + index, + activeFilter: selectedFilter?.id ?? 'all', + activeSearchTerm: searchTerm, + fromScreen: Screens.DappsScreen, + }) + } + + // Scroll Aware Header + const scrollPosition = useSharedValue(0) + const [titleHeight, setTitleHeight] = useState(0) + + const handleMeasureTitleHeight = (event: LayoutChangeEvent) => { + setTitleHeight(event.nativeEvent.layout.height) + } + + const handleScroll = useAnimatedScrollHandler((event) => { + scrollPosition.value = event.contentOffset.y + }) + + useScrollAwareHeader({ + navigation, + title: t('dappsScreen.exploreDapps'), + scrollPosition, + startFadeInPosition: titleHeight - titleHeight * 0.33, + animationDistance: titleHeight * 0.33, + }) + + const sections: SectionData[] = useMemo(() => { + const dappsMatchingFilter = selectedFilter + ? nonFavoriteDappsWithCategoryNames.filter((dapp) => selectedFilter.filterFn(dapp)) + : nonFavoriteDappsWithCategoryNames + const dappsMatchingFilterAndSearch = searchTerm + ? searchDappList(dappsMatchingFilter, searchTerm) + : dappsMatchingFilter + + const favouriteDappsMatchingFilter = selectedFilter + ? favoriteDappsWithCategoryNames.filter((dapp) => selectedFilter.filterFn(dapp)) + : favoriteDappsWithCategoryNames + const favouriteDappsMatchingFilterAndSearch = searchTerm + ? searchDappList(favouriteDappsMatchingFilter, searchTerm) + : favouriteDappsMatchingFilter + + const noMatchingResults = + dappsMatchingFilterAndSearch.length === 0 && + favouriteDappsMatchingFilterAndSearch.length === 0 + + return [ + ...(favouriteDappsMatchingFilterAndSearch.length > 0 + ? [ + { + data: favouriteDappsMatchingFilterAndSearch, + sectionName: t('dappsScreen.favoriteDapps').toLocaleUpperCase(language ?? 'en-US'), + dappSection: DappSection.FavoritesDappScreen, + testID: 'DappsScreen/FavoritesSection', + }, + ] + : []), + { + data: dappsMatchingFilterAndSearch, + sectionName: (noMatchingResults + ? t('dappsScreen.favoriteDappsAndAll') + : t('dappsScreen.allDapps') + ).toLocaleUpperCase(language ?? 'en-US'), + dappSection: DappSection.All, + testID: 'DappsScreen/AllSection', + }, + ] + }, [ + nonFavoriteDappsWithCategoryNames, + favoriteDappsWithCategoryNames, + searchTerm, + selectedFilter, + ]) + + return ( + + <> + {!loading && !!error && ( + + {t('dappsScreen.errorMessage')} + + )} + {!!categories.length && ( + dispatch(fetchDappsList())} + /> + } + // TODO: resolve type error + // @ts-expect-error + ref={sectionListRef} + ListFooterComponent={ + {t('dappsDisclaimerAllDapps')} + } + ListHeaderComponent={ + <> + { + + {t('dappsScreen.exploreDapps')} + + } + + { + setSearchTerm(text) + }} + value={searchTerm} + multiline={false} + placeholderTextColor={Colors.gray4} + underlineColorAndroid="transparent" + placeholder={t('dappsScreen.searchPlaceHolder') ?? undefined} + showClearButton={true} + allowFontScaling={false} + /> + + + } + style={styles.sectionList} + contentContainerStyle={[ + styles.sectionListContentContainer, + { paddingBottom: Math.max(insets.bottom, Spacing.Regular16) }, + ]} + // Workaround iOS setting an incorrect automatic inset at the top + scrollIndicatorInsets={{ top: 0.01 }} + scrollEventThrottle={16} + onScroll={handleScroll} + sections={sections} + renderItem={({ item: dapp, index, section }) => { + return ( + + onPressDapp({ ...dapp, openedFrom: section.dappSection }, index) + } + onFavoriteDapp={onFavoriteDapp} + showBorder={true} + testID={`${section.testID}/DappCard`} + /> + ) + }} + renderSectionFooter={({ section }) => { + if (section.data.length === 0) { + return ( + + ) + } + + return null + }} + renderSectionHeader={({ section: { sectionName, testID } }) => { + return ( + + {sectionName} + + ) + }} + keyExtractor={(dapp) => dapp.id} + stickySectionHeadersEnabled={false} + testID="DappsScreen/DappsList" + ListFooterComponentStyle={styles.listFooterComponent} + keyboardShouldPersistTaps="always" + keyboardDismissMode="on-drag" + /> + )} + + {DappFavoritedToast} + + + ) +} + +const styles = StyleSheet.create({ + safeAreaContainer: { + flex: 1, + }, + centerContainer: { + alignItems: 'center', + justifyContent: 'center', + flex: 1, + }, + dappFilterView: { + paddingTop: Spacing.Regular16, + }, + sectionListContentContainer: { + padding: Spacing.Thick24, + paddingTop: 0, + flexGrow: 1, + }, + refreshControl: { + backgroundColor: Colors.white, + }, + sectionList: { + flex: 1, + }, + sectionTitle: { + ...fontStyles.label, + color: Colors.gray4, + marginTop: Spacing.Large32, + }, + disclaimer: { + ...fontStyles.xsmall, + color: Colors.gray4, + textAlign: 'center', + marginTop: Spacing.Large32, + marginBottom: Spacing.Regular16, + }, + listFooterComponent: { + flex: 1, + justifyContent: 'flex-end', + }, + title: { + ...typeScale.titleMedium, + color: Colors.black, + marginBottom: Spacing.Thick24, + }, +}) + +export default DappsScreen diff --git a/src/dapps/selectors.ts b/src/dapps/selectors.ts index 5f036a5e789..3dac0e4a3ef 100644 --- a/src/dapps/selectors.ts +++ b/src/dapps/selectors.ts @@ -83,7 +83,7 @@ export const mostPopularDappsSelector = createSelector( } ) -const favoriteDappsSelector = createSelector( +export const favoriteDappsSelector = createSelector( dappsListSelector, favoriteDappIdsSelector, (dapps, favoriteDappIds) => dapps.filter((dapp) => favoriteDappIds.includes(dapp.id)) diff --git a/src/dappsExplorer/DappCard.tsx b/src/dappsExplorer/DappCard.tsx index f8345233893..12ba58a8b7d 100644 --- a/src/dappsExplorer/DappCard.tsx +++ b/src/dappsExplorer/DappCard.tsx @@ -13,19 +13,23 @@ import { useDispatch, useSelector } from 'src/redux/hooks' import { Colors } from 'src/styles/colors' import fontStyles from 'src/styles/fonts' import { vibrateSuccess } from 'src/styles/hapticFeedback' -import { Shadow, Spacing } from 'src/styles/styles' +import { Spacing } from 'src/styles/styles' interface DappCardContentProps { dapp: Dapp onFavoriteDapp?: (dapp: Dapp) => void favoritedFromSection: DappSection + disableFavoriting?: boolean + showBorder?: boolean } interface Props { onPressDapp: () => void dapp: Dapp testID: string + disableFavoriting?: boolean onFavoriteDapp?: (dapp: Dapp) => void + showBorder?: boolean } // Since this icon exists within a touchable, make the hitslop bigger than usual @@ -35,6 +39,8 @@ export function DappCardContent({ dapp, onFavoriteDapp, favoritedFromSection, + disableFavoriting, + showBorder, }: DappCardContentProps) { const dispatch = useDispatch() const favoriteDappIds = useSelector(favoriteDappIdsSelector) @@ -60,33 +66,54 @@ export function DappCardContent({ vibrateSuccess() } - return ( - + {dapp.name} {dapp.description} - {isFavorited ? : } + {isFavorited ? : !disableFavoriting ? : <>} ) } -function DappCard({ dapp, onPressDapp, onFavoriteDapp, testID }: Props) { +function DappCard({ + dapp, + onPressDapp, + onFavoriteDapp, + disableFavoriting, + showBorder, + testID, +}: Props) { return ( - + @@ -98,6 +125,10 @@ const styles = StyleSheet.create({ flex: 1, marginRight: Spacing.Regular16, }, + borderStyle: { + borderWidth: 1, + borderColor: Colors.gray2, + }, dappIcon: { width: 40, height: 40, @@ -108,7 +139,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', flex: 1, - padding: Spacing.Regular16, + paddingVertical: Spacing.Regular16, }, card: { marginTop: Spacing.Regular16, diff --git a/src/dappsExplorer/DiscoverDappsCard.tsx b/src/dappsExplorer/DiscoverDappsCard.tsx new file mode 100644 index 00000000000..36ed5d7f0fc --- /dev/null +++ b/src/dappsExplorer/DiscoverDappsCard.tsx @@ -0,0 +1,179 @@ +import React, { useEffect, useMemo, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { SectionList, StyleSheet, Text, View } from 'react-native' +import { useSafeAreaInsets } from 'react-native-safe-area-context' +import { DappExplorerEvents } from 'src/analytics/Events' +import ValoraAnalytics from 'src/analytics/ValoraAnalytics' +import { mostPopularDappsSelector, favoriteDappsSelector } from 'src/dapps/selectors' +import { fetchDappsList } from 'src/dapps/slice' +import { ActiveDapp, Dapp, DappSection } from 'src/dapps/types' +import DappCard from 'src/dappsExplorer/DappCard' +import useOpenDapp from 'src/dappsExplorer/useOpenDapp' +import { currentLanguageSelector } from 'src/i18n/selectors' +import { Screens } from 'src/navigator/Screens' +import { useDispatch, useSelector } from 'src/redux/hooks' +import { Colors } from 'src/styles/colors' +import fontStyles, { typeScale } from 'src/styles/fonts' +import { Spacing } from 'src/styles/styles' +import TextButton from 'src/components/TextButton' +import { navigate } from 'src/navigator/NavigationService' + +interface SectionData { + data: Dapp[] + sectionName: string + dappSection: DappSection + testID: string +} + +const MAX_DAPPS = 5 + +function DiscoverDappsCard() { + const { t } = useTranslation() + + const insets = useSafeAreaInsets() + + const sectionListRef = useRef(null) + + const language = useSelector(currentLanguageSelector) + const dispatch = useDispatch() + const favoriteDapps = useSelector(favoriteDappsSelector) + const mostPopularDapps = useSelector(mostPopularDappsSelector) + + const { onSelectDapp } = useOpenDapp() + + useEffect(() => { + dispatch(fetchDappsList()) + ValoraAnalytics.track(DappExplorerEvents.dapp_screen_open) + }, []) + + const onPressDapp = (dapp: ActiveDapp, index: number) => { + onSelectDapp(dapp, { + position: 1 + index, + fromScreen: Screens.TabDiscover, + }) + } + + const sections: SectionData[] = useMemo(() => { + const favoriteDappIds = favoriteDapps.map((dapp) => dapp.id) + let mostPopularSection: SectionData[] = [] + if (favoriteDapps.length <= 2) { + mostPopularSection = [ + { + data: mostPopularDapps + .filter((dapp) => !favoriteDappIds.includes(dapp.id)) + .slice(0, MAX_DAPPS - favoriteDapps.length), + sectionName: t('dappsScreen.mostPopularDapps').toLocaleUpperCase(language ?? 'en-US'), + dappSection: DappSection.MostPopular, + testID: 'DiscoverDappsCard/MostPopularSection', + }, + ] + } + const favoritesSection = + favoriteDapps.length > 0 + ? [ + { + data: favoriteDapps.slice(0, MAX_DAPPS), + sectionName: t('dappsScreen.favoriteDapps').toLocaleUpperCase(language ?? 'en-US'), + dappSection: DappSection.FavoritesDappScreen, + testID: 'DiscoverDappsCard/FavoritesSection', + }, + ] + : [] + + return [...favoritesSection, ...mostPopularSection] + }, [favoriteDapps, mostPopularDapps]) + + const onPressExploreAll = () => { + ValoraAnalytics.track(DappExplorerEvents.dapp_explore_all) + navigate(Screens.DappsScreen) + } + + if (!sections.length) return null + + return ( + + {t('dappsScreen.exploreDapps')}} + ListFooterComponent={ + + {t('dappsScreen.exploreAll')} + + } + contentContainerStyle={[ + styles.sectionListContentContainer, + { paddingBottom: Math.max(insets.bottom, Spacing.Regular16) }, + ]} + sections={sections} + renderItem={({ item: dapp, index, section }) => { + return ( + + onPressDapp({ ...dapp, openedFrom: section.dappSection }, index)} + disableFavoriting={true} + testID={`${section.testID}/DappCard`} + /> + + ) + }} + renderSectionHeader={({ section: { sectionName, testID } }) => { + return ( + + {sectionName} + + ) + }} + keyExtractor={(dapp) => dapp.id} + stickySectionHeadersEnabled={false} + testID="DAppsExplorerScreen/DiscoverDappsCard" + ListFooterComponentStyle={styles.listFooterComponent} + keyboardShouldPersistTaps="always" + keyboardDismissMode="on-drag" + /> + + ) +} + +const styles = StyleSheet.create({ + container: { + padding: Spacing.Regular16, + paddingBottom: 0, + gap: Spacing.Smallest8, + borderColor: Colors.gray2, + borderWidth: 1, + borderRadius: Spacing.Smallest8, + }, + sectionListContentContainer: { + paddingTop: Spacing.Regular16, + flexGrow: 1, + }, + sectionTitle: { + ...fontStyles.label, + paddingTop: Spacing.Small12, + paddingLeft: Spacing.Regular16, + color: Colors.gray4, + }, + listFooterComponent: { + flex: 1, + alignItems: 'center', + paddingTop: Spacing.Regular16, + }, + footer: { + ...typeScale.labelSemiBoldXSmall, + flex: 1, + color: Colors.primary, + }, + title: { + ...typeScale.titleMedium, + color: Colors.black, + marginBottom: Spacing.Large32, + paddingLeft: Spacing.Regular16, + }, + cardContainer: { + paddingLeft: Spacing.Smallest8, + }, +}) + +export default DiscoverDappsCard diff --git a/src/dappsExplorer/TabDiscover.test.tsx b/src/dappsExplorer/TabDiscover.test.tsx index 5f1a605e05d..718073871f7 100644 --- a/src/dappsExplorer/TabDiscover.test.tsx +++ b/src/dappsExplorer/TabDiscover.test.tsx @@ -1,9 +1,9 @@ -import { fireEvent, render, within } from '@testing-library/react-native' +import { fireEvent, render } from '@testing-library/react-native' import * as React from 'react' import { Provider } from 'react-redux' import { DappExplorerEvents } from 'src/analytics/Events' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' -import { dappSelected, favoriteDapp, fetchDappsList, unfavoriteDapp } from 'src/dapps/slice' +import { dappSelected, fetchDappsList } from 'src/dapps/slice' import { DappCategory, DappSection } from 'src/dapps/types' import TabDiscover from 'src/dappsExplorer/TabDiscover' import { getFeatureGate } from 'src/statsig' @@ -13,6 +13,8 @@ import networkConfig from 'src/web3/networkConfig' import MockedNavigator from 'test/MockedNavigator' import { createMockStore } from 'test/utils' import { mockAaveArbUsdcAddress, mockDappListWithCategoryNames, mockUSDCAddress } from 'test/values' +import { navigate } from 'src/navigator/NavigationService' +import { Screens } from 'src/navigator/Screens' jest.mock('src/analytics/ValoraAnalytics') jest.mock('src/statsig', () => ({ @@ -23,7 +25,36 @@ jest.mock('src/statsig', () => ({ getFeatureGate: jest.fn(), })) -const dappsList = mockDappListWithCategoryNames +const dappsList = [ + ...mockDappListWithCategoryNames, + { + name: 'Dapp 4', + id: 'dapp4', + categories: ['1'], + categoryNames: ['Swap'], + description: 'Some dapp thing', + iconUrl: 'https://raw.githubusercontent.com/valora-inc/app-list/main/assets/dapp4.png', + dappUrl: 'https://app.dapp4.org/', + }, + { + name: 'Dapp 5', + id: 'dapp5', + categories: ['1'], + categoryNames: ['Swap'], + description: 'Some dapp thing', + iconUrl: 'https://raw.githubusercontent.com/valora-inc/app-list/main/assets/dapp5.png', + dappUrl: 'https://app.dapp5.org/', + }, + { + name: 'Dapp 6', + id: 'dapp6', + categories: ['1'], + categoryNames: ['Swap'], + description: 'Some dapp thing', + iconUrl: 'https://raw.githubusercontent.com/valora-inc/app-list/main/assets/dapp6.png', + dappUrl: 'https://app.dapp6.org/', + }, +] const dappsCategories: DappCategory[] = [ { @@ -40,6 +71,17 @@ const dappsCategories: DappCategory[] = [ }, ] +const mostPopularDappIds = ['dapp1', 'dapp2', 'dapp3', 'dapp4', 'dapp5'] + +const defaultExpectedDappOpenProps = { + categories: ['1'], + dappId: 'dapp1', + dappName: 'Dapp 1', + position: 1, + section: 'favorites dapp screen', + fromScreen: 'TabDiscover', +} + const defaultStore = createMockStore({ dapps: { dappListApiUrl: 'http://url.com', dappsList, dappsCategories }, }) @@ -51,683 +93,240 @@ describe('TabDiscover', () => { jest.mocked(getFeatureGate).mockReturnValue(false) }) - it('renders correctly and fires the correct actions on press dapp', () => { - const { getByText, queryByText } = render( - + it('renders correctly when there are no favorite dapps', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + mostPopularDappIds, + favoriteDappIds: [], + }, + }) + const { queryByTestId, getByTestId, getAllByTestId } = render( + ) - expect(defaultStore.getActions()).toEqual([fetchDappsList()]) - expect(queryByText('featuredDapp')).toBeFalsy() - expect(getByText('Dapp 1')).toBeTruthy() - expect(getByText('Dapp 2')).toBeTruthy() + expect(getByTestId('DiscoverDappsCard')).toBeTruthy() - fireEvent.press(getByText('Dapp 1')) + const mostPopularSectionResults = getAllByTestId( + 'DiscoverDappsCard/MostPopularSection/DappCard' + ) - expect(defaultStore.getActions()).toEqual([ - fetchDappsList(), - dappSelected({ dapp: { ...dappsList[0], openedFrom: DappSection.All } }), - ]) - }) + expect(mostPopularSectionResults.length).toBe(5) + expect(mostPopularSectionResults[0]).toHaveTextContent(dappsList[0].name) + expect(mostPopularSectionResults[0]).toHaveTextContent(dappsList[0].description) - it('renders correctly and fires the correct actions on press deep linked dapp', () => { - const { getByText, queryByText } = render( - - - - ) + expect(mostPopularSectionResults[1]).toHaveTextContent(dappsList[1].name) + expect(mostPopularSectionResults[1]).toHaveTextContent(dappsList[1].description) - expect(defaultStore.getActions()).toEqual([fetchDappsList()]) - expect(getByText('Dapp 1')).toBeTruthy() - expect(getByText('Dapp 2')).toBeTruthy() - expect(queryByText('featuredDapp')).toBeFalsy() + expect(mostPopularSectionResults[2]).toHaveTextContent(dappsList[2].name) + expect(mostPopularSectionResults[2]).toHaveTextContent(dappsList[2].description) - fireEvent.press(getByText('Dapp 2')) + expect(mostPopularSectionResults[3]).toHaveTextContent(dappsList[3].name) + expect(mostPopularSectionResults[3]).toHaveTextContent(dappsList[3].description) - expect(defaultStore.getActions()).toEqual([ - fetchDappsList(), - dappSelected({ dapp: { ...dappsList[1], openedFrom: DappSection.All } }), - ]) + expect(mostPopularSectionResults[4]).toHaveTextContent(dappsList[4].name) + expect(mostPopularSectionResults[4]).toHaveTextContent(dappsList[4].description) + + expect(queryByTestId('DiscoverDappsCard/FavoritesSection/Title')).toBeFalsy() }) - it('pens dapps directly', () => { + it('renders correctly when there are <=2 favorite dapps', () => { const store = createMockStore({ dapps: { dappListApiUrl: 'http://url.com', dappsList, dappsCategories, + mostPopularDappIds, + favoriteDappIds: ['dapp1', 'dapp3'], }, }) - const { getByText } = render( + const { getByTestId, getAllByTestId } = render( ) - expect(getByText('dappsDisclaimerAllDapps')).toBeTruthy() + expect(getByTestId('DiscoverDappsCard')).toBeTruthy() - fireEvent.press(getByText('Dapp 1')) + const mostPopularSectionResults = getAllByTestId( + 'DiscoverDappsCard/MostPopularSection/DappCard' + ) - expect(store.getActions()).toEqual([ - fetchDappsList(), - dappSelected({ dapp: { ...dappsList[0], openedFrom: DappSection.All } }), - ]) + expect(mostPopularSectionResults.length).toBe(3) + expect(mostPopularSectionResults[0]).toHaveTextContent(dappsList[1].name) + expect(mostPopularSectionResults[0]).toHaveTextContent(dappsList[1].description) + + expect(mostPopularSectionResults[1]).toHaveTextContent(dappsList[3].name) + expect(mostPopularSectionResults[1]).toHaveTextContent(dappsList[3].description) + + expect(mostPopularSectionResults[2]).toHaveTextContent(dappsList[4].name) + expect(mostPopularSectionResults[2]).toHaveTextContent(dappsList[4].description) + + const favoritesSectionResults = getAllByTestId('DiscoverDappsCard/FavoritesSection/DappCard') + + expect(favoritesSectionResults.length).toBe(2) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].name) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].description) + + expect(favoritesSectionResults[1]).toHaveTextContent(dappsList[2].name) + expect(favoritesSectionResults[1]).toHaveTextContent(dappsList[2].description) }) - it('renders error message when fetching dapps fails', () => { + it('renders correctly when there are >2 favorite dapps', () => { const store = createMockStore({ dapps: { dappListApiUrl: 'http://url.com', - dappsList: [], - dappsCategories: [], - dappsListError: 'Error fetching dapps', + dappsList, + dappsCategories, + mostPopularDappIds, + favoriteDappIds: ['dapp1', 'dapp3', 'dapp4'], }, }) - const { getByText, getByTestId } = render( + const { queryByTestId, getByTestId, getAllByTestId } = render( ) - expect(getByText('dappsScreen.errorMessage')).toBeTruthy() - // asserts whether categories.length (0) isn't rendered, which causes a - // crash in the app - expect(getByTestId('DAppsExplorerScreen')).not.toHaveTextContent('0') - }) - - describe('favorite dapps', () => { - it('renders correctly when there are no favorite dapps', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: [], - }, - }) - const { getByText, queryByText } = render( - - - - ) - - expect(queryByText('dappsScreen.noFavorites.title')).toBeNull() - expect(queryByText('dappsScreen.noFavorites.description')).toBeNull() - expect(getByText('Dapp 1')).toBeTruthy() - expect(getByText('Dapp 2')).toBeTruthy() - }) - - it('renders correctly when there are favourited dapps', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp2'], - }, - }) - const { queryByText, getAllByTestId } = render( - - - - ) - - const favoritesSectionResults = getAllByTestId( - 'DAppsExplorerScreen/FavoritesSection/DappCard' - ) - expect(favoritesSectionResults.length).toBe(1) - expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[1].name) - expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[1].description) - expect(queryByText('dappsScreen.favoritedDappToast.message')).toBeFalsy() - }) - - it('triggers the events when favoriting', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp1'], - }, - }) - const { getByTestId, getByText } = render( - - - - ) + expect(getByTestId('DiscoverDappsCard')).toBeTruthy() - // don't include events dispatched on screen load - jest.clearAllMocks() + expect(queryByTestId('DiscoverDappsCard/MostPopularSection/Title')).toBeFalsy() - const allDappsSection = getByTestId('DAppsExplorerScreen/DappsList') - fireEvent.press(within(allDappsSection).getByTestId('Dapp/Favorite/dapp2')) + const favoritesSectionResults = getAllByTestId('DiscoverDappsCard/FavoritesSection/DappCard') - // favorited dapp confirmation toast - expect(getByText('dappsScreen.favoritedDappToast.message')).toBeTruthy() - expect(getByText('dappsScreen.favoritedDappToast.labelCTA')).toBeTruthy() + expect(favoritesSectionResults.length).toBe(3) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].name) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].description) - expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) - expect(ValoraAnalytics.track).toHaveBeenCalledWith('dapp_favorite', { - categories: ['2'], - dappId: 'dapp2', - dappName: 'Dapp 2', - section: 'all', - }) - expect(store.getActions()).toEqual([fetchDappsList(), favoriteDapp({ dappId: 'dapp2' })]) - }) + expect(favoritesSectionResults[1]).toHaveTextContent(dappsList[2].name) + expect(favoritesSectionResults[1]).toHaveTextContent(dappsList[2].description) - it('triggers the events when unfavoriting', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp2'], - }, - }) - const { getByTestId, getAllByTestId, queryByText } = render( - - - - ) - - // don't include events dispatched on screen load - jest.clearAllMocks() - - const selectedDappCards = getAllByTestId('Dapp/dapp2') - // should only appear once, in the favorites section - expect(selectedDappCards).toHaveLength(1) - - fireEvent.press(getByTestId('Dapp/Favorite/dapp2')) - - expect(queryByText('dappsScreen.favoritedDappToast.message')).toBeFalsy() - - expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) - expect(ValoraAnalytics.track).toHaveBeenCalledWith('dapp_unfavorite', { - categories: ['2'], - dappId: 'dapp2', - dappName: 'Dapp 2', - section: 'all', - }) - expect(store.getActions()).toEqual([fetchDappsList(), unfavoriteDapp({ dappId: 'dapp2' })]) - }) + expect(favoritesSectionResults[2]).toHaveTextContent(dappsList[3].name) + expect(favoritesSectionResults[2]).toHaveTextContent(dappsList[3].description) }) - describe('searching dapps', () => { - it('renders correctly when there are no search results', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: [], - }, - }) - - const { getByTestId, queryByTestId } = render( - - - - ) - - fireEvent.changeText(getByTestId('SearchInput'), 'iDoNotExist') - - // Should the no results within the all section - expect(queryByTestId('DAppsExplorerScreen/FavoritesSection/NoResults')).toBeNull() - expect(getByTestId('DAppsExplorerScreen/AllSection/NoResults')).toBeTruthy() + it('renders correctly when there are >5 favorite dapps', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + mostPopularDappIds, + favoriteDappIds: ['dapp1', 'dapp2', 'dapp3', 'dapp4', 'dapp5', 'dapp6'], + }, }) + const { queryByTestId, getByTestId, getAllByTestId } = render( + + + + ) - it('renders correctly when there are search results in both sections', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp2'], - }, - }) - - const { getByTestId, queryByTestId, getAllByTestId } = render( - - - - ) - - fireEvent.changeText(getByTestId('SearchInput'), 'dapp') - - // Should display the correct sections - const favoritesSectionResults = getAllByTestId( - 'DAppsExplorerScreen/FavoritesSection/DappCard' - ) - expect(favoritesSectionResults.length).toBe(1) - expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[1].name) - - const allSectionResults = getAllByTestId('DAppsExplorerScreen/AllSection/DappCard') - expect(allSectionResults.length).toBe(2) - expect(allSectionResults[0]).toHaveTextContent(dappsList[0].name) - expect(allSectionResults[1]).toHaveTextContent(dappsList[2].name) + expect(getByTestId('DiscoverDappsCard')).toBeTruthy() - // No results sections should not be displayed - expect(queryByTestId('DAppsExplorerScreen/FavoritesSection/NoResults')).toBeNull() - expect(queryByTestId('DAppsExplorerScreen/AllSection/NoResults')).toBeNull() - }) + expect(queryByTestId('DiscoverDappsCard/MostPopularSection/Title')).toBeFalsy() - it('clearing search input should show all dapps', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp2'], - }, - }) + const favoritesSectionResults = getAllByTestId('DiscoverDappsCard/FavoritesSection/DappCard') - const { getByTestId, getAllByTestId } = render( - - - - ) + expect(favoritesSectionResults.length).toBe(5) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].name) + expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].description) - // Type in search that should have no results - fireEvent.press(getByTestId('SearchInput')) - fireEvent.changeText(getByTestId('SearchInput'), 'iDoNotExist') + expect(favoritesSectionResults[1]).toHaveTextContent(dappsList[1].name) + expect(favoritesSectionResults[1]).toHaveTextContent(dappsList[1].description) - // Clear search field - onPress is tested in src/components/CircleButton.test.tsx - fireEvent.changeText(getByTestId('SearchInput'), '') + expect(favoritesSectionResults[2]).toHaveTextContent(dappsList[2].name) + expect(favoritesSectionResults[2]).toHaveTextContent(dappsList[2].description) - // Dapps displayed in the correct sections - const favoritesSectionResults = getAllByTestId( - 'DAppsExplorerScreen/FavoritesSection/DappCard' - ) - expect(favoritesSectionResults.length).toBe(1) - expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[1].name) + expect(favoritesSectionResults[3]).toHaveTextContent(dappsList[3].name) + expect(favoritesSectionResults[3]).toHaveTextContent(dappsList[3].description) - const allSectionResults = getAllByTestId('DAppsExplorerScreen/AllSection/DappCard') - expect(allSectionResults.length).toBe(2) - expect(allSectionResults[0]).toHaveTextContent(dappsList[0].name) - expect(allSectionResults[1]).toHaveTextContent(dappsList[2].name) - }) + expect(favoritesSectionResults[4]).toHaveTextContent(dappsList[4].name) + expect(favoritesSectionResults[4]).toHaveTextContent(dappsList[4].description) }) - describe('filter dapps', () => { - it('renders correctly when there are no filters applied', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp1'], - }, - }) - const { getByText, queryByText, getAllByTestId } = render( - - - - ) - - // All Filter Chips is not displayed - expect(queryByText('dappsScreen.allDapps')).toBeFalsy() - // Category Filter Chips displayed - expect(getByText(dappsCategories[0].name)).toBeTruthy() - expect(getByText(dappsCategories[1].name)).toBeTruthy() - - const favoritesSectionResults = getAllByTestId( - 'DAppsExplorerScreen/FavoritesSection/DappCard' - ) - expect(favoritesSectionResults.length).toBe(1) - expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].name) - - const allSectionResults = getAllByTestId('DAppsExplorerScreen/AllSection/DappCard') - expect(allSectionResults.length).toBe(2) - expect(allSectionResults[0]).toHaveTextContent(dappsList[1].name) - expect(allSectionResults[1]).toHaveTextContent(dappsList[2].name) - }) - - it('renders correctly when there are filters applied', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp1'], - }, - }) - const { getByTestId, getByText, queryByTestId, getAllByTestId } = render( - - - - ) - - // Tap on category 2 filter - fireEvent.press(getByText(dappsCategories[1].name)) - - // Favorite Section should not show results - expect(queryByTestId('DAppsExplorerScreen/FavoritesSection/DappCard')).toBeNull() - - // All Section should show only 'dapp 2' - const allDappsSection = getByTestId('DAppsExplorerScreen/DappsList') - // queryByText returns null if not found - expect(within(allDappsSection).queryByText(dappsList[0].name)).toBeFalsy() - expect(within(allDappsSection).queryByText(dappsList[0].description)).toBeFalsy() - expect(within(allDappsSection).getByText(dappsList[1].name)).toBeTruthy() - expect(within(allDappsSection).getByText(dappsList[1].description)).toBeTruthy() - - const allSectionResults = getAllByTestId('DAppsExplorerScreen/AllSection/DappCard') - expect(allSectionResults.length).toBe(1) - expect(allSectionResults[0]).toHaveTextContent(dappsList[1].name) - }) - - it('triggers event when filtering', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp1'], - }, - }) - const { getByText } = render( - - - - ) - - // don't include events dispatched on screen load - jest.clearAllMocks() - - // Tap on category 2 filter - fireEvent.press(getByText(dappsCategories[1].name)) - - expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) - expect(ValoraAnalytics.track).toHaveBeenCalledWith(DappExplorerEvents.dapp_filter, { - filterId: '2', - remove: false, - }) - }) - - it('triggers events when toggling a category filter', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp1'], - }, - }) - const { getByText } = render( - - - - ) - - // don't include events dispatched on screen load - jest.clearAllMocks() - - // Tap on category 2 filter - fireEvent.press(getByText(dappsCategories[1].name)) - - // Tap on category 2 filter again to remove it - fireEvent.press(getByText(dappsCategories[1].name)) - - expect(ValoraAnalytics.track).toHaveBeenCalledTimes(2) - expect(ValoraAnalytics.track).toHaveBeenNthCalledWith(1, DappExplorerEvents.dapp_filter, { - filterId: '2', - remove: false, - }) - expect(ValoraAnalytics.track).toHaveBeenNthCalledWith(2, DappExplorerEvents.dapp_filter, { - filterId: '2', - remove: true, - }) - }) - - it('triggers event when clearing filters from category section', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp2'], - }, - }) - const { getByTestId, getByText } = render( - - - - ) - - // don't include events dispatched on screen load - jest.clearAllMocks() - - // Tap on category 2 filter - fireEvent.press(getByText(dappsCategories[1].name)) - - // Tap on remove filters from all section - fireEvent.press(getByTestId('DAppsExplorerScreen/AllSection/NoResults/RemoveFilter')) - - // Assert correct analytics are fired - expect(ValoraAnalytics.track).toHaveBeenCalledTimes(2) - expect(ValoraAnalytics.track).toHaveBeenCalledWith(DappExplorerEvents.dapp_filter, { - filterId: '2', - remove: false, - }) - expect(ValoraAnalytics.track).toHaveBeenCalledWith(DappExplorerEvents.dapp_filter, { - filterId: '2', - remove: true, - }) + it('fires event on favorite dapp press', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + mostPopularDappIds, + favoriteDappIds: ['dapp1', 'dapp3'], + }, }) + const { getByText } = render( + + + + ) - it('triggers event when clearing filters from favorite section', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp1'], - }, - }) - const { getByText, queryByTestId } = render( - - - - ) - - // don't include events dispatched on screen load - jest.clearAllMocks() - - // Tap on category 2 filter - fireEvent.press(getByText(dappsCategories[1].name)) - - // Should not render favorites section - expect(queryByTestId('DAppsExplorerScreen/FavoritesSection/Title')).toBeNull() - expect(queryByTestId('DAppsExplorerScreen/FavoritesSection/DappCard')).toBeNull() + fireEvent.press(getByText('Dapp 1')) - // Assert correct analytics are fired - expect(ValoraAnalytics.track).toHaveBeenCalledTimes(1) - expect(ValoraAnalytics.track).toHaveBeenCalledWith(DappExplorerEvents.dapp_filter, { - filterId: '2', - remove: false, - }) - }) + expect(store.getActions()).toEqual([ + fetchDappsList(), + dappSelected({ dapp: { ...dappsList[0], openedFrom: DappSection.FavoritesDappScreen } }), + ]) + expect(ValoraAnalytics.track).toHaveBeenCalledWith( + DappExplorerEvents.dapp_open, + defaultExpectedDappOpenProps + ) }) - describe('searching and filtering', () => { - it('renders correctly when there are results in all and favorites sections', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp1'], - }, - }) - const { getByTestId, getAllByTestId } = render( - - - - ) - - // Search for 'tokens' - fireEvent.changeText(getByTestId('SearchInput'), 'tokens') - - // Favorites section displays correctly - const favoritesSectionResults = getAllByTestId( - 'DAppsExplorerScreen/FavoritesSection/DappCard' - ) - expect(favoritesSectionResults.length).toBe(1) - expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].name) - - // All section displays correctly - const allSectionResults = getAllByTestId('DAppsExplorerScreen/AllSection/DappCard') - expect(allSectionResults.length).toBe(1) - expect(allSectionResults[0]).toHaveTextContent(dappsList[1].name) - }) - - it('renders correctly when there are results in favorites and no results in all', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp1'], - }, - }) - const { getByTestId, getByText, getAllByTestId, queryByTestId } = render( - - - - ) - - // Searching for tokens should return both dapps - fireEvent.changeText(getByTestId('SearchInput'), 'tokens') - - // Filtering by category 1 should return only dapp 1 - fireEvent.press(getByText(dappsCategories[0].name)) - - // Favorite Section should show only 'dapp 1' - const favoritesSectionResults = getAllByTestId( - 'DAppsExplorerScreen/FavoritesSection/DappCard' - ) - expect(favoritesSectionResults.length).toBe(1) - expect(favoritesSectionResults[0]).toHaveTextContent(dappsList[0].name) - - // All Section should show no results - expect(queryByTestId('DAppsExplorerScreen/AllSection/DappCard')).toBeFalsy() - expect(getByTestId('DAppsExplorerScreen/AllSection/NoResults')).toBeTruthy() + it('fires event on popular dapp press', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + mostPopularDappIds, + favoriteDappIds: ['dapp1', 'dapp3'], + }, }) + const { getByText } = render( + + + + ) - it('renders correctly when there are no results in favorites and results in all', () => { - const store = createMockStore({ - dapps: { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: ['dapp2'], - }, - }) - const { getByTestId, getByText, queryByTestId } = render( - - - - ) - - // Searching for tokens should return both dapps - fireEvent.changeText(getByTestId('SearchInput'), 'tokens') - - // Filtering by category 1 should return only dapp 1 - fireEvent.press(getByText(dappsCategories[0].name)) - - // Favorite Section should not show - expect(queryByTestId('FavoriteDappsSection')).toBeNull() + fireEvent.press(getByText('Dapp 2')) - // All Section should show only 'dapp 1' - const allDappsSection = getByTestId('DAppsExplorerScreen/DappsList') - expect(within(allDappsSection).getByText(dappsList[0].name)).toBeTruthy() - expect(within(allDappsSection).getByText(dappsList[0].description)).toBeTruthy() - expect(within(allDappsSection).queryByText(dappsList[1].name)).toBeFalsy() - expect(within(allDappsSection).queryByText(dappsList[1].description)).toBeFalsy() + expect(store.getActions()).toEqual([ + fetchDappsList(), + dappSelected({ dapp: { ...dappsList[1], openedFrom: DappSection.MostPopular } }), + ]) + expect(ValoraAnalytics.track).toHaveBeenCalledWith(DappExplorerEvents.dapp_open, { + ...defaultExpectedDappOpenProps, + categories: ['2'], + dappId: 'dapp2', + dappName: 'Dapp 2', + section: 'mostPopular', }) }) - describe('dapp open analytics event properties', () => { - const defaultStoreProps = { - dappListApiUrl: 'http://url.com', - dappsList, - dappsCategories, - favoriteDappIds: [], - } - const defaultExpectedDappOpenProps = { - activeFilter: 'all', - activeSearchTerm: '', - categories: ['1'], - dappId: 'dapp3', - dappName: 'Dapp 3', - position: 3, - section: 'all', - } - - it('should dispatch with no filter or search, from the normal list with no confirmation bottom sheet', () => { - const store = createMockStore({ - dapps: defaultStoreProps, - }) - const { getByText } = render( - - - - ) - - fireEvent.press(getByText('Dapp 3')) - - expect(ValoraAnalytics.track).toHaveBeenLastCalledWith( - DappExplorerEvents.dapp_open, - defaultExpectedDappOpenProps - ) - }) - - it('should dispatch with no filter or search, with favourites', () => { - const store = createMockStore({ - dapps: { - ...defaultStoreProps, - favoriteDappIds: ['dapp1'], - }, - }) - const { getByText } = render( - - - - ) - - fireEvent.press(getByText('Dapp 3')) - - expect(ValoraAnalytics.track).toHaveBeenLastCalledWith(DappExplorerEvents.dapp_open, { - ...defaultExpectedDappOpenProps, - position: 2, // note the position explicitly does not take into account the number of favorites - }) + it('navigates to dapp screen on button press and fires event', () => { + const store = createMockStore({ + dapps: { + dappListApiUrl: 'http://url.com', + dappsList, + dappsCategories, + mostPopularDappIds, + favoriteDappIds: ['dapp1', 'dapp3'], + }, }) + const { getByText } = render( + + + + ) - it('should dispatch with filter and search', () => { - const store = createMockStore({ - dapps: defaultStoreProps, - }) - const { getByText, getByTestId, getAllByTestId } = render( - - - - ) - - fireEvent.changeText(getByTestId('SearchInput'), 'cool') - fireEvent.press(getByText(dappsCategories[0].name)) - - expect(getAllByTestId('DAppsExplorerScreen/AllSection/DappCard')).toHaveLength(1) - fireEvent.press(getByText('Dapp 3')) + fireEvent.press(getByText('dappsScreen.exploreAll')) - expect(ValoraAnalytics.track).toHaveBeenLastCalledWith(DappExplorerEvents.dapp_open, { - ...defaultExpectedDappOpenProps, - activeFilter: '1', - activeSearchTerm: 'cool', - position: 1, - }) - }) + expect(ValoraAnalytics.track).toHaveBeenCalledWith(DappExplorerEvents.dapp_explore_all) + expect(navigate).toHaveBeenCalledWith(Screens.DappsScreen) }) describe('earn', () => { diff --git a/src/dappsExplorer/TabDiscover.tsx b/src/dappsExplorer/TabDiscover.tsx index a9150a56aaa..78dbc8286fc 100644 --- a/src/dappsExplorer/TabDiscover.tsx +++ b/src/dappsExplorer/TabDiscover.tsx @@ -1,152 +1,40 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' -import React, { useEffect, useMemo, useRef, useState } from 'react' +import React, { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { - LayoutChangeEvent, - RefreshControl, - SectionList, - SectionListProps, - StyleSheet, - Text, - View, -} from 'react-native' +import { StyleSheet, Text, View } from 'react-native' import { ScrollView } from 'react-native-gesture-handler' -import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' -import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context' +import { useSharedValue } from 'react-native-reanimated' +import { SafeAreaView } from 'react-native-safe-area-context' import { DappExplorerEvents } from 'src/analytics/Events' import ValoraAnalytics from 'src/analytics/ValoraAnalytics' import { BottomSheetRefType } from 'src/components/BottomSheet' -import FilterChipsCarousel, { BooleanFilterChip } from 'src/components/FilterChipsCarousel' -import SearchInput from 'src/components/SearchInput' -import { - dappsCategoriesAlphabeticalSelector, - dappsListErrorSelector, - dappsListLoadingSelector, - favoriteDappsWithCategoryNamesSelector, - nonFavoriteDappsWithCategoryNamesSelector, -} from 'src/dapps/selectors' -import { fetchDappsList } from 'src/dapps/slice' -import { ActiveDapp, Dapp, DappSection, DappWithCategoryNames } from 'src/dapps/types' -import DappCard from 'src/dappsExplorer/DappCard' import { DappFeaturedActions } from 'src/dappsExplorer/DappFeaturedActions' -import { DappRankingsBottomSheet } from 'src/dappsExplorer/DappRankingsBottomSheet' -import NoResults from 'src/dappsExplorer/NoResults' -import { searchDappList } from 'src/dappsExplorer/searchDappList' -import useDappFavoritedToast from 'src/dappsExplorer/useDappFavoritedToast' -import useOpenDapp from 'src/dappsExplorer/useOpenDapp' import { EarnCardDiscover } from 'src/earn/EarnCard' -import { currentLanguageSelector } from 'src/i18n/selectors' import { Screens } from 'src/navigator/Screens' import useScrollAwareHeader from 'src/navigator/ScrollAwareHeader' import { StackParamList } from 'src/navigator/types' import PointsDiscoverCard from 'src/points/PointsDiscoverCard' -import { useDispatch, useSelector } from 'src/redux/hooks' import { Colors } from 'src/styles/colors' -import fontStyles, { typeScale } from 'src/styles/fonts' +import { typeScale } from 'src/styles/fonts' import { Spacing } from 'src/styles/styles' import networkConfig from 'src/web3/networkConfig' - -const AnimatedSectionList = - Animated.createAnimatedComponent>(SectionList) -interface SectionData { - data: DappWithCategoryNames[] - sectionName: string - dappSection: DappSection - testID: string -} +import DiscoverDappsCard from 'src/dappsExplorer/DiscoverDappsCard' type Props = NativeStackScreenProps function TabDiscover({ navigation }: Props) { const { t } = useTranslation() - const insets = useSafeAreaInsets() - - const sectionListRef = useRef(null) - - const horizontalScrollView = useRef(null) const dappRankingsBottomSheetRef = useRef(null) - const dispatch = useDispatch() - const loading = useSelector(dappsListLoadingSelector) - const error = useSelector(dappsListErrorSelector) - const categories = useSelector(dappsCategoriesAlphabeticalSelector) - const language = useSelector(currentLanguageSelector) - const nonFavoriteDappsWithCategoryNames = useSelector(nonFavoriteDappsWithCategoryNamesSelector) - const favoriteDappsWithCategoryNames = useSelector(favoriteDappsWithCategoryNamesSelector) - - const [filterChips, setFilterChips] = useState[]>(() => - categories.map((category) => ({ - id: category.id, - name: category.name, - filterFn: (dapp: DappWithCategoryNames) => dapp.categories.includes(category.id), - isSelected: false, - })) - ) - const selectedFilter = useMemo( - () => filterChips.find((filter) => filter.isSelected), - [filterChips] - ) - - const [searchTerm, setSearchTerm] = useState('') - - const { onSelectDapp } = useOpenDapp() - const { onFavoriteDapp, DappFavoritedToast } = useDappFavoritedToast(sectionListRef) - - const removeFilter = (filter: BooleanFilterChip) => { - ValoraAnalytics.track(DappExplorerEvents.dapp_filter, { - filterId: filter.id, - remove: true, - }) - setFilterChips((prev) => prev.map((filter) => ({ ...filter, isSelected: false }))) - horizontalScrollView.current?.scrollTo({ x: 0, animated: true }) - } - - const handleToggleFilterChip = (filter: BooleanFilterChip) => { - ValoraAnalytics.track(DappExplorerEvents.dapp_filter, { - filterId: filter.id, - remove: selectedFilter?.id === filter.id, - }) - - setFilterChips((prev) => - prev.map((prevFilter) => { - return { - ...prevFilter, - isSelected: prevFilter.id === filter.id ? !prevFilter.isSelected : false, - } - }) - ) - } - const handleShowDappRankings = () => { ValoraAnalytics.track(DappExplorerEvents.dapp_rankings_open) dappRankingsBottomSheetRef.current?.snapToIndex(0) } - useEffect(() => { - dispatch(fetchDappsList()) - ValoraAnalytics.track(DappExplorerEvents.dapp_screen_open) - }, []) - - const onPressDapp = (dapp: ActiveDapp, index: number) => { - onSelectDapp(dapp, { - position: 1 + index, - activeFilter: selectedFilter?.id ?? 'all', - activeSearchTerm: searchTerm, - }) - } - // Scroll Aware Header const scrollPosition = useSharedValue(0) - const [titleHeight, setTitleHeight] = useState(0) - - const handleMeasureTitleHeight = (event: LayoutChangeEvent) => { - setTitleHeight(event.nativeEvent.layout.height) - } - - const handleScroll = useAnimatedScrollHandler((event) => { - scrollPosition.value = event.contentOffset.y - }) + const [titleHeight] = useState(0) useScrollAwareHeader({ navigation, @@ -156,171 +44,21 @@ function TabDiscover({ navigation }: Props) { animationDistance: titleHeight * 0.33, }) - const sections: SectionData[] = useMemo(() => { - const dappsMatchingFilter = selectedFilter - ? nonFavoriteDappsWithCategoryNames.filter((dapp) => selectedFilter.filterFn(dapp)) - : nonFavoriteDappsWithCategoryNames - const dappsMatchingFilterAndSearch = searchTerm - ? searchDappList(dappsMatchingFilter, searchTerm) - : dappsMatchingFilter - - const favouriteDappsMatchingFilter = selectedFilter - ? favoriteDappsWithCategoryNames.filter((dapp) => selectedFilter.filterFn(dapp)) - : favoriteDappsWithCategoryNames - const favouriteDappsMatchingFilterAndSearch = searchTerm - ? searchDappList(favouriteDappsMatchingFilter, searchTerm) - : favouriteDappsMatchingFilter - - const noMatchingResults = - dappsMatchingFilterAndSearch.length === 0 && - favouriteDappsMatchingFilterAndSearch.length === 0 - - return [ - ...(favouriteDappsMatchingFilterAndSearch.length > 0 - ? [ - { - data: favouriteDappsMatchingFilterAndSearch, - sectionName: t('dappsScreen.favoriteDapps').toLocaleUpperCase(language ?? 'en-US'), - dappSection: DappSection.FavoritesDappScreen, - testID: 'DAppsExplorerScreen/FavoritesSection', - }, - ] - : []), - { - data: dappsMatchingFilterAndSearch, - sectionName: (noMatchingResults - ? t('dappsScreen.favoriteDappsAndAll') - : t('dappsScreen.allDapps') - ).toLocaleUpperCase(language ?? 'en-US'), - dappSection: DappSection.All, - testID: 'DAppsExplorerScreen/AllSection', - }, - ] - }, [ - nonFavoriteDappsWithCategoryNames, - favoriteDappsWithCategoryNames, - searchTerm, - selectedFilter, - ]) - return ( - - <> - {!loading && !!error && ( - - {t('dappsScreen.errorMessage')} - - )} - {!!categories.length && ( - dispatch(fetchDappsList())} - /> - } - // TODO: resolve type error - // @ts-expect-error - ref={sectionListRef} - ListFooterComponent={ - {t('dappsDisclaimerAllDapps')} - } - ListHeaderComponent={ - <> - { - - {t('bottomTabsNavigator.discover.title')} - - } - - - - { - setSearchTerm(text) - }} - value={searchTerm} - multiline={false} - placeholderTextColor={Colors.gray4} - underlineColorAndroid="transparent" - placeholder={t('dappsScreen.searchPlaceHolder') ?? undefined} - showClearButton={true} - allowFontScaling={false} - /> - - - } - style={styles.sectionList} - contentContainerStyle={[ - styles.sectionListContentContainer, - { paddingBottom: Math.max(insets.bottom, Spacing.Regular16) }, - ]} - // Workaround iOS setting an incorrect automatic inset at the top - scrollIndicatorInsets={{ top: 0.01 }} - scrollEventThrottle={16} - onScroll={handleScroll} - sections={sections} - renderItem={({ item: dapp, index, section }) => { - return ( - - onPressDapp({ ...dapp, openedFrom: section.dappSection }, index) - } - onFavoriteDapp={onFavoriteDapp} - testID={`${section.testID}/DappCard`} - /> - ) - }} - renderSectionFooter={({ section }) => { - if (section.data.length === 0) { - return ( - - ) - } - - return null - }} - renderSectionHeader={({ section: { sectionName, testID } }) => { - return ( - - {sectionName} - - ) - }} - keyExtractor={(dapp) => dapp.id} - stickySectionHeadersEnabled={false} - testID="DAppsExplorerScreen/DappsList" - ListFooterComponentStyle={styles.listFooterComponent} - keyboardShouldPersistTaps="always" - keyboardDismissMode="on-drag" + + + + {t('bottomTabsNavigator.discover.title')} + + + - )} - - {DappFavoritedToast} - - + + + + ) } @@ -328,44 +66,14 @@ const styles = StyleSheet.create({ safeAreaContainer: { flex: 1, }, - centerContainer: { - alignItems: 'center', - justifyContent: 'center', - flex: 1, - }, - dappFilterView: { - paddingTop: Spacing.Regular16, - }, - sectionListContentContainer: { - padding: Spacing.Regular16, - flexGrow: 1, - }, - refreshControl: { - backgroundColor: Colors.white, - }, - sectionList: { - flex: 1, - }, - sectionTitle: { - ...fontStyles.label, - color: Colors.gray4, - marginTop: Spacing.Large32, - }, - disclaimer: { - ...fontStyles.xsmall, - color: Colors.gray4, - textAlign: 'center', - marginTop: Spacing.Large32, - marginBottom: Spacing.Regular16, - }, - listFooterComponent: { - flex: 1, - justifyContent: 'flex-end', + contentContainer: { + paddingHorizontal: Spacing.Thick24, + paddingBottom: Spacing.Thick24, }, title: { ...typeScale.titleMedium, color: Colors.black, - marginBottom: Spacing.Large32, + paddingVertical: Spacing.Thick24, }, }) diff --git a/src/navigator/Navigator.tsx b/src/navigator/Navigator.tsx index b7ce2f4ecd1..f291901c45c 100644 --- a/src/navigator/Navigator.tsx +++ b/src/navigator/Navigator.tsx @@ -123,6 +123,7 @@ import VerificationStartScreen from 'src/verify/VerificationStartScreen' import WalletConnectSessionsScreen from 'src/walletConnect/screens/Sessions' import WalletConnectRequest from 'src/walletConnect/screens/WalletConnectRequest' import WebViewScreen from 'src/webview/WebViewScreen' +import DappsScreen from 'src/dapps/DappsScreen' const TAG = 'Navigator' @@ -288,6 +289,11 @@ const consumerIncentivesScreens = (Navigator: typeof Stack) => ( component={DappShortcutsRewards} options={headerWithBackButton} /> + ) diff --git a/src/navigator/Screens.tsx b/src/navigator/Screens.tsx index 76ceaddb9f1..4178f92b4b1 100644 --- a/src/navigator/Screens.tsx +++ b/src/navigator/Screens.tsx @@ -15,6 +15,7 @@ export enum Screens { DappKitSignTxScreen = 'DappKitSignTxScreen', DappShortcutsRewards = 'DappShortcutsRewards', DappShortcutTransactionRequest = 'DappShortcutTransactionRequest', + DappsScreen = 'DappsScreen', Debug = 'Debug', EarnInfoScreen = 'EarnInfoScreen', EarnEnterAmount = 'EarnEnterAmount', diff --git a/src/navigator/types.tsx b/src/navigator/types.tsx index 6fb1816c88e..9a36a756b40 100644 --- a/src/navigator/types.tsx +++ b/src/navigator/types.tsx @@ -80,6 +80,7 @@ export type StackParamList = { [Screens.DappShortcutTransactionRequest]: { rewardId: string } + [Screens.DappsScreen]: undefined [Screens.Debug]: undefined [Screens.EarnInfoScreen]: { tokenId: string diff --git a/test/RootStateSchema.json b/test/RootStateSchema.json index b570c13f875..54dc03727c0 100644 --- a/test/RootStateSchema.json +++ b/test/RootStateSchema.json @@ -3130,6 +3130,7 @@ "DappKitSignTxScreen", "DappShortcutTransactionRequest", "DappShortcutsRewards", + "DappsScreen", "Debug", "EarnCollectScreen", "EarnEnterAmount",