diff --git a/assets/images/companyCards/card-plaid.svg b/assets/images/companyCards/card-plaid.svg index c4ed528778ae6..7b9b4aeb4c630 100644 --- a/assets/images/companyCards/card-plaid.svg +++ b/assets/images/companyCards/card-plaid.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/assets/images/companyCards/large/card-plaid-large.svg b/assets/images/companyCards/large/card-plaid-large.svg index db61a2cd76ee4..8dd4ed29a80c8 100644 --- a/assets/images/companyCards/large/card-plaid-large.svg +++ b/assets/images/companyCards/large/card-plaid-large.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/components/PlaidCardFeedIcon.tsx b/src/components/PlaidCardFeedIcon.tsx index 0588315948501..813cff7b44f5b 100644 --- a/src/components/PlaidCardFeedIcon.tsx +++ b/src/components/PlaidCardFeedIcon.tsx @@ -13,9 +13,10 @@ type PlaidCardFeedIconProps = { plaidUrl: string; style?: StyleProp; isLarge?: boolean; + isSmall?: boolean; }; -function PlaidCardFeedIcon({plaidUrl, style, isLarge}: PlaidCardFeedIconProps) { +function PlaidCardFeedIcon({plaidUrl, style, isLarge, isSmall}: PlaidCardFeedIconProps) { const [isBrokenImage, setIsBrokenImage] = useState(false); const styles = useThemeStyles(); const illustrations = useThemeIllustrations(); @@ -23,6 +24,10 @@ function PlaidCardFeedIcon({plaidUrl, style, isLarge}: PlaidCardFeedIconProps) { const width = isLarge ? variables.cardPreviewWidth : variables.cardIconWidth; const height = isLarge ? variables.cardPreviewHeight : variables.cardIconHeight; const [loading, setLoading] = useState(true); + const plaidImageStyle = isLarge ? styles.plaidIcon : styles.plaidIconSmall; + const iconWidth = isSmall ? variables.cardMiniatureWidth : width; + const iconHeight = isSmall ? variables.cardMiniatureHeight : height; + const plaidLoadedStyle = isSmall ? styles.plaidIconExtraSmall : plaidImageStyle; useEffect(() => { if (!plaidUrl) { @@ -37,31 +42,32 @@ function PlaidCardFeedIcon({plaidUrl, style, isLarge}: PlaidCardFeedIconProps) { {isBrokenImage ? ( ) : ( <> setIsBrokenImage(true)} onLoadEnd={() => setLoading(false)} /> {loading ? ( - + ) : ( )} diff --git a/src/components/SelectionList/Search/CardListItem.tsx b/src/components/SelectionList/Search/CardListItem.tsx index 03a4245a90c41..4410bbe216efb 100644 --- a/src/components/SelectionList/Search/CardListItem.tsx +++ b/src/components/SelectionList/Search/CardListItem.tsx @@ -5,6 +5,7 @@ import Avatar from '@components/Avatar'; import Checkbox from '@components/Checkbox'; import Icon from '@components/Icon'; import {FallbackAvatar} from '@components/Icon/Expensicons'; +import PlaidCardFeedIcon from '@components/PlaidCardFeedIcon'; import BaseListItem from '@components/SelectionList/BaseListItem'; import type {BaseListItemProps, ListItem} from '@components/SelectionList/types'; import TextWithTooltip from '@components/TextWithTooltip'; @@ -18,7 +19,15 @@ import CONST from '@src/CONST'; import type {PersonalDetails} from '@src/types/onyx'; import type {BankIcon} from '@src/types/onyx/Bank'; -type AdditionalCardProps = {shouldShowOwnersAvatar?: boolean; cardOwnerPersonalDetails?: PersonalDetails; bankIcon?: BankIcon; lastFourPAN?: string; isVirtual?: boolean; cardName?: string}; +type AdditionalCardProps = { + shouldShowOwnersAvatar?: boolean; + cardOwnerPersonalDetails?: PersonalDetails; + bankIcon?: BankIcon; + lastFourPAN?: string; + isVirtual?: boolean; + cardName?: string; + plaidUrl?: string; +}; type CardListItemProps = BaseListItemProps; function CardListItem({ @@ -102,21 +111,34 @@ function CardListItem({ - + {!!item?.plaidUrl && ( + + )} + {!item?.plaidUrl && ( + + )} ) : ( - + <> + {!!item?.plaidUrl && } + {!item?.plaidUrl && ( + + )} + )} )} diff --git a/src/libs/API/parameters/OpenPlaidCompanyCardLoginParams.ts b/src/libs/API/parameters/OpenPlaidCompanyCardLoginParams.ts index b4d62d78ea526..80720eb37e878 100644 --- a/src/libs/API/parameters/OpenPlaidCompanyCardLoginParams.ts +++ b/src/libs/API/parameters/OpenPlaidCompanyCardLoginParams.ts @@ -2,6 +2,8 @@ type OpenPlaidCompanyCardLoginParams = { redirectURI: string | undefined; androidPackage?: string; country: string; + domain?: string; + feed?: string; }; export default OpenPlaidCompanyCardLoginParams; diff --git a/src/libs/CardFeedUtils.ts b/src/libs/CardFeedUtils.ts index 9057109466fa6..2cc74238ed3f2 100644 --- a/src/libs/CardFeedUtils.ts +++ b/src/libs/CardFeedUtils.ts @@ -1,4 +1,4 @@ -import type {OnyxCollection} from 'react-native-onyx/dist/types'; +import type {OnyxCollection} from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {AdditionalCardProps} from '@components/SelectionList/Search/CardListItem'; import type IllustrationsType from '@styles/theme/illustrations/types'; @@ -6,11 +6,21 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Card, CardFeeds, CardList, CompanyCardFeed, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {getBankName, getCardFeedIcon, getCompanyFeeds, getCustomOrFormattedFeedName, isCard, isCardClosed, isCardHiddenFromSearch} from './CardUtils'; +import { + getBankName, + getCardFeedIcon, + getCompanyFeeds, + getCustomOrFormattedFeedName, + getPlaidInstitutionIconUrl, + getPlaidInstitutionId, + isCard, + isCardClosed, + isCardHiddenFromSearch, +} from './CardUtils'; import {getDescriptionForPolicyDomainCard, getPolicy} from './PolicyUtils'; import type {OptionData} from './ReportUtils'; -type CardFilterItem = Partial & AdditionalCardProps & {isCardFeed?: boolean; correspondingCards?: string[]; cardFeedKey: string}; +type CardFilterItem = Partial & AdditionalCardProps & {isCardFeed?: boolean; correspondingCards?: string[]; cardFeedKey: string; plaidUrl?: string}; type DomainFeedData = {bank: string; domainName: string; correspondingCardIDs: string[]; fundID?: string}; type ItemsGroupedBySelection = {selected: CardFilterItem[]; unselected: CardFilterItem[]}; type CardFeedNamesWithType = Record; @@ -74,6 +84,7 @@ function createCardFilterItem(card: Card, personalDetailsList: PersonalDetailsLi const icon = getCardFeedIcon(card?.bank as CompanyCardFeed, illustrations); const cardName = card?.nameValuePairs?.cardTitle; const text = personalDetails?.displayName ?? cardName; + const plaidUrl = getPlaidInstitutionIconUrl(card?.bank); return { lastFourPAN: card.lastFourPAN, @@ -82,6 +93,7 @@ function createCardFilterItem(card: Card, personalDetailsList: PersonalDetailsLi cardName, cardOwnerPersonalDetails: personalDetails ?? undefined, text, + plaidUrl, keyForList: card.cardID.toString(), isSelected, bankIcon: { @@ -101,7 +113,7 @@ function buildCardsData( isClosedCards = false, ): ItemsGroupedBySelection { // Filter condition to build different cards data for closed cards and individual cards based on the isClosedCards flag, we don't want to show closed cards in the individual cards section - const filterCondition = (card: Card) => (isClosedCards ? isCardClosed(card) : !isCardHiddenFromSearch(card) && !isCardClosed(card)); + const filterCondition = (card: Card) => (isClosedCards ? isCardClosed(card) : !isCardHiddenFromSearch(card) && !isCardClosed(card) && isCard(card)); const userAssignedCards: CardFilterItem[] = Object.values(userCardList ?? {}) .filter((card) => filterCondition(card)) .map((card) => createCardFilterItem(card, personalDetailsList, selectedCards, illustrations)); @@ -170,21 +182,24 @@ function getWorkspaceCardFeedData(cardFeed: WorkspaceCardsList | undefined, repe if (!representativeCard || !cardFeedArray.some((cardFeedItem) => isCard(cardFeedItem) && !isCardHiddenFromSearch(cardFeedItem))) { return; } - const {domainName, bank} = representativeCard; + const {domainName, bank, cardName} = representativeCard; const isBankRepeating = repeatingBanks.includes(bank); const policyID = domainName.match(CONST.REGEX.EXPENSIFY_POLICY_DOMAIN_NAME)?.[1] ?? ''; // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation const correspondingPolicy = getPolicy(policyID?.toUpperCase()); const cardFeedLabel = isBankRepeating ? correspondingPolicy?.name : undefined; - const cardFeedBankName = bank === CONST.EXPENSIFY_CARD.BANK ? translate('search.filters.card.expensify') : getBankName(bank as CompanyCardFeed); - const cardName = + const isPlaid = !!getPlaidInstitutionId(bank); + const companyCardBank = isPlaid && cardName ? cardName : getBankName(bank as CompanyCardFeed); + + const cardFeedBankName = bank === CONST.EXPENSIFY_CARD.BANK ? translate('search.filters.card.expensify') : companyCardBank; + const fullCardName = cardFeedBankName === CONST.COMPANY_CARDS.CARD_TYPE.CSV ? translate('search.filters.card.cardFeedNameCSV', {cardFeedLabel}) : translate('search.filters.card.cardFeedName', {cardFeedBankName, cardFeedLabel}); return { - cardName, + cardName: fullCardName, bank, label: cardFeedLabel, type: 'workspace', @@ -276,6 +291,7 @@ function createCardFeedItem({ illustrations: IllustrationsType; }): CardFilterItem { const isSelected = correspondingCardIDs.every((card) => selectedCards.includes(card)); + const plaidUrl = getPlaidInstitutionIconUrl(bank); const icon = getCardFeedIcon(bank as CompanyCardFeed, illustrations); return { @@ -286,6 +302,7 @@ function createCardFeedItem({ bankIcon: { icon, }, + plaidUrl, cardFeedKey, isCardFeed: true, correspondingCards: correspondingCardIDs, diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 2367d785f194a..f0c64ff026421 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -787,6 +787,39 @@ function buildUserReadableQueryString( operator: queryFilter.at(0)?.operator ?? CONST.SEARCH.SYNTAX_OPERATORS.AND, value: taxRate, })); + } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.FEED) { + displayQueryFilters = queryFilter.reduce((acc, filter) => { + const feedKey = filter.value.toString(); + const cardFeedsForDisplay = getCardFeedsForDisplay(cardFeeds, cardList); + const plaidFeedName = feedKey?.split(CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID)?.at(1); + const regularBank = feedKey?.split('_')?.at(1) ?? CONST.DEFAULT_NUMBER_ID; + const idPrefix = feedKey?.split('_')?.at(0) ?? CONST.DEFAULT_NUMBER_ID; + const plaidValue = cardFeedsForDisplay[`${idPrefix}_${CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID}${plaidFeedName}` as OnyxTypes.CompanyCardFeed]?.name; + if (plaidFeedName) { + if (plaidValue) { + acc.push({operator: filter.operator, value: plaidValue}); + } + return acc; + } + const value = cardFeedsForDisplay[`${idPrefix}_${regularBank}` as OnyxTypes.CompanyCardFeed]?.name ?? feedKey; + acc.push({operator: filter.operator, value}); + + return acc; + }, [] as QueryFilter[]); + } else if (key === CONST.SEARCH.SYNTAX_FILTER_KEYS.CARD_ID) { + displayQueryFilters = queryFilter.reduce((acc, filter) => { + const cardValue = filter.value.toString(); + const cardID = parseInt(cardValue, 10); + + if (cardList?.[cardID]) { + if (Number.isNaN(cardID)) { + acc.push({operator: filter.operator, value: cardID}); + } else { + acc.push({operator: filter.operator, value: getCardDescription(cardID, cardList) || cardID}); + } + } + return acc; + }, [] as QueryFilter[]); } else { displayQueryFilters = queryFilter.map((filter) => ({ operator: filter.operator, diff --git a/src/libs/actions/Plaid.ts b/src/libs/actions/Plaid.ts index 3d1f75fc01815..8190a1fcf9afa 100644 --- a/src/libs/actions/Plaid.ts +++ b/src/libs/actions/Plaid.ts @@ -47,13 +47,15 @@ function openPlaidBankLogin(allowDebit: boolean, bankAccountID: number) { /** * Gets the Plaid Link token used to initialize the Plaid SDK for Company card */ -function openPlaidCompanyCardLogin(country: string) { +function openPlaidCompanyCardLogin(country: string, domain?: string, feed?: string) { const {redirectURI, androidPackage} = getPlaidLinkTokenParameters(); const params: OpenPlaidCompanyCardLoginParams = { redirectURI, androidPackage, country, + domain, + feed, }; const optimisticData = [ diff --git a/src/libs/actions/getCompanyCardBankConnection/index.tsx b/src/libs/actions/getCompanyCardBankConnection/index.tsx index fbecc1a68106d..b593774f7f0d4 100644 --- a/src/libs/actions/getCompanyCardBankConnection/index.tsx +++ b/src/libs/actions/getCompanyCardBankConnection/index.tsx @@ -23,7 +23,7 @@ type CompanyCardPlaidConnection = { plaidAccounts: string; }; -function getCompanyCardBankConnection(policyID?: string, bankName?: string) { +function getCompanyCardBankConnection(policyID?: string, bankName?: string | null) { const bankConnection = Object.keys(CONST.COMPANY_CARDS.BANKS).find((key) => CONST.COMPANY_CARDS.BANKS[key as keyof typeof CONST.COMPANY_CARDS.BANKS] === bankName); if (!bankName || !bankConnection || !policyID) { diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 600d14f770001..6ae17dd42b04d 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -130,7 +130,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti return; } if (isPlaid) { - onImportPlaidAccounts(); return; } if (url) { diff --git a/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx b/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx index 264e8e8a93bbb..68e1cb1950b0e 100644 --- a/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx +++ b/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useEffect, useRef} from 'react'; -import {ActivityIndicator, View} from 'react-native'; +import {ActivityIndicator, InteractionManager, View} from 'react-native'; import type {LinkSuccessMetadata} from 'react-native-plaid-link-sdk'; import type {PlaidLinkOnSuccessMetadata} from 'react-plaid-link/src/types'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -16,16 +16,17 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setAddNewCompanyCardStepAndData, setAssignCardStepAndData} from '@libs/actions/CompanyCards'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import Log from '@libs/Log'; +import {getDomainNameForPolicy} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import {handleRestrictedEvent} from '@userActions/App'; import {setPlaidEvent} from '@userActions/BankAccounts'; -import {openPlaidCompanyCardLogin} from '@userActions/Plaid'; +import {importPlaidAccounts, openPlaidCompanyCardLogin} from '@userActions/Plaid'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CompanyCardFeed} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -function PlaidConnectionStep({feed}: {feed?: CompanyCardFeed}) { +function PlaidConnectionStep({feed, policyID}: {feed?: CompanyCardFeed; policyID?: string}) { const {isDevelopment} = useEnvironment(); const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -41,6 +42,7 @@ function PlaidConnectionStep({feed}: {feed?: CompanyCardFeed}) { // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const plaidDataErrorMessage = !isEmptyObject(plaidErrors) ? (Object.values(plaidErrors).at(0) as string) : ''; const {isOffline} = useNetwork(); + const domain = getDomainNameForPolicy(policyID); // s77rt remove DEV lock const shouldSelectStatementCloseDate = isDevelopment; @@ -81,7 +83,7 @@ function PlaidConnectionStep({feed}: {feed?: CompanyCardFeed}) { return unsubscribeToNavigationShortcuts; } if (addNewCard?.data?.selectedCountry) { - openPlaidCompanyCardLogin(addNewCard.data.selectedCountry); + openPlaidCompanyCardLogin(addNewCard.data.selectedCountry, domain, feed); return unsubscribeToNavigationShortcuts; } @@ -93,10 +95,10 @@ function PlaidConnectionStep({feed}: {feed?: CompanyCardFeed}) { // If we are coming back from offline and we haven't authenticated with Plaid yet, we need to re-run our call to kick off Plaid // previousNetworkState.current also makes sure that this doesn't run on the first render. if (previousNetworkState.current && !isOffline && !isAuthenticatedWithPlaid() && addNewCard?.data?.selectedCountry) { - openPlaidCompanyCardLogin(addNewCard.data.selectedCountry); + openPlaidCompanyCardLogin(addNewCard.data.selectedCountry, domain, feed); } previousNetworkState.current = isOffline; - }, [addNewCard?.data?.selectedCountry, isAuthenticatedWithPlaid, isOffline]); + }, [addNewCard?.data?.selectedCountry, domain, feed, isAuthenticatedWithPlaid, isOffline]); const handleBackButtonPress = () => { if (feed) { @@ -124,6 +126,28 @@ function PlaidConnectionStep({feed}: {feed?: CompanyCardFeed}) { (metadata?.institution as PlaidLinkOnSuccessMetadata['institution'])?.name ?? (metadata?.institution as LinkSuccessMetadata['institution'])?.name; if (feed) { + if (plaidConnectedFeed && addNewCard?.data?.selectedCountry && plaidConnectedFeedName) { + importPlaidAccounts( + publicToken, + plaidConnectedFeed, + plaidConnectedFeedName, + addNewCard.data.selectedCountry, + getDomainNameForPolicy(policyID), + JSON.stringify(metadata?.accounts), + ); + InteractionManager.runAfterInteractions(() => { + setAssignCardStepAndData({ + data: { + plaidAccessToken: publicToken, + institutionId: plaidConnectedFeed, + plaidConnectedFeedName, + plaidAccounts: metadata?.accounts, + }, + currentStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION, + }); + }); + return; + } setAssignCardStepAndData({ data: { plaidAccessToken: publicToken, diff --git a/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx b/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx index a5c72b8ce9509..828f96ea41c1b 100644 --- a/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx +++ b/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx @@ -32,7 +32,7 @@ function SelectBankStep() { const {isBetaEnabled} = usePermissions(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true}); - const [bankSelected, setBankSelected] = useState>(); + const [bankSelected, setBankSelected] = useState | null>(); const [hasError, setHasError] = useState(false); const isOtherBankSelected = bankSelected === CONST.COMPANY_CARDS.BANKS.OTHER; @@ -65,7 +65,7 @@ function SelectBankStep() { return; } if (isBetaEnabled(CONST.BETAS.PLAID_COMPANY_CARDS)) { - setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_FEED_TYPE}); + setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_FEED_TYPE, data: {selectedBank: null}}); } else { Navigation.goBack(); } diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index f95dd37fa3ebb..ade4bc367b089 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -60,7 +60,12 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { /> ); case CONST.COMPANY_CARD.STEP.PLAID_CONNECTION: - return ; + return ( + + ); case CONST.COMPANY_CARD.STEP.ASSIGNEE: return ( top: 4, }, + plaidIconExtraSmall: { + height: variables.iconSizeXSmall, + width: variables.iconSizeXSmall, + position: 'absolute', + right: 1, + zIndex: 1, + top: 1, + }, + walletCardNumber: { color: theme.text, fontSize: variables.fontSizeNormal, diff --git a/src/types/form/SearchAdvancedFiltersForm.ts b/src/types/form/SearchAdvancedFiltersForm.ts index 4227f7da0b1d3..ebb137001ba5f 100644 --- a/src/types/form/SearchAdvancedFiltersForm.ts +++ b/src/types/form/SearchAdvancedFiltersForm.ts @@ -82,6 +82,7 @@ const ALLOWED_TYPE_FILTERS = { FILTER_KEYS.PAYER, FILTER_KEYS.DESCRIPTION, FILTER_KEYS.CARD_ID, + FILTER_KEYS.FEED, FILTER_KEYS.POSTED_AFTER, FILTER_KEYS.POSTED_BEFORE, FILTER_KEYS.POSTED_ON, @@ -124,6 +125,7 @@ const ALLOWED_TYPE_FILTERS = { FILTER_KEYS.PAYER, FILTER_KEYS.DESCRIPTION, FILTER_KEYS.CARD_ID, + FILTER_KEYS.FEED, FILTER_KEYS.POSTED_AFTER, FILTER_KEYS.POSTED_BEFORE, FILTER_KEYS.POSTED_ON, @@ -162,6 +164,7 @@ const ALLOWED_TYPE_FILTERS = { FILTER_KEYS.PAYER, FILTER_KEYS.DESCRIPTION, FILTER_KEYS.CARD_ID, + FILTER_KEYS.FEED, FILTER_KEYS.POSTED_AFTER, FILTER_KEYS.POSTED_BEFORE, FILTER_KEYS.POSTED_ON, diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts index df6ca03425c4a..88ecfb9093c36 100644 --- a/src/types/onyx/CardFeeds.ts +++ b/src/types/onyx/CardFeeds.ts @@ -137,7 +137,7 @@ type AddNewCardFeedData = { cardTitle: string; /** Selected bank */ - selectedBank: ValueOf; + selectedBank: ValueOf | null; /** Selected feed type */ selectedFeedType: ValueOf;