diff --git a/webapp/.eslintrc.cjs b/webapp/.eslintrc.cjs index c367014dd..e27542a51 100644 --- a/webapp/.eslintrc.cjs +++ b/webapp/.eslintrc.cjs @@ -21,8 +21,7 @@ module.exports = { '@typescript-eslint/no-unsafe-member-access': 'off', // TODO: migrate code progressively to remove this line. https://typescript-eslint.io/rules/no-unsafe-member-access/ '@typescript-eslint/no-unsafe-argument': 'off', // TODO: migrate code progressively to remove this line. https://typescript-eslint.io/rules/no-unsafe-argument/ '@typescript-eslint/no-explicit-any': 'off', // TODO: migrate code progressively to remove this line. https://typescript-eslint.io/rules/no-explicit-any - 'import/order': 'off', // TODO: migrate code progressively to remove this line. - '@typescript-eslint/no-unsafe-enum-comparison': 'off' + 'import/order': 'off' // TODO: migrate code progressively to remove this line. }, parserOptions: { project: ['./tsconfig.json'] diff --git a/webapp/src/components/AccountSidebar/AccountSidebar.types.ts b/webapp/src/components/AccountSidebar/AccountSidebar.types.ts index f4f7887cd..eec329b01 100644 --- a/webapp/src/components/AccountSidebar/AccountSidebar.types.ts +++ b/webapp/src/components/AccountSidebar/AccountSidebar.types.ts @@ -1,8 +1,9 @@ import { Dispatch } from 'redux' import { browse, BrowseAction } from '../../modules/routing/actions' +import { Section } from '../../modules/vendor/decentraland' export type Props = { - section: string + section: Section address: string isCurrentAccount?: boolean onBrowse: typeof browse diff --git a/webapp/src/components/AccountSidebar/OtherAccountSidebar/OtherAccountSidebar.types.tsx b/webapp/src/components/AccountSidebar/OtherAccountSidebar/OtherAccountSidebar.types.tsx index e64143abb..99ddc625b 100644 --- a/webapp/src/components/AccountSidebar/OtherAccountSidebar/OtherAccountSidebar.types.tsx +++ b/webapp/src/components/AccountSidebar/OtherAccountSidebar/OtherAccountSidebar.types.tsx @@ -1,8 +1,9 @@ import { AssetType } from '../../../modules/asset/types' import { BrowseOptions } from '../../../modules/routing/types' +import { Section } from '../../../modules/vendor/decentraland' export type Props = { - section: string + section: Section address: string assetType: AssetType onBrowse: (options: BrowseOptions) => void diff --git a/webapp/src/components/AssetFilters/PriceFilter/utils.ts b/webapp/src/components/AssetFilters/PriceFilter/utils.ts index b82bdb4af..9dc5afe4c 100644 --- a/webapp/src/components/AssetFilters/PriceFilter/utils.ts +++ b/webapp/src/components/AssetFilters/PriceFilter/utils.ts @@ -2,6 +2,7 @@ import { NFTCategory } from '@dcl/schemas' import { ethers } from 'ethers' import { getCategoryFromSection, getSearchEmoteCategory, getSearchWearableCategory } from '../../../modules/routing/search' import { PriceFilterExtraOption, PriceFilters, Section } from '../../../modules/vendor/decentraland' +import { isOfEnumType } from '../../../utils/enums' const LAND_MAX_PRICE_ALLOWED = ethers.BigNumber.from('1000000000000000000000000000') // 1B @@ -11,21 +12,21 @@ const WEARABLES_MAX_PRICE_ALLOWED = ethers.BigNumber.from('100000000000000000000 export const getChartUpperBound = (section: string) => { let upperBound = WEARABLES_MAX_PRICE_ALLOWED - switch (section) { - case Section.LAND: - case Section.ESTATES: - case Section.PARCELS: - upperBound = LAND_MAX_PRICE_ALLOWED - break - case Section.ENS: - upperBound = ENS_MAX_PRICE_ALLOWED - break - - default: - upperBound = WEARABLES_MAX_PRICE_ALLOWED - break + if (isOfEnumType(section, Section)) { + switch (section) { + case Section.LAND: + case Section.ESTATES: + case Section.PARCELS: + upperBound = LAND_MAX_PRICE_ALLOWED + break + case Section.ENS: + upperBound = ENS_MAX_PRICE_ALLOWED + break + default: + upperBound = WEARABLES_MAX_PRICE_ALLOWED + break + } } - return upperBound } diff --git a/webapp/src/components/AssetFilters/StatusFilter/StatusFilter.tsx b/webapp/src/components/AssetFilters/StatusFilter/StatusFilter.tsx index 0fd9ad4e7..84b35689c 100644 --- a/webapp/src/components/AssetFilters/StatusFilter/StatusFilter.tsx +++ b/webapp/src/components/AssetFilters/StatusFilter/StatusFilter.tsx @@ -16,8 +16,8 @@ export const StatusFilter = ({ status, onChange, defaultCollapsed = false }: Sta const isMobileOrTablet = useTabletAndBelowMediaQuery() const statusOptions = useMemo( () => - Object.keys(AssetStatusFilter).map(opt => ({ - value: opt.toLocaleLowerCase(), + Object.values(AssetStatusFilter).map(opt => ({ + value: opt, text: t(`nft_filters.status.${opt.toLocaleLowerCase()}`) })), [] diff --git a/webapp/src/components/AssetImage/AssetImage.types.ts b/webapp/src/components/AssetImage/AssetImage.types.ts index e0a84db7e..9d4530eed 100644 --- a/webapp/src/components/AssetImage/AssetImage.types.ts +++ b/webapp/src/components/AssetImage/AssetImage.types.ts @@ -1,6 +1,6 @@ import React from 'react' import { Dispatch } from 'redux' -import { Avatar, IPreviewController, Item, Order, Rarity } from '@dcl/schemas' +import { Avatar, IPreviewController, Item, Order, Rarity, Network } from '@dcl/schemas' import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types' import { setIsTryingOn, @@ -89,5 +89,5 @@ export type AvailableForMintPopupType = { rarity: Rarity contractAddress: string itemId: string - network: string + network: Network } diff --git a/webapp/src/components/AssetImage/AvailableForMintPopup.tsx b/webapp/src/components/AssetImage/AvailableForMintPopup.tsx index ca9ae29f8..a5bf3173f 100644 --- a/webapp/src/components/AssetImage/AvailableForMintPopup.tsx +++ b/webapp/src/components/AssetImage/AvailableForMintPopup.tsx @@ -41,7 +41,7 @@ const AvailableForMintPopup = ({ price, stock, rarity, contractAddress, itemId,
- + {formatWeiToAssetCard(price)}
diff --git a/webapp/src/components/AssetTopbar/SelectedFilters/SelectedFilters.tsx b/webapp/src/components/AssetTopbar/SelectedFilters/SelectedFilters.tsx index 749722a68..41357671e 100644 --- a/webapp/src/components/AssetTopbar/SelectedFilters/SelectedFilters.tsx +++ b/webapp/src/components/AssetTopbar/SelectedFilters/SelectedFilters.tsx @@ -93,7 +93,7 @@ export const SelectedFilters = ({ browseOptions, isLandSection, category, onBrow const handleDeleteRarity = useCallback( (rarity: string) => { - onBrowse({ rarities: rarities?.filter((r: Rarity) => r !== rarity) }) + onBrowse({ rarities: rarities?.filter((r: Rarity) => r !== (rarity as Rarity)) }) }, [onBrowse, rarities] ) diff --git a/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.tsx b/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.tsx index c01718e71..b99eab867 100644 --- a/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.tsx +++ b/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.tsx @@ -88,7 +88,9 @@ export const BuyWithCryptoModal = (props: Props) => { }, [providerChains, selectedChain]) const chainNativeToken = useMemo(() => { - return providerTokens.find(t => +t.chainId === selectedChain && t.symbol === selectedProviderChain?.nativeCurrency.symbol) + return providerTokens.find( + t => +t.chainId.toString() === selectedChain.toString() && t.symbol === selectedProviderChain?.nativeCurrency.symbol + ) }, [selectedChain, selectedProviderChain, providerTokens]) const { gasCost, isFetchingGasCost } = onGetGasCost(selectedToken, chainNativeToken, wallet) diff --git a/webapp/src/components/SuccessPage/SuccessPage.tsx b/webapp/src/components/SuccessPage/SuccessPage.tsx index 76ba2a492..9f51c43d2 100644 --- a/webapp/src/components/SuccessPage/SuccessPage.tsx +++ b/webapp/src/components/SuccessPage/SuccessPage.tsx @@ -6,6 +6,7 @@ import { NFTCategory } from '@dcl/schemas' import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { locations } from '../../modules/routing/locations' import { config } from '../../config' +import { isOfEnumType } from '../../utils/enums' import { Footer } from '../Footer' import { Asset, AssetType } from '../../modules/asset/types' import { AssetImage } from '../AssetImage' @@ -38,7 +39,7 @@ export function SuccessPage(props: Props) { const search = new URLSearchParams(useLocation().search) const contractAddress = search.get('contractAddress') const tokenId = search.get('tokenId') - const assetType = search.get('assetType') + const assetType = isOfEnumType(search.get('assetType'), AssetType) ? (search.get('assetType') as AssetType) : null const subdomain = search.get('subdomain') // this is a workaround to show the NAME while the transaction is being mined or the tokenId getting retrieved. diff --git a/webapp/src/modules/nft/utils.ts b/webapp/src/modules/nft/utils.ts index 18b67b82e..6f8bd1ae8 100644 --- a/webapp/src/modules/nft/utils.ts +++ b/webapp/src/modules/nft/utils.ts @@ -20,11 +20,11 @@ export function getNFT(contractAddress: string | null, tokenId: string | null, n export const getBodyShapeUrn = (bodyShape: string) => `urn:decentraland:off-chain:base-avatars:${bodyShape}` -export function isGender(bodyShapes: BodyShape[], gender: BodyShape) { +export function isGender(bodyShapes: (BodyShape | string)[], gender: BodyShape): boolean { if (bodyShapes.length !== 1) { return false } - return bodyShapes[0] === gender || getBodyShapeUrn(bodyShapes[0]) === gender + return bodyShapes[0] === gender.toString() || getBodyShapeUrn(bodyShapes[0]) === gender.toString() } export function isUnisex(bodyShapes: BodyShape[]) { diff --git a/webapp/src/modules/routing/sagas.ts b/webapp/src/modules/routing/sagas.ts index 0be2f6253..331f3d4f8 100644 --- a/webapp/src/modules/routing/sagas.ts +++ b/webapp/src/modules/routing/sagas.ts @@ -62,7 +62,7 @@ import { GO_BACK, GoBackAction } from './actions' -import { BrowseOptions, Sections } from './types' +import { BrowseOptions } from './types' import { Section } from '../vendor/decentraland' import { getClearedBrowseOptions, isCatalogView, rentalFilters, SALES_PER_PAGE, sellFilters, buildBrowseURL } from './utils' import { FetchSalesFailureAction, fetchSalesRequest, FETCH_SALES_FAILURE, FETCH_SALES_SUCCESS } from '../sale/actions' @@ -83,6 +83,7 @@ import { ClaimNameSuccessAction, ClaimNameTransactionSubmittedAction } from '../ens/actions' +import { isOfEnumType } from '../../utils/enums' import { DCLRegistrar__factory } from '../../contracts/factories/DCLRegistrar__factory' import { REGISTRAR_ADDRESS } from '../ens/sagas' @@ -168,7 +169,7 @@ export function* fetchAssetsFromRoute(options: BrowseOptions) { const view = options.view! const vendor = options.vendor! const page = options.page! - const section = options.section! + const section = options.section && isOfEnumType(options.section, Section) ? options.section : undefined const sortBy = options.sortBy! const { search, @@ -193,7 +194,7 @@ export function* fetchAssetsFromRoute(options: BrowseOptions) { yield put(setView(view)) } - const category = getCategoryFromSection(section) + const category = section ? getCategoryFromSection(section) : undefined const currentPageInState: number = yield select(getPage) const offset = currentPageInState && currentPageInState < page ? page - 1 : 0 @@ -240,12 +241,12 @@ export function* fetchAssetsFromRoute(options: BrowseOptions) { ) break default: { - const isWearableHead = section === Sections[VendorName.DECENTRALAND].WEARABLES_HEAD - const isWearableAccessory = section === Sections[VendorName.DECENTRALAND].WEARABLES_ACCESSORIES + const isWearableHead = section === Section.WEARABLES_HEAD + const isWearableAccessory = section === Section.WEARABLES_ACCESSORIES - const wearableCategory = !isWearableAccessory ? getSearchWearableCategory(section) : undefined + const wearableCategory = !isWearableAccessory && section ? getSearchWearableCategory(section) : undefined - const emoteCategory = category === NFTCategory.EMOTE ? getSearchEmoteCategory(section) : undefined + const emoteCategory = category === NFTCategory.EMOTE && section ? getSearchEmoteCategory(section) : undefined const { rarities, wearableGenders, emotePlayMode } = options diff --git a/webapp/src/modules/routing/search.ts b/webapp/src/modules/routing/search.ts index 8e0b87b46..bd72fde3d 100644 --- a/webapp/src/modules/routing/search.ts +++ b/webapp/src/modules/routing/search.ts @@ -5,6 +5,7 @@ import { Section } from '../vendor/decentraland' import { NFTSortBy } from '../nft/types' import { isAccountView, isLandSection } from '../ui/utils' import { AssetStatusFilter } from '../../utils/filters' +import { isOfEnumType } from '../../utils/enums' import { AssetType } from '../asset/types' import { isCatalogView, isCatalogViewWithStatusFilter } from './utils' @@ -161,7 +162,11 @@ export function getSearchParams(options?: BrowseOptions) { return params } -export function getCategoryFromSection(section: string) { +export function getCategoryFromSection(section: string): NFTCategory | undefined { + if (!isOfEnumType(section, Section)) { + return undefined + } + switch (section) { case Section.PARCELS: return NFTCategory.PARCEL @@ -246,7 +251,11 @@ export function getSearchSection(category: WearableCategory | EmoteCategory) { } } -export function getSearchWearableCategory(section: string) { +export function getSearchWearableCategory(section: string): WearableCategory | undefined { + if (!isOfEnumType(section, Section)) { + return undefined + } + switch (section) { case Section.WEARABLES_EYEBROWS: return WearableCategory.EYEBROWS @@ -285,7 +294,11 @@ export function getSearchWearableCategory(section: string) { } } -export function getSearchEmoteCategory(section: string) { +export function getSearchEmoteCategory(section: string): EmoteCategory | undefined { + if (!isOfEnumType(section, Section)) { + return undefined + } + switch (section) { case Section.EMOTES_DANCE: return EmoteCategory.DANCE diff --git a/webapp/src/modules/routing/selectors.ts b/webapp/src/modules/routing/selectors.ts index 826a452cd..e64ea44a8 100644 --- a/webapp/src/modules/routing/selectors.ts +++ b/webapp/src/modules/routing/selectors.ts @@ -4,6 +4,7 @@ import { getSearch as getRouterSearch, getLocation } from 'connected-react-route import { EmotePlayMode, GenderFilterOption, Network, Rarity } from '@dcl/schemas' import { t } from 'decentraland-dapps/dist/modules/translation/utils' import { AssetStatusFilter } from '../../utils/filters' +import { isOfEnumType } from '../../utils/enums' import { getView } from '../ui/browse/selectors' import { View } from '../ui/types' import { VendorName } from '../vendor/types' @@ -53,7 +54,11 @@ export const getSection = createSelector { const SORT_BY_MAP = getAllSortByOptions() let orderByDropdownOptions: SortByOption[] = [] - if (status) { + if (status && isOfEnumType(status, AssetStatusFilter)) { const baseFilters = [ SORT_BY_MAP[SortBy.NEWEST], SORT_BY_MAP[SortBy.RECENTLY_LISTED], diff --git a/webapp/src/modules/store/utils.ts b/webapp/src/modules/store/utils.ts index 7f300b5e7..e88885c6f 100644 --- a/webapp/src/modules/store/utils.ts +++ b/webapp/src/modules/store/utils.ts @@ -6,6 +6,7 @@ import { BuildEntityWithoutFilesOptions } from 'dcl-catalyst-client/dist/client/ import { EntityContentItemReference } from 'dcl-catalyst-commons' import { peerUrl } from '../../lib/environment' import { convertToOutputString } from '../../utils/output' +import { isOfEnumType } from '../../utils/enums' import { LinkType, Store, StoreEntityMetadata } from './types' export const getPeerCoverUrl = (hash: string) => `${peerUrl}/content/contents/${hash}` @@ -48,7 +49,7 @@ export const getStoreFromEntity = (entity: Entity): Store => { coverName = reference.file } - const getLink = (type: LinkType) => metadata.links.find(link => link.name === type)?.url || '' + const getLink = (type: LinkType) => metadata.links.find(link => isOfEnumType(link.name, LinkType) && link.name === type)?.url || '' return { cover, diff --git a/webapp/src/modules/ui/sagas.ts b/webapp/src/modules/ui/sagas.ts index 7e01c858b..21139aa65 100644 --- a/webapp/src/modules/ui/sagas.ts +++ b/webapp/src/modules/ui/sagas.ts @@ -59,7 +59,7 @@ function* handleSetWearablePreviewController(action: SetWearablePreviewControlle try { while (true) { try { - const event: string = yield take(emotesChannel) + const event: PreviewEmoteEventType = yield take(emotesChannel) switch (event) { case PreviewEmoteEventType.ANIMATION_PLAY: yield put(setEmotePlaying(true)) diff --git a/webapp/src/modules/vendor/utils.ts b/webapp/src/modules/vendor/utils.ts index 6473e2284..327ecb8a6 100644 --- a/webapp/src/modules/vendor/utils.ts +++ b/webapp/src/modules/vendor/utils.ts @@ -18,7 +18,7 @@ export function getFilters(vendor: VendorName, options: BrowseOptions): NFTsFetc const isWearableHead = section === currentSection.WEARABLES_HEAD const isWearableAccessory = section === currentSection.WEARABLES_ACCESSORIES - const category = getCategoryFromSection(section!) + const category = section ? getCategoryFromSection(section) : undefined const wearableCategory = !isWearableAccessory && category === NFTCategory.WEARABLE ? getSearchWearableCategory(section!) : undefined const emoteCategory = category === NFTCategory.EMOTE ? getSearchEmoteCategory(section!) : undefined @@ -79,7 +79,7 @@ export function getOriginURL(vendor: VendorName) { } } -export function isVendor(vendor: string) { +export function isVendor(vendor: string): vendor is VendorName { return Object.values(VendorName).includes(vendor as VendorName) } diff --git a/webapp/src/utils/enums.spec.ts b/webapp/src/utils/enums.spec.ts new file mode 100644 index 000000000..7d1434130 --- /dev/null +++ b/webapp/src/utils/enums.spec.ts @@ -0,0 +1,65 @@ +import { isOfEnumType } from './enums' + +enum ANumericEnum { + A = 1, + B = 2, + C = 3 +} + +enum AnotherNumericEnum { + A = 4, + B = 5, + C = 6 +} + +enum StringEnum { + A = '1', + B = '2', + C = '3' +} + +enum AnotherStringEnum { + A = '4', + B = '5', + C = '6' +} + +describe('when checking if a value is of an enum type', () => { + let value: string | number + + describe('and the value is numeric', () => { + beforeEach(() => { + value = 1 + }) + + describe('and the value belongs to the enum', () => { + it('should return true', () => { + expect(isOfEnumType(value, ANumericEnum)).toBe(true) + }) + }) + + describe('and the value does not belong to the enum', () => { + it('should return false', () => { + expect(isOfEnumType(value, AnotherNumericEnum)).toBe(false) + }) + }) + }) + + describe('and the value is a string', () => { + beforeEach(() => { + value = '1' + }) + + describe('and the value belongs to the enum', () => { + it('should return true', () => { + expect(isOfEnumType(value, StringEnum)).toBe(true) + }) + }) + + describe('and the value does not belong to the enum', () => { + it('should return false', () => { + expect(isOfEnumType(value, AnotherStringEnum)).toBe(false) + }) + }) + }) +}) diff --git a/webapp/src/utils/enums.ts b/webapp/src/utils/enums.ts new file mode 100644 index 000000000..b6629adfc --- /dev/null +++ b/webapp/src/utils/enums.ts @@ -0,0 +1,7 @@ +type Enum = Record +type Keys = keyof Enum +type Values = E[Keys] + +export const isOfEnumType = >(value: unknown, enumObject: T): value is Values => { + return Object.values(enumObject).includes(value) +}