diff --git a/src/components/home-card/HomeCard.tsx b/src/components/home-card/HomeCard.tsx index 46d5ee6b02..771dc0b979 100644 --- a/src/components/home-card/HomeCard.tsx +++ b/src/components/home-card/HomeCard.tsx @@ -41,6 +41,7 @@ interface HomeCardProps { footer?: ReactNode; onCTAPress?: () => void; backgroundImg?: () => ReactElement; + cardHeight?: number; } const CardBodyHeader = styled(BaseText)` @@ -98,8 +99,14 @@ export const NeedBackupText = styled(BaseText)` export const HOME_CARD_HEIGHT = 143; export const HOME_CARD_WIDTH = 170; - -const HomeCard: React.FC = ({body, footer, onCTAPress}) => { +export const IMPORT_CARD_HEIGHT = 185; + +const HomeCard: React.FC = ({ + body, + footer, + onCTAPress, + cardHeight = HOME_CARD_HEIGHT, +}) => { const {t} = useTranslation(); const theme = useTheme(); const { @@ -116,6 +123,9 @@ const HomeCard: React.FC = ({body, footer, onCTAPress}) => { const BodyComp = ( + {cardHeight > HOME_CARD_HEIGHT && ( + + )} {title && {title}} {needsBackup && !pendingTssSession ? ( @@ -164,7 +174,7 @@ const HomeCard: React.FC = ({body, footer, onCTAPress}) => { backgroundColor: theme.dark ? CharcoalBlack : White, borderColor: theme.dark ? LightBlack : Slate30, borderWidth: 1, - height: HOME_CARD_HEIGHT, + height: cardHeight, width: HOME_CARD_WIDTH, }} /> diff --git a/src/managers/OngoingProcessManager.tsx b/src/managers/OngoingProcessManager.tsx index 5577a5e977..c1270e148b 100644 --- a/src/managers/OngoingProcessManager.tsx +++ b/src/managers/OngoingProcessManager.tsx @@ -110,6 +110,20 @@ const getCount = (data?: OngoingProcessData): number => ? data : 0; +export const getOngoingProcessMessage = ( + key: OnGoingProcessMessages, + data?: OngoingProcessData, +): string => translations[key]?.(data) ?? i18n.t('Loading'); + +export const IMPORT_PROGRESS_VISIBLE_EVENTS: readonly string[] = [ + 'findingCopayers', + 'foundCopayers', + 'gettingStatuses', + 'gatheringWalletsInfos', + 'walletInfo.gatheringTokens', + 'IMPORT_SCANNING_FUNDS', +]; + const translations: Record< OnGoingProcessMessages, (data?: OngoingProcessData) => string diff --git a/src/navigation/tabs/home/HomeRoot.tsx b/src/navigation/tabs/home/HomeRoot.tsx index f490ea0f0c..350469d083 100644 --- a/src/navigation/tabs/home/HomeRoot.tsx +++ b/src/navigation/tabs/home/HomeRoot.tsx @@ -8,6 +8,8 @@ import { ScrollView, View, } from 'react-native'; +import styled from 'styled-components/native'; +import {BaseText} from '../../../components/styled/Text'; import { EXCHANGE_RATES_CURRENCIES, STATIC_CONTENT_CARDS_ENABLED, @@ -111,6 +113,8 @@ const HomeRoot: React.FC = ({route, navigation}) => { ); const showPortfolioValue = useAppSelector(selectShowPortfolioValue); const portfolioChartsRequested = showPortfolioValue === true; + const pendingImport = useAppSelector(({APP}) => APP.pendingImport); + const importIsFirstKey = useAppSelector(({APP}) => APP.importIsFirstKey); const hasKeys = Object.values(keys).length; const portfolioAllocationTotalFiat = useMemo(() => { @@ -403,36 +407,42 @@ const HomeRoot: React.FC = ({route, navigation}) => { /> }> {/* ////////////////////////////// PORTFOLIO BALANCE */} - + {/* ////////////////////////////// CTA BUY SWAP RECEIVE SEND BUTTONS */} - {hasKeys ? ( - - { - dispatch( - Analytics.track('Clicked Receive Crypto', { - context: 'HomeRoot', - }), - ); - dispatch(receiveCrypto(navigation, 'HomeRoot')); - }, - }} - send={{ - cta: () => { - dispatch( - Analytics.track('Clicked Send Crypto', { - context: 'HomeRoot', - }), - ); - dispatch(sendCrypto('HomeRoot')); - }, - }} - /> - + {hasKeys || (pendingImport && importIsFirstKey) ? ( + + + { + dispatch( + Analytics.track('Clicked Receive Crypto', { + context: 'HomeRoot', + }), + ); + dispatch(receiveCrypto(navigation, 'HomeRoot')); + }, + }} + send={{ + cta: () => { + dispatch( + Analytics.track('Clicked Send Crypto', { + context: 'HomeRoot', + }), + ); + dispatch(sendCrypto('HomeRoot')); + }, + }} + /> + + ) : null} {/* ////////////////////////////// MARKETING */} @@ -444,13 +454,13 @@ const HomeRoot: React.FC = ({route, navigation}) => { {/* ////////////////////////////// CRYPTO */} - + {/* ////////////////////////////// SECURE WITH PASSKEY */} - {hasKeys ? ( + {hasKeys || (pendingImport && importIsFirstKey) ? ( = ({route, navigation}) => { ) : null} - {showPortfolioValue && showPortfolioAllocationSection ? ( + {showPortfolioValue && + (showPortfolioAllocationSection || + (pendingImport && importIsFirstKey)) ? ( diff --git a/src/navigation/tabs/home/components/AllocationSection.tsx b/src/navigation/tabs/home/components/AllocationSection.tsx index b2ad249d18..a5a63df9e9 100644 --- a/src/navigation/tabs/home/components/AllocationSection.tsx +++ b/src/navigation/tabs/home/components/AllocationSection.tsx @@ -444,6 +444,8 @@ const AllocationSection: React.FC = () => { const keys = useAppSelector(({WALLET}) => WALLET.keys) as Record; const {defaultAltCurrency} = useAppSelector(({APP}) => APP); const homeCarouselConfig = useAppSelector(({APP}) => APP.homeCarouselConfig); + const pendingImport = useAppSelector(({APP}) => APP.pendingImport); + const importIsFirstKey = useAppSelector(({APP}) => APP.importIsFirstKey); const visibleWallets = useMemo( () => getVisibleWalletsFromKeys(keys, homeCarouselConfig), @@ -485,7 +487,10 @@ const AllocationSection: React.FC = () => { diff --git a/src/navigation/tabs/home/components/AssetRow.tsx b/src/navigation/tabs/home/components/AssetRow.tsx index b1a9e9700b..ede230ff52 100644 --- a/src/navigation/tabs/home/components/AssetRow.tsx +++ b/src/navigation/tabs/home/components/AssetRow.tsx @@ -182,7 +182,8 @@ const AssetRow: React.FC = ({ const [loadingDelayElapsed, setLoadingDelayElapsed] = useState(false); const preservedEntry = lastSettledItemRef.current; const preservedItem = - preservedEntry?.presentationResetToken === presentationResetToken + preservedEntry !== undefined && + preservedEntry.presentationResetToken === presentationResetToken ? preservedEntry.item : undefined; diff --git a/src/navigation/tabs/home/components/AssetsSection.tsx b/src/navigation/tabs/home/components/AssetsSection.tsx index 37b8dd0831..b944b2b475 100644 --- a/src/navigation/tabs/home/components/AssetsSection.tsx +++ b/src/navigation/tabs/home/components/AssetsSection.tsx @@ -92,6 +92,8 @@ const AssetsSection: React.FC = ({enabled = true}) => { const defaultAltCurrency = useAppSelector(({APP}) => APP.defaultAltCurrency); const showPortfolioValue = useAppSelector(selectShowPortfolioValue); const homeCarouselConfig = useAppSelector(({APP}) => APP.homeCarouselConfig); + const pendingImport = useAppSelector(({APP}) => APP.pendingImport); + const importIsFirstKey = useAppSelector(({APP}) => APP.importIsFirstKey); const keys = useAppSelector(({WALLET}) => WALLET.keys) as Record; const focusRefreshToken = useScreenFocusRefreshToken(); const portfolioChartsEnabled = showPortfolioValue === true; @@ -236,10 +238,11 @@ const AssetsSection: React.FC = ({enabled = true}) => { visibleItems, ]); const shouldShowActivationPlaceholder = - portfolioChartsEnabled && - hasAnyVisibleWalletBalance && - !items.length && - (!!visibleWallets.length || !!portfolio.populateStatus?.inProgress); + (pendingImport && importIsFirstKey) || + (portfolioChartsEnabled && + hasAnyVisibleWalletBalance && + !items.length && + (!!visibleWallets.length || !!portfolio.populateStatus?.inProgress)); if (shouldShowActivationPlaceholder) { return ( diff --git a/src/navigation/tabs/home/components/Crypto.tsx b/src/navigation/tabs/home/components/Crypto.tsx index a30ada508f..4559e787c7 100644 --- a/src/navigation/tabs/home/components/Crypto.tsx +++ b/src/navigation/tabs/home/components/Crypto.tsx @@ -1,5 +1,9 @@ -import {NavigationProp, useNavigation} from '@react-navigation/native'; -import React, {ReactElement, useEffect, useMemo, useState} from 'react'; +import { + NavigationProp, + useIsFocused, + useNavigation, +} from '@react-navigation/native'; +import React, {ReactElement, useEffect, useMemo, useRef, useState} from 'react'; import Carousel from 'react-native-reanimated-carousel'; import styled from 'styled-components/native'; import { @@ -14,6 +18,8 @@ import WalletCardComponent from './Wallet'; import {BottomNotificationConfig} from '../../../../components/modal/bottom-notification/BottomNotification'; import { dismissDecryptPasswordModal, + setImportProgress, + setPendingImport, showBottomNotificationModal, showDecryptPasswordModal, } from '../../../../store/app/app.actions'; @@ -41,7 +47,8 @@ import { HomeSectionTitle, SectionHeaderContainer, } from './Styled'; -import {View} from 'react-native'; +import {ActivityIndicator, Animated, ScrollView, View} from 'react-native'; +import {Action} from '../../../../styles/colors'; import {TouchableOpacity} from '@components/base/TouchableOpacity'; import CustomizeSvg from './CustomizeSvg'; import haptic from '../../../../components/haptic-feedback/haptic'; @@ -50,6 +57,7 @@ import CoinbaseBalanceCard from '../../../coinbase/components/CoinbaseBalanceCar import { HOME_CARD_HEIGHT, HOME_CARD_WIDTH, + IMPORT_CARD_HEIGHT, } from '../../../../components/home-card/HomeCard'; import { buildLegacyLastDayRateRequestsForWallets, @@ -72,6 +80,8 @@ import {isTSSKey} from '../../../../store/wallet/effects/tss-send/tss-send'; import {logManager} from '../../../../managers/LogManager'; import {WalletScreens} from '../../../../navigation/wallet/WalletGroup'; import {IsVMChain} from '../../../../store/wallet/utils/currency'; +import ListKeySkeleton from './cards/ListKeySkeleton'; +import CarouselKeySkeleton from './cards/CarouselKeySkeleton'; //import {ConnectLedgerNanoXCard} from './cards/ConnectLedgerNanoX'; const CryptoContainer = styled.View` @@ -99,6 +109,18 @@ const NoKeysSectionHeaderContainer = styled(SectionHeaderContainer)` margin-bottom: 0px; `; +const MountFade = ({children}: {children: React.ReactNode}) => { + const opacity = useRef(new Animated.Value(0)).current; + useEffect(() => { + Animated.timing(opacity, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }).start(); + }, [opacity]); + return {children}; +}; + const NoKeysButtonWrapper = styled.View` margin-bottom: 15px; `; @@ -240,6 +262,7 @@ export const createHomeCardList = ({ context, onPress, currency, + cardHeight, }: { navigation: any; keys: Key[]; @@ -257,6 +280,7 @@ export const createHomeCardList = ({ context?: 'keySelector'; onPress?: (currency: any, selectedKey: Key) => any; currency?: any; + cardHeight?: number; }) => { let list: {id: string; component: ReactElement}[] = []; const defaults: {id: string; component: ReactElement}[] = []; @@ -311,6 +335,7 @@ export const createHomeCardList = ({ needsBackup={!backupComplete} context={context} pendingTssSession={hasPendingTssSession} + cardHeight={cardHeight} onPress={ onPress ? () => { @@ -397,11 +422,17 @@ export const createHomeCardList = ({ }; }; -const Crypto = () => { +interface CryptoProps { + scrollRef?: React.RefObject; +} + +const Crypto = ({scrollRef}: CryptoProps) => { const {t: translate} = useTranslation(); const navigation = useNavigation(); + const isFocused = useIsFocused(); + const skeletonRef = useRef(null); const dispatch = useAppDispatch(); - const keys = useAppSelector(({WALLET}) => WALLET.keys); + const keys = useAppSelector(({WALLET}) => WALLET.keys) as Record; const homeCarouselConfig = useAppSelector(({APP}) => APP.homeCarouselConfig); const linkedCoinbase = useAppSelector( ({COINBASE}) => !!COINBASE.token[COINBASE_ENV], @@ -417,6 +448,9 @@ const Crypto = () => { const hasCompletedFullPortfolioPopulate = useAppSelector( selectHasCompletedFullPortfolioPopulate, ); + const importMessage = useAppSelector(({APP}) => APP.importBannerMessage); + const pendingImport = useAppSelector(({APP}) => APP.pendingImport); + const importProgress = useAppSelector(({APP}) => APP.importProgress); const portfolioChartsRequested = showPortfolioValue === true; const portfolioChartsEnabled = portfolioChartsRequested && hasCompletedFullPortfolioPopulate; @@ -485,6 +519,83 @@ const Crypto = () => { const portfolioPopulateStatus = portfolioChartsEnabled ? portfolio?.populateStatus : undefined; + + const skeletonOpacity = useRef( + new Animated.Value(importMessage ? 1 : 0), + ).current; + const [skeletonVisible, setSkeletonVisible] = useState(!!importMessage); + const prevImportMessage = useRef(importMessage); + + useEffect(() => { + const wasShowing = !!prevImportMessage.current; + const isShowing = !!importMessage; + prevImportMessage.current = importMessage; + + if (!wasShowing && isShowing) { + setSkeletonVisible(true); + skeletonOpacity.setValue(0); + Animated.timing(skeletonOpacity, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }).start(); + } else if (wasShowing && !isShowing) { + Animated.timing(skeletonOpacity, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }).start(() => setSkeletonVisible(false)); + } + }, [importMessage, skeletonOpacity]); + + useEffect(() => { + if ( + !isFocused || + (!skeletonVisible && !pendingImport) || + homeCarouselLayoutType !== 'listView' || + !scrollRef?.current || + !skeletonRef.current + ) { + return; + } + const timer = setTimeout(() => { + if (!scrollRef.current || !skeletonRef.current) { + return; + } + (scrollRef.current as any).measure( + (_x: number, _y: number, _w: number, scrollViewHeight: number) => { + skeletonRef.current?.measureLayout( + scrollRef.current as any, + ( + _sx: number, + skeletonY: number, + _sw: number, + skeletonHeight: number, + ) => { + const scrollY = + skeletonY + skeletonHeight - scrollViewHeight + 20; + scrollRef.current?.scrollTo({ + y: Math.max(0, scrollY), + animated: true, + }); + }, + () => {}, + ); + }, + ); + }, 200); + return () => clearTimeout(timer); + }, [ + isFocused, + skeletonVisible, + pendingImport, + homeCarouselLayoutType, + scrollRef, + ]); + + const activeCardHeight = + skeletonVisible || pendingImport ? IMPORT_CARD_HEIGHT : HOME_CARD_HEIGHT; + const [cardsList, setCardsList] = useState( createHomeCardList({ navigation, @@ -498,6 +609,7 @@ const Crypto = () => { portfolioPercentageDifferenceByKey: visiblePortfolioPercentageDifferenceByKey, populateStatus: portfolioPopulateStatus, + cardHeight: activeCardHeight, }), ); @@ -515,6 +627,7 @@ const Crypto = () => { portfolioPercentageDifferenceByKey: visiblePortfolioPercentageDifferenceByKey, populateStatus: portfolioPopulateStatus, + cardHeight: activeCardHeight, }), ); }, [ @@ -528,9 +641,10 @@ const Crypto = () => { legacyPercentageDifferenceByKey, portfolioPopulateStatus, visiblePortfolioPercentageDifferenceByKey, + activeCardHeight, ]); - if (!hasKeys && !linkedCoinbase) { + if (!hasKeys && !linkedCoinbase && !importMessage && !pendingImport) { return ( @@ -590,11 +704,20 @@ const Crypto = () => { activeOpacity={ActiveOpacity} testID="my-crypto-add-button" accessibilityLabel="Add crypto wallet" + disabled={pendingImport} onPress={() => { haptic('soft'); navigation.navigate('CreationOptions'); }}> - + {pendingImport ? ( + + ) : ( + + )} { {homeCarouselLayoutType === 'carousel' ? ( + + + ), + }, + ] + : pendingImport + ? [ + { + id: '__import_skeleton__', + component: ( + { + dispatch(setPendingImport(false)); + dispatch(setImportProgress(0)); + navigation.navigate(WalletScreens.IMPORT, {}); + }} + /> + ), + }, + ] + : []), + ...cardsList.list, + ]} scrollAnimationDuration={0} renderItem={_renderItem} enabled={true} @@ -632,8 +792,30 @@ const Crypto = () => { ) : ( {cardsList.list.map(data => { - return {data.component}; + return {data.component}; })} + {skeletonVisible || pendingImport ? ( + + {skeletonVisible ? ( + + + + ) : ( + { + dispatch(setPendingImport(false)); + dispatch(setImportProgress(0)); + navigation.navigate(WalletScreens.IMPORT, {}); + }} + /> + )} + + ) : null} )} diff --git a/src/navigation/tabs/home/components/PortfolioBalance.tsx b/src/navigation/tabs/home/components/PortfolioBalance.tsx index 62ac3d60fc..4f66aa6f2f 100644 --- a/src/navigation/tabs/home/components/PortfolioBalance.tsx +++ b/src/navigation/tabs/home/components/PortfolioBalance.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import styled from 'styled-components/native'; +import styled, {useTheme} from 'styled-components/native'; import {BaseText, H2} from '../../../../components/styled/Text'; -import {SlateDark, White} from '../../../../styles/colors'; +import {Slate30, SlateDark, White} from '../../../../styles/colors'; import {useSelector} from 'react-redux'; import {RootState} from '../../../../store'; import {formatFiatAmount} from '../../../../utils/helper-methods'; @@ -12,6 +12,7 @@ import { ScreenGutter, } from '../../../../components/styled/Containers'; import {useAppDispatch, useAppSelector} from '../../../../utils/hooks'; +import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; import { setHomeChartCollapsed, showBottomNotificationModal, @@ -54,6 +55,7 @@ import type {FiatRateInterval} from '../../../../store/rate/rate.models'; import type {Wallet} from '../../../../store/wallet/wallet.models'; import CollapseContentButton from './CollapseContentButton'; import useLegacyLastDayChangeRowData from '../../../../components/charts/useLegacyLastDayChangeRowData'; +import Loader from '../../../../components/loader/Loader'; const PortfolioContainer = styled.View` justify-content: center; @@ -151,6 +153,9 @@ const PortfolioBalanceChangeRow = ({ const PortfolioBalanceContent = () => { const {t} = useTranslation(); + const theme = useTheme(); + const pendingImport = useAppSelector(({APP}) => APP.pendingImport); + const importIsFirstKey = useAppSelector(({APP}) => APP.importIsFirstKey); const coinbaseBalance = useAppSelector(({COINBASE}) => COINBASE.balance[COINBASE_ENV]) || 0.0; @@ -233,6 +238,22 @@ const PortfolioBalanceContent = () => { const fullChartHeight = chartBlockHeight || HOME_BALANCE_EXPANDED_CHART_HEIGHT; + const awaitingFirstWalletRef = React.useRef(false); + if (pendingImport && importIsFirstKey) { + awaitingFirstWalletRef.current = true; + } else if (awaitingFirstWalletRef.current) { + if ( + balanceChartsEnabled || + (!portfolioChartsRequested && !pendingImport && !importIsFirstKey) + ) { + awaitingFirstWalletRef.current = false; + } + } + + const showImportSkeleton = + ((pendingImport && importIsFirstKey) || awaitingFirstWalletRef.current) && + !balanceChartsEnabled; + useEffect(() => { setIsChartCollapsed(shouldApplyChartCollapse); cancelAnimation(collapseProgress); @@ -519,7 +540,8 @@ const PortfolioBalanceContent = () => { /> ) : null} - + { - { - dispatch(toggleHideAllBalances()); - }}> - {!hideAllBalances ? ( - <> - - {formattedPortfolioBalance} - - - ) : ( - - {maskIfHidden(true, totalBalanceIncludingCoinbase)} - - )} - + {showImportSkeleton ? ( + + + + + + + ) : ( + { + dispatch(toggleHideAllBalances()); + }}> + {!hideAllBalances ? ( + <> + + {formattedPortfolioBalance} + + + ) : ( + + {maskIfHidden(true, totalBalanceIncludingCoinbase)} + + )} + + )} + {!hideAllBalances && showImportSkeleton ? ( + + + + ) : null} + {!hideAllBalances && (displayedChangeRowData || balanceChartsEnabled) ? ( = ({ layout, context, pendingTssSession, + cardHeight, }) => { const {t} = useTranslation(); const defaultAltCurrency = useAppSelector(({APP}) => APP.defaultAltCurrency); @@ -306,6 +308,7 @@ const WalletCardComponent: React.FC = ({ }} footer={CardFooter} onCTAPress={onPress} + cardHeight={cardHeight} /> ); }; diff --git a/src/navigation/tabs/home/components/cards/CarouselKeySkeleton.tsx b/src/navigation/tabs/home/components/cards/CarouselKeySkeleton.tsx new file mode 100644 index 0000000000..56e7d47bdb --- /dev/null +++ b/src/navigation/tabs/home/components/cards/CarouselKeySkeleton.tsx @@ -0,0 +1,198 @@ +import React, {useEffect, useRef, useState} from 'react'; +import {Animated, TouchableOpacity, View} from 'react-native'; +import styled, {useTheme} from 'styled-components/native'; +import { + Action, + CharcoalBlack, + LightBlack, + Slate30, + SlateDark, + White, +} from '../../../../../styles/colors'; +import {BaseText} from '../../../../../components/styled/Text'; +import {ScreenGutter} from '../../../../../components/styled/Containers'; +import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; +import {HOME_CARD_WIDTH} from '../../../../../components/home-card/HomeCard'; +import {BoxShadow} from '../Styled'; + +const CardOuter = styled.View` + left: ${ScreenGutter}; +`; + +const CardInner = styled.View<{dark: boolean; cardHeight: number}>` + height: ${({cardHeight}) => cardHeight}px; + width: ${HOME_CARD_WIDTH}px; + border-radius: 12px; + border-width: 1px; + border-color: ${({dark}) => (dark ? LightBlack : Slate30)}; + background-color: ${({dark}) => (dark ? CharcoalBlack : White)}; + padding: 14px 14px 12px; + justify-content: flex-start; +`; + +const Message = styled(BaseText)` + font-size: 11px; + line-height: 14px; + color: ${({theme: {dark}}) => (dark ? Slate30 : SlateDark)}; + margin-bottom: 10px; +`; + +const ProgressTrack = styled.View` + height: 4px; + border-radius: 100px; + background-color: ${({theme: {dark}}) => (dark ? '#363636' : Slate30)}; + overflow: hidden; + margin-bottom: 16px; +`; + +const ProgressFill = styled(Animated.View)<{failed?: boolean}>` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 100px; + background-color: ${({failed: f}) => (f ? '#B42727' : Action)}; +`; + +const FailedLabel = styled(BaseText)` + font-size: 12px; + color: ${({theme: {dark}}) => (dark ? Slate30 : SlateDark)}; +`; + +const RetryText = styled(BaseText)` + font-size: 12px; + color: ${Action}; + font-weight: 600; +`; + +interface CarouselKeySkeletonProps { + message?: string | null; + progress?: number; + failed?: boolean; + onRetry?: () => void; + cardHeight: number; +} + +const CarouselKeySkeleton = ({ + message, + progress = 0, + failed, + onRetry, + cardHeight, +}: CarouselKeySkeletonProps) => { + const theme = useTheme(); + const [trackWidth, setTrackWidth] = useState(0); + const progressAnim = useRef(new Animated.Value(progress)).current; + + useEffect(() => { + Animated.timing(progressAnim, { + toValue: progress, + duration: 400, + useNativeDriver: true, + }).start(); + }, [progress, progressAnim]); + + return ( + + + {failed ? ( + + Failed import + Retry + + ) : message ? ( + {message} + ) : null} + + setTrackWidth(e.nativeEvent.layout.width)}> + {trackWidth > 0 ? ( + + ) : null} + + + + + {/* Row 1 */} + + {/* Row 2 */} + + {/* Row 3: circle + bar side by side */} + + + + + {/* Row 4: circle (left) + tall bar (right) */} + + + + + + + + + ); +}; + +export default CarouselKeySkeleton; diff --git a/src/navigation/tabs/home/components/cards/ListKeySkeleton.tsx b/src/navigation/tabs/home/components/cards/ListKeySkeleton.tsx index 3c531f0ab7..0b299c1870 100644 --- a/src/navigation/tabs/home/components/cards/ListKeySkeleton.tsx +++ b/src/navigation/tabs/home/components/cards/ListKeySkeleton.tsx @@ -1,6 +1,14 @@ -import React from 'react'; +import React, {useEffect, useRef, useState} from 'react'; +import {Animated, TouchableOpacity} from 'react-native'; import styled, {useTheme} from 'styled-components/native'; -import {LightBlack, Slate30, White} from '../../../../../styles/colors'; +import { + Action, + Caution, + LightBlack, + Slate30, + White, +} from '../../../../../styles/colors'; +import {BaseText} from '../../../../../components/styled/Text'; import {ScreenGutter} from '../../../../../components/styled/Containers'; import SkeletonPlaceholder from 'react-native-skeleton-placeholder'; import {BoxShadow} from '../Styled'; @@ -9,41 +17,169 @@ const ListCard = styled.View` background-color: ${({theme: {dark}}) => (dark ? LightBlack : White)}; border-radius: 12px; margin: 10px ${ScreenGutter}; + padding: 12px 15px; +`; + +const ImportMessage = styled(BaseText)` + font-size: 13px; + margin-bottom: 8px; +`; + +const ProgressTrack = styled.View` + height: 5px; + border-radius: 3px; + background-color: ${({theme: {dark}}) => (dark ? '#363636' : Slate30)}; + overflow: hidden; + margin-bottom: 12px; +`; + +const ProgressFill = styled(Animated.View)<{failed?: boolean}>` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 3px; + background-color: ${({failed: f}) => (f ? '#B42727' : Action)}; +`; + +const SkeletonRow = styled.View` flex-direction: row; align-items: center; justify-content: space-between; - padding: 15px; - height: 75px; `; -const ListKeySkeleton = () => { +const FailedRow = styled.View` + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 4px 0; +`; + +const FailedText = styled(BaseText)` + font-size: 14px; + color: ${Caution}; +`; + +const RetryText = styled(BaseText)` + font-size: 14px; + color: ${Action}; + font-weight: 600; +`; + +interface ListKeySkeletonProps { + message?: string | null; + progress?: number; + failed?: boolean; + onRetry?: () => void; +} + +const ListKeySkeleton = ({ + message, + progress = 0, + failed, + onRetry, +}: ListKeySkeletonProps) => { const theme = useTheme(); + const [trackWidth, setTrackWidth] = useState(0); + const progressAnim = useRef(new Animated.Value(progress)).current; + + useEffect(() => { + Animated.timing(progressAnim, { + toValue: progress, + duration: 400, + useNativeDriver: true, + }).start(); + }, [progress, progressAnim]); return ( - - - - - - - - - + {message ? ( + <> + {message} + setTrackWidth(e.nativeEvent.layout.width)}> + {trackWidth > 0 ? ( + + ) : null} + + + ) : null} + {failed ? ( + <> + + {'Failed import'} + + Retry + + + setTrackWidth(e.nativeEvent.layout.width)}> + {trackWidth > 0 ? ( + + ) : null} + + + ) : null} + + + + + + + + + + + + + + ); }; diff --git a/src/navigation/tabs/settings/components/WalletsAndKeys.tsx b/src/navigation/tabs/settings/components/WalletsAndKeys.tsx index a6a8c1a6ab..eaef24da07 100644 --- a/src/navigation/tabs/settings/components/WalletsAndKeys.tsx +++ b/src/navigation/tabs/settings/components/WalletsAndKeys.tsx @@ -27,6 +27,7 @@ const WalletsAndKeys = () => { const dispatch = useAppDispatch(); const keys = useAppSelector(({WALLET}) => WALLET.keys); const keyList = Object.values(keys); + const pendingImport = useAppSelector(({APP}) => APP.pendingImport); const onPressKey = (key: Key) => { key.backupComplete @@ -61,7 +62,8 @@ const WalletsAndKeys = () => { )) : null} navigation.navigate('CreationOptions')} activeOpacity={ActiveOpacity}> {t('Create or Import Key')} diff --git a/src/navigation/wallet/components/RecoveryPhrase.tsx b/src/navigation/wallet/components/RecoveryPhrase.tsx index 6c756dcd8e..0bc86d3265 100644 --- a/src/navigation/wallet/components/RecoveryPhrase.tsx +++ b/src/navigation/wallet/components/RecoveryPhrase.tsx @@ -29,6 +29,10 @@ import { import Button, {ButtonState} from '../../../components/button/Button'; import { setHomeCarouselConfig, + setImportBannerMessage, + setImportIsFirstKey, + setImportProgress, + setPendingImport, showBottomNotificationModal, } from '../../../store/app/app.actions'; import {yupResolver} from '@hookform/resolvers/yup'; @@ -52,7 +56,9 @@ import { startImportMnemonic, startImportWithDerivationPath, } from '../../../store/wallet/effects'; -import {useNavigation, useRoute} from '@react-navigation/native'; +import {CommonActions, useNavigation, useRoute} from '@react-navigation/native'; +import {RootStacks} from '../../../Root'; +import {TabsScreens} from '../../tabs/TabsStack'; import {ImportObj} from '../../../store/scan/scan.models'; import {RouteProp} from '@react-navigation/core'; import {WalletGroupParamList} from '../WalletGroup'; @@ -93,6 +99,7 @@ import {useTranslation} from 'react-i18next'; import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view'; import {Analytics} from '../../../store/analytics/analytics.effects'; import {IS_ANDROID, IS_IOS} from '../../../constants'; +import {batch} from 'react-redux'; import { useAppDispatch, useAppSelector, @@ -101,6 +108,10 @@ import { import {TouchableOpacity} from '@components/base/TouchableOpacity'; import {useOngoingProcess} from '../../../contexts'; import haptic from '../../../components/haptic-feedback/haptic'; +import { + getOngoingProcessMessage, + IMPORT_PROGRESS_VISIBLE_EVENTS, +} from '../../../managers/OngoingProcessManager'; const ScrollViewContainer = styled(KeyboardAwareScrollView)` margin-top: 20px; @@ -226,6 +237,7 @@ const RecoveryPhrase = () => { const walletTermsAccepted = useAppSelector( ({WALLET}: RootState) => WALLET.walletTermsAccepted, ); + const existingKeys = useAppSelector(({WALLET}: RootState) => WALLET.keys); const [importButtonState, setImportButtonState] = useState(); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const [derivationPathEnabled, setDerivationPathEnabled] = useState(false); @@ -416,7 +428,15 @@ const RecoveryPhrase = () => { const scanFunds = async (key: Key) => { try { - showOngoingProcess('IMPORT_SCANNING_FUNDS'); + if (derivationPathEnabled) { + showOngoingProcess('IMPORT_SCANNING_FUNDS'); + } else { + dispatch( + setImportBannerMessage( + getOngoingProcessMessage('IMPORT_SCANNING_FUNDS'), + ), + ); + } logger.debug('[Scan funds] Get rates (1/4)...'); await dispatch(startGetRates({force: true})); logger.debug('[Scan funds] Fix wallet addresses (2/4)...'); @@ -446,45 +466,111 @@ const RecoveryPhrase = () => { importData: {words?: string | undefined; xPrivKey?: string | undefined}, opts: Partial, ): Promise => { - try { - setImportButtonState('loading'); - showOngoingProcess('IMPORTING'); - await sleep(1000); - const key = !derivationPathEnabled - ? ((await dispatch(startImportMnemonic(importData, opts))) as Key) - : ((await dispatch( - startImportWithDerivationPath(importData, opts), - )) as Key); - await sleep(1000); - dispatch(setHomeCarouselConfig({id: key.id, show: true})); - await scanFunds(key); - setImportButtonState('success'); - await sleep(500); - setImportButtonState(undefined); - hideOngoingProcess(); - backupRedirect({ - context: route.params?.context, - navigation, - walletTermsAccepted, - key, - }); - dispatch( - Analytics.track('Imported Key', { - context: route.params?.context || '', - source: 'RecoveryPhrase', - }), - ); - } catch (err: any) { - const errMsg = err instanceof Error ? err.message : JSON.stringify(err); - logger.error(errMsg); - setImportButtonState('failed'); - await sleep(500); - setImportButtonState(undefined); - hideOngoingProcess(); - await sleep(600); - showErrorModal(err); + if (derivationPathEnabled) { + try { + setImportButtonState('loading'); + await sleep(1000); + const key = (await dispatch( + startImportWithDerivationPath(importData, opts), + )) as Key; + await sleep(1000); + dispatch(setHomeCarouselConfig({id: key.id, show: true})); + await scanFunds(key); + setImportButtonState('success'); + await sleep(500); + setImportButtonState(undefined); + hideOngoingProcess(); + backupRedirect({ + context: route.params?.context, + navigation, + walletTermsAccepted, + key, + }); + dispatch( + Analytics.track('Imported Key', { + context: route.params?.context || '', + source: 'RecoveryPhrase', + }), + ); + } catch (err: any) { + const errMsg = err instanceof Error ? err.message : JSON.stringify(err); + logger.error(errMsg); + hideOngoingProcess(); + showErrorModal(err); + } return; } + setImportButtonState('loading'); + dispatch(setPendingImport(true)); + dispatch(setImportIsFirstKey(Object.keys(existingKeys).length === 0)); + dispatch(setImportProgress(0)); + dispatch(setImportBannerMessage(getOngoingProcessMessage('IMPORTING'))); + await sleep(1000); + backupRedirect({ + context: route.params?.context, + navigation, + walletTermsAccepted, + }); + (async () => { + try { + const seenEvts = new Set(); + const onProgress = (evt: string, msg: string) => { + if ( + IMPORT_PROGRESS_VISIBLE_EVENTS.includes(evt) && + !seenEvts.has(evt) + ) { + seenEvts.add(evt); + dispatch( + setImportProgress( + Math.min( + seenEvts.size / IMPORT_PROGRESS_VISIBLE_EVENTS.length, + 0.95, + ), + ), + ); + } + dispatch(setImportBannerMessage(msg)); + }; + const key = (await dispatch( + startImportMnemonic(importData, opts, onProgress), + )) as Key; + dispatch( + Analytics.track('Imported Key', { + context: route.params?.context || '', + source: 'RecoveryPhrase', + }), + ); + if (!seenEvts.has('IMPORT_SCANNING_FUNDS')) { + seenEvts.add('IMPORT_SCANNING_FUNDS'); + dispatch( + setImportProgress( + Math.min( + seenEvts.size / IMPORT_PROGRESS_VISIBLE_EVENTS.length, + 0.95, + ), + ), + ); + } + await scanFunds(key); + batch(() => { + dispatch(setHomeCarouselConfig({id: key.id, show: true})); + dispatch(setImportBannerMessage(null)); + dispatch(setImportProgress(0)); + dispatch(setPendingImport(false)); + dispatch(setImportIsFirstKey(false)); + }); + } catch (err: any) { + const errMsg = err instanceof Error ? err.message : JSON.stringify(err); + logger.error(errMsg); + batch(() => { + dispatch(setImportBannerMessage(null)); + dispatch(setPendingImport(false)); + dispatch(setImportIsFirstKey(false)); + }); + setImportButtonState(undefined); + showErrorModal(err); + } + })(); }; const setOptsAndCreate = async ( @@ -631,6 +717,7 @@ const RecoveryPhrase = () => { testID="recovery-phrase-view" accessibilityLabel="Recovery phrase view" extraScrollHeight={90} + enableResetScrollToCoords={false} keyboardShouldPersistTaps={'handled'}> diff --git a/src/navigation/wallet/screens/Backup.tsx b/src/navigation/wallet/screens/Backup.tsx index e57c822616..261cf2e7dc 100644 --- a/src/navigation/wallet/screens/Backup.tsx +++ b/src/navigation/wallet/screens/Backup.tsx @@ -17,13 +17,16 @@ import haptic from '../../../components/haptic-feedback/haptic'; import {showBottomNotificationModal} from '../../../store/app/app.actions'; import {WalletGroupParamList, WalletScreens} from '../WalletGroup'; import {Key} from '../../../store/wallet/wallet.models'; -import {useNavigation} from '@react-navigation/native'; +import {CommonActions, useNavigation} from '@react-navigation/native'; import {StackActions} from '@react-navigation/native'; import {RootState} from '../../../store'; import {NativeStackScreenProps} from '@react-navigation/native-stack'; import {useThemeType} from '../../../utils/hooks/useThemeType'; import {useTranslation} from 'react-i18next'; import {ExternalServicesScreens} from '../../services/ExternalServicesGroup'; +import {SwapCryptoScreens} from '../../services/swap-crypto/SwapCryptoGroup'; +import {RootStacks} from '../../../Root'; +import {TabsScreens} from '../../tabs/TabsStack'; const BackupImage = { light: ( @@ -78,29 +81,34 @@ export const backupRedirect = ({ key?: Key; }) => { if (context === 'onboarding') { - navigation.navigate('TermsOfUse'); + navigation.navigate(WalletScreens.TERMS_OF_USE); } else if (context === 'keySettings') { - navigation.navigate('KeySettings', {key}); + navigation.navigate(WalletScreens.KEY_SETTINGS, {key}); } else if (context === 'settings') { - navigation.navigate('Tabs', {screen: 'Settings', params: {key}}); + navigation.navigate(RootStacks.TABS, { + screen: TabsScreens.SETTINGS, + params: {key}, + }); } else if (!key?.backupComplete) { - navigation.navigate('Tabs', {screen: 'Home'}); + navigation.navigate(RootStacks.TABS, {screen: TabsScreens.HOME}); } else if (!walletTermsAccepted) { - navigation.navigate('TermsOfUse', {key}); + navigation.navigate(WalletScreens.TERMS_OF_USE, {key}); } else if (context === 'createNewMultisigKey') { navigation.dispatch(StackActions.popToTop()); navigation.dispatch( - StackActions.push('KeyOverview', {id: key.id, context}), + StackActions.push(WalletScreens.KEY_OVERVIEW, {id: key.id, context}), ); } else if (context === 'swapCrypto' || context === 'swapTo') { - navigation.navigate('SwapCryptoRoot'); + navigation.navigate(SwapCryptoScreens.SWAP_CRYPTO_ROOT); } else if (context === 'buyCrypto') { navigation.navigate(ExternalServicesScreens.ROOT_BUY_AND_SELL, { context: 'buyCrypto', }); } else { navigation.dispatch(StackActions.popToTop()); - navigation.dispatch(StackActions.push('KeyOverview', {id: key.id})); + navigation.dispatch( + StackActions.push(WalletScreens.KEY_OVERVIEW, {id: key.id}), + ); } }; diff --git a/src/store/app/app.actions.ts b/src/store/app/app.actions.ts index d68b3cf886..46c98b5de9 100644 --- a/src/store/app/app.actions.ts +++ b/src/store/app/app.actions.ts @@ -25,6 +25,28 @@ export const importLedgerModalToggled = (show: boolean): AppActionType => ({ payload: show, }); +export const setImportBannerMessage = ( + message: string | null, +): AppActionType => ({ + type: AppActionTypes.SET_IMPORT_BANNER_MESSAGE, + payload: message, +}); + +export const setPendingImport = (pending: boolean): AppActionType => ({ + type: AppActionTypes.SET_PENDING_IMPORT, + payload: pending, +}); + +export const setImportProgress = (progress: number): AppActionType => ({ + type: AppActionTypes.SET_IMPORT_PROGRESS, + payload: progress, +}); + +export const setImportIsFirstKey = (isFirstKey: boolean): AppActionType => ({ + type: AppActionTypes.SET_IMPORT_IS_FIRST_KEY, + payload: isFirstKey, +}); + export const networkChanged = (network: Network): AppActionType => ({ type: AppActionTypes.NETWORK_CHANGED, payload: network, diff --git a/src/store/app/app.reducer.ts b/src/store/app/app.reducer.ts index 7cd2ac5104..12179e0ef1 100644 --- a/src/store/app/app.reducer.ts +++ b/src/store/app/app.reducer.ts @@ -53,6 +53,8 @@ export const appReduxPersistBlackList: Array = [ 'tokensDataLoaded', 'isImportLedgerModalVisible', 'showArchaxBanner', + 'importBannerMessage', + 'importIsFirstKey', ]; export type ModalId = @@ -171,6 +173,10 @@ export interface AppState { tokensDataLoaded: boolean; showArchaxBanner: boolean; dismissedMarketingCardIds: string[]; + importBannerMessage: string | null; + pendingImport: boolean; + importProgress: number; + importIsFirstKey: boolean; } const initialState: AppState = { @@ -272,6 +278,10 @@ const initialState: AppState = { tokensDataLoaded: false, showArchaxBanner: false, dismissedMarketingCardIds: [], + importBannerMessage: null, + pendingImport: false, + importProgress: 0, + importIsFirstKey: false, }; export const appReducer = ( @@ -827,6 +837,18 @@ export const appReducer = ( }; } + case AppActionTypes.SET_IMPORT_BANNER_MESSAGE: + return {...state, importBannerMessage: action.payload}; + + case AppActionTypes.SET_PENDING_IMPORT: + return {...state, pendingImport: action.payload}; + + case AppActionTypes.SET_IMPORT_PROGRESS: + return {...state, importProgress: action.payload}; + + case AppActionTypes.SET_IMPORT_IS_FIRST_KEY: + return {...state, importIsFirstKey: action.payload}; + default: return state; } diff --git a/src/store/app/app.types.ts b/src/store/app/app.types.ts index 56b9202236..ee4201290d 100644 --- a/src/store/app/app.types.ts +++ b/src/store/app/app.types.ts @@ -99,6 +99,10 @@ export enum AppActionTypes { IN_APP_BROWSER_OPEN = 'APP/IN_APP_BROWSER_OPEN', SHOW_ARCHAX_BANNER = 'APP/SHOW_ARCHAX_BANNER', DISMISS_MARKETING_CONTENT_CARD = 'APP/DISMISS_MARKETING_CONTENT_CARD', + SET_IMPORT_BANNER_MESSAGE = 'APP/SET_IMPORT_BANNER_MESSAGE', + SET_PENDING_IMPORT = 'APP/SET_PENDING_IMPORT', + SET_IMPORT_PROGRESS = 'APP/SET_IMPORT_PROGRESS', + SET_IMPORT_IS_FIRST_KEY = 'APP/SET_IMPORT_IS_FIRST_KEY', } interface ImportLedgerModalToggled { @@ -454,6 +458,26 @@ interface DismissMarketingContentCard { payload: string; } +interface SetImportBannerMessage { + type: typeof AppActionTypes.SET_IMPORT_BANNER_MESSAGE; + payload: string | null; +} + +interface SetPendingImport { + type: typeof AppActionTypes.SET_PENDING_IMPORT; + payload: boolean; +} + +interface SetImportProgress { + type: typeof AppActionTypes.SET_IMPORT_PROGRESS; + payload: number; +} + +interface SetImportIsFirstKey { + type: typeof AppActionTypes.SET_IMPORT_IS_FIRST_KEY; + payload: boolean; +} + export type AppActionType = | NetworkChanged | SuccessAppInit @@ -531,4 +555,8 @@ export type AppActionType = | ShowWalletConnectStartModal | DismissWalletConnectStartModal | ShowArchaxBanner - | DismissMarketingContentCard; + | DismissMarketingContentCard + | SetImportBannerMessage + | SetPendingImport + | SetImportProgress + | SetImportIsFirstKey; diff --git a/src/store/wallet/effects/import/import.ts b/src/store/wallet/effects/import/import.ts index 6755aaa8e5..5909e974a6 100644 --- a/src/store/wallet/effects/import/import.ts +++ b/src/store/wallet/effects/import/import.ts @@ -108,8 +108,8 @@ import {tokenManager} from '../../../../managers/TokenManager'; import {logManager} from '../../../../managers/LogManager'; import * as Sentry from '@sentry/react-native'; import { + getOngoingProcessMessage, importProgressEvents, - ongoingProcessManager, } from '../../../../managers/OngoingProcessManager'; import {EventEmitter} from 'events'; @@ -878,6 +878,7 @@ export const startImportMnemonic = ( importData: {words?: string; xPrivKey?: string}, opts: Partial, + onProgress?: (evt: string, message: string) => void, ): Effect => async (dispatch, getState): Promise => { return new Promise(async (resolve, reject) => { @@ -903,7 +904,7 @@ export const startImportMnemonic = opts.words = normalizeMnemonic(words); opts.xPrivKey = xPrivKey; - const data = await serverAssistedImport(opts); + const data = await serverAssistedImport(opts, onProgress); // we need to ensure that each evm/svm account has all supported wallets attached. const vmWallets = getEvmGasWallets(data.wallets); @@ -1613,6 +1614,7 @@ const createKeyAndCredentialsWithFile = async ( export const serverAssistedImport = async ( opts: Partial, + onProgress?: (evt: string, message: string) => void, ): Promise<{key: KeyMethods; wallets: Wallet[]}> => { return new Promise((resolve, reject) => { try { @@ -1633,7 +1635,10 @@ export const serverAssistedImport = async ( return; } draining = true; - ongoingProcessManager.show(item.evt as any, item.data); + onProgress?.( + item.evt, + getOngoingProcessMessage(item.evt as any, item.data), + ); setTimeout(drainQueue, MIN_DISPLAY_MS); }; @@ -1655,6 +1660,7 @@ export const serverAssistedImport = async ( ]); const eventIterations: Record = {}; + const chainIterations: Record = {}; importProgressEvents.forEach(evt => { events.on(evt, (data?: any) => { @@ -1666,7 +1672,18 @@ export const serverAssistedImport = async ( const baseData = typeof data === 'object' && data !== null ? data : {}; const countData = typeof data === 'number' ? {count: data} : {}; - eventQueue.push({evt, data: {...baseData, ...countData, iteration}}); + + let chainIteration: number | undefined; + if (evt === 'walletInfo.gatheringTokens' && baseData.chain) { + const chainKey = baseData.chain; + chainIterations[chainKey] = (chainIterations[chainKey] ?? 0) + 1; + chainIteration = chainIterations[chainKey]; + } + + eventQueue.push({ + evt, + data: {...baseData, ...countData, iteration, chainIteration}, + }); if (!draining) { drainQueue(); } diff --git a/src/store/wallet/wallet.reducer.spec.ts b/src/store/wallet/wallet.reducer.spec.ts index 2ac35cdf3c..4a1eaf4ba1 100644 --- a/src/store/wallet/wallet.reducer.spec.ts +++ b/src/store/wallet/wallet.reducer.spec.ts @@ -355,7 +355,7 @@ describe('SUCCESS_UPDATE_KEYS_TOTAL_BALANCE', () => { type: WalletActionTypes.SUCCESS_UPDATE_KEYS_TOTAL_BALANCE, payload: [{keyId: 'ghost', totalBalance: 100, totalBalanceLastDay: 100}], }); - expect(state.keys['ghost']).toBeUndefined(); + expect(state.keys.ghost).toBeUndefined(); }); }); diff --git a/src/utils/portfolio/assets.pure.spec.ts b/src/utils/portfolio/assets.pure.spec.ts index 92eb1e7b73..aa1c9345fc 100644 --- a/src/utils/portfolio/assets.pure.spec.ts +++ b/src/utils/portfolio/assets.pure.spec.ts @@ -389,8 +389,8 @@ describe('buildWalletIdsByAssetGroupKey', () => { makeWallet('w3', 'livenet', 'eth'), ]; const result = buildWalletIdsByAssetGroupKey(wallets); - expect(result['btc']).toEqual(['w1', 'w2']); - expect(result['eth']).toEqual(['w3']); + expect(result.btc).toEqual(['w1', 'w2']); + expect(result.eth).toEqual(['w3']); }); it('skips wallets that are not on mainnet (livenet)', () => { @@ -399,7 +399,7 @@ describe('buildWalletIdsByAssetGroupKey', () => { makeWallet('w2', 'livenet', 'btc'), ]; const result = buildWalletIdsByAssetGroupKey(wallets); - expect(result['btc']).toEqual(['w2']); + expect(result.btc).toEqual(['w2']); }); it('skips wallets with no id', () => { @@ -408,7 +408,7 @@ describe('buildWalletIdsByAssetGroupKey', () => { makeWallet('w2', 'livenet', 'btc'), ]; const result = buildWalletIdsByAssetGroupKey(wallets); - expect(result['btc']).toEqual(['w2']); + expect(result.btc).toEqual(['w2']); }); it('skips wallets with no currencyAbbreviation', () => { @@ -417,7 +417,7 @@ describe('buildWalletIdsByAssetGroupKey', () => { makeWallet('w2', 'livenet', 'eth'), ]; const result = buildWalletIdsByAssetGroupKey(wallets); - expect(result['eth']).toEqual(['w2']); + expect(result.eth).toEqual(['w2']); expect(result['']).toBeUndefined(); }); }); @@ -975,7 +975,7 @@ describe('getPopulateLoadingByAssetKey', () => { } as any, }); expect(result).not.toBeUndefined(); - expect(result!['btc']).toBe(false); + expect(result!.btc).toBe(false); }); it('returns map with true for assets with in_progress wallets', () => { @@ -989,7 +989,7 @@ describe('getPopulateLoadingByAssetKey', () => { currentWalletId: undefined, } as any, }); - expect(result!['btc']).toBe(true); + expect(result!.btc).toBe(true); }); it('uses prev value when no wallets are in scope for an asset key', () => { @@ -1005,7 +1005,7 @@ describe('getPopulateLoadingByAssetKey', () => { prev: {eth: true}, }); // w2 is not in scope (not in statusById, not currentWalletId) → use prev - expect(result!['eth']).toBe(true); + expect(result!.eth).toBe(true); }); it('preserves prev keys that are not in items', () => { @@ -1021,8 +1021,8 @@ describe('getPopulateLoadingByAssetKey', () => { prev: {eth: true, btc: false}, }); // eth was in prev but not in items → it should be copied over - expect(result!['eth']).toBe(true); - expect(result!['btc']).toBe(false); + expect(result!.eth).toBe(true); + expect(result!.btc).toBe(false); }); it('returns false for asset with error status (counts as finished)', () => { @@ -1036,7 +1036,7 @@ describe('getPopulateLoadingByAssetKey', () => { currentWalletId: undefined, } as any, }); - expect(result!['btc']).toBe(false); + expect(result!.btc).toBe(false); }); it('returns true for asset in fullPopulate mode (wallets total matches)', () => { @@ -1052,7 +1052,7 @@ describe('getPopulateLoadingByAssetKey', () => { currentWalletId: undefined, } as any, }); - expect(result!['btc']).toBe(true); + expect(result!.btc).toBe(true); }); it('returns prev reference when next is identical to prev (same keys and values)', () => { @@ -1087,7 +1087,7 @@ describe('getPopulateLoadingByAssetKey', () => { }); // w1 is currentWalletId but not in statusById → statusById[w1] = undefined // undefined !== 'done' && undefined !== 'error' → allFinished=false → loading=true - expect(result!['btc']).toBe(true); + expect(result!.btc).toBe(true); }); }); diff --git a/test/setup.js b/test/setup.js index b4b6a34d3a..076491220a 100644 --- a/test/setup.js +++ b/test/setup.js @@ -21,7 +21,7 @@ jest.mock('react-native/Libraries/Utilities/Platform', () => { isPad: false, isTV: false, isTesting: true, - select: spec => spec['android'] ?? null, + select: spec => spec.android ?? null, }; return {...Platform, default: Platform}; });