diff --git a/apps/mobile/src/components/WealthChart/WealthChart.tsx b/apps/mobile/src/components/WealthChart/WealthChart.tsx
index b55079f93..ad15528fb 100644
--- a/apps/mobile/src/components/WealthChart/WealthChart.tsx
+++ b/apps/mobile/src/components/WealthChart/WealthChart.tsx
@@ -24,13 +24,14 @@ import {
useAllAccounts,
WalletAccount,
} from '@perawallet/wallet-core-accounts'
-import { LoadingView } from '../LoadingView'
import { EmptyView } from '../EmptyView'
+import { LoadingView } from '../LoadingView'
import {
CHART_ANIMATION_DURATION,
CHART_FOCUS_DEBOUNCE_TIME,
CHART_HEIGHT,
} from '@constants/ui'
+import { getChartYAxisRange } from '@utils/chart'
export type WealthChartProps = {
account?: WalletAccount
@@ -74,15 +75,10 @@ export const WealthChart = ({
[data],
)
- const yAxisOffsets = useMemo(() => {
- const minValue = Math.min(...dataPoints.map(p => p.value))
- const maxValue = Math.max(...dataPoints.map(p => p.value))
- if (minValue === 0 && maxValue === 0) {
- return [-1, 1]
- } else {
- return [minValue - minValue / 10, maxValue + maxValue / 10]
- }
- }, [dataPoints])
+ const yAxisRange = useMemo(
+ () => getChartYAxisRange(dataPoints),
+ [dataPoints],
+ )
const onFocus = useCallback(
({
@@ -113,18 +109,14 @@ export const WealthChart = ({
],
)
- if (isPending) {
- return (
-
- )
- }
-
return (
- {!dataPoints?.length ? (
+ {isPending ? (
+
+ ) : !dataPoints?.length ? (
{
const listRef = useRef(null)
const styles = useStyles()
@@ -62,6 +64,7 @@ export const AccountAssetList = ({
optOutConfirmationState,
assetForOptOut,
isOptingOut,
+ headerState,
setSearchFilter,
handleConfirmOptOut,
handleCloseOptOut,
@@ -108,33 +111,42 @@ export const AccountAssetList = ({
const listHeader = (
-
- {header}
-
-
- {t('account_details.assets.title')}
-
- {!isWatch && (
-
-
-
+
+ {headerState.isOpen && (
+ <>
+ {header}
+
+
+ {t('account_details.assets.title')}
+
+ {!isWatch && (
+
+
+
+
+ )}
- )}
-
+ >
+ )}
)
@@ -145,38 +157,43 @@ export const AccountAssetList = ({
behavior='padding'
style={styles.keyboardAvoidingViewContainer}
>
- item.assetId}
- estimatedItemSize={72}
- recycleItems
- automaticallyAdjustKeyboardInsets
- keyboardDismissMode='interactive'
- contentContainerStyle={styles.rootContainer}
- ListHeaderComponent={listHeader}
- searchPlaceholder={t(
- 'account_details.assets.search_placeholder',
- )}
- onSearchChange={setSearchFilter}
- ListEmptyComponent={
- isPending ? (
-
- ) : (
-
- )
- }
- />
+
+ item.assetId}
+ estimatedItemSize={72}
+ recycleItems
+ automaticallyAdjustKeyboardInsets
+ keyboardDismissMode='interactive'
+ contentContainerStyle={styles.rootContainer}
+ ListHeaderComponent={listHeader}
+ searchPlaceholder={t(
+ 'account_details.assets.search_placeholder',
+ )}
+ onSearchChange={setSearchFilter}
+ ListEmptyComponent={
+ isPending ? (
+
+ ) : (
+
+ )
+ }
+ />
+
{
describe('backup banner integration', () => {
beforeEach(() => {
mockBackupReminderBannerHook.mockReset()
+ vi.useFakeTimers()
})
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
+ const advancePastRevealDelay = () => {
+ act(() => {
+ vi.advanceTimersByTime(BACKUP_REMINDER_BANNER_REVEAL_DELAY)
+ })
+ }
+
it('renders backup reminder banner when hook reports visible', () => {
mockBackupReminderBannerHook.mockReturnValue({
isVisible: true,
@@ -262,6 +275,7 @@ describe('AccountAssetList', () => {
})
render()
+ advancePastRevealDelay()
expect(screen.getByTestId('backup_reminder_banner')).toBeTruthy()
})
@@ -275,6 +289,7 @@ describe('AccountAssetList', () => {
const { queryByTestId } = render(
,
)
+ advancePastRevealDelay()
expect(queryByTestId('backup_reminder_banner')).toBeNull()
})
diff --git a/apps/mobile/src/modules/accounts/components/AccountAssetList/useAccountAssetList.ts b/apps/mobile/src/modules/accounts/components/AccountAssetList/useAccountAssetList.ts
index abd6f6f83..4b210fcb4 100644
--- a/apps/mobile/src/modules/accounts/components/AccountAssetList/useAccountAssetList.ts
+++ b/apps/mobile/src/modules/accounts/components/AccountAssetList/useAccountAssetList.ts
@@ -45,6 +45,7 @@ type UseAccountAssetListResult = {
hideZeroBalance: boolean
assetSortMode: AssetSortMode
searchFilter: string
+ headerState: ModalState
manageSheetState: ModalState
sortSheetState: ModalState
filterSheetState: ModalState
@@ -79,6 +80,7 @@ export const useAccountAssetList = ({
account,
t,
}: UseAccountAssetListParams): UseAccountAssetListResult => {
+ const headerState = useModalState(true)
const manageSheetState = useModalState(false)
const sortSheetState = useModalState(false)
const filterSheetState = useModalState(false)
@@ -161,6 +163,7 @@ export const useAccountAssetList = ({
const goToAssetScreen = useCallback(
(item: AssetWithAccountBalance) => {
+ headerState.open()
const assetInfo = assets?.get(item.assetId)
if (assetInfo && isCollectible(assetInfo)) {
navigation.navigate('CollectibleDetails', {
@@ -172,7 +175,7 @@ export const useAccountAssetList = ({
})
}
},
- [navigation, assets],
+ [headerState, navigation, assets],
)
const handleOptOut = useCallback(
@@ -279,6 +282,7 @@ export const useAccountAssetList = ({
hideZeroBalance,
assetSortMode,
searchFilter,
+ headerState,
manageSheetState,
sortSheetState,
filterSheetState,
diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverview.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverview.tsx
index c868a340c..d998ea2c1 100644
--- a/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverview.tsx
+++ b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverview.tsx
@@ -10,7 +10,6 @@
limitations under the License
*/
-import { useEffect, useMemo } from 'react'
import { AccountOverviewHeader } from './AccountOverviewHeader'
import { SendFundsBottomSheet } from '@modules/transactions/components/send-funds/SendFundsBottomSheet/SendFundsBottomSheet'
import { ReceiveFundsBottomSheet } from '@modules/transactions/components/receive-funds/ReceiveFundsBottomSheet'
@@ -20,10 +19,7 @@ import { useAccountOverview } from './useAccountOverview'
import { PWView } from '@components/core'
import { AccountAssetList } from '../AccountAssetList'
import { AccountOptionsBottomSheet } from '../AccountOptionsBottomSheet'
-import {
- AccountOverviewModalContext,
- UseAccountOverviewModalResult,
-} from './AccountOverviewModalContext'
+import { AccountOverviewModalContext } from './AccountOverviewModalContext'
export type AccountOverviewProps = {
account: WalletAccount
@@ -39,38 +35,16 @@ export const AccountOverview = ({
const styles = useStyles()
const {
isSendFundsVisible,
- openSendFunds,
handleCloseSendFunds,
isReceiveFundsVisible,
openReceiveFunds,
handleCloseReceiveFunds,
isAccountOptionsVisible,
- openAccountOptions,
handleCloseAccountOptions,
scrollingEnabled,
- onScrollEnabledChange,
- } = useAccountOverview(account)
-
- useEffect(() => {
- onSwipeEnabledChange?.(scrollingEnabled)
- }, [scrollingEnabled, onSwipeEnabledChange])
-
- const contextValue = useMemo(
- () => ({
- account,
- openSendFunds,
- openReceiveFunds,
- openAccountOptions,
- onScrollEnabledChange,
- }),
- [
- account,
- openSendFunds,
- openReceiveFunds,
- openAccountOptions,
- onScrollEnabledChange,
- ],
- )
+ isLoading,
+ contextValue,
+ } = useAccountOverview({ account, onSwipeEnabledChange })
return (
@@ -78,10 +52,12 @@ export const AccountOverview = ({
}
/>
diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeader.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeader.tsx
index d9507485f..ff5dfaac0 100644
--- a/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeader.tsx
+++ b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeader.tsx
@@ -31,15 +31,18 @@ import { ALGO_ASSET } from '@perawallet/wallet-core-assets'
import { PreferredCurrencyDisplay } from '@components/PreferredCurrencyDisplay'
import { ExpandablePanel } from '@components/ExpandablePanel'
import { useAccountOverviewHeader } from './useAccountOverviewHeader'
+import { AccountOverviewHeaderSkeleton } from './AccountOverviewHeaderSkeleton'
export type AccountOverviewHeaderProps = {
account: WalletAccount
chartVisible: boolean
+ isLoading: boolean
}
export const AccountOverviewHeader = ({
account,
chartVisible,
+ isLoading,
}: AccountOverviewHeaderProps) => {
const styles = useStyles()
const { t } = useLanguage()
@@ -55,81 +58,84 @@ export const AccountOverviewHeader = ({
handleChartSelectionChange,
} = useAccountOverviewHeader(account)
- return hasBalance || isPending ? (
+ // While loading we always render the primary layout underneath so its mount
+ // animations (ExpandablePanel, LineChart) run hidden by the opaque skeleton
+ // overlay. By the time the overlay unmounts, everything below is settled.
+ const showPrimary = hasBalance || isLoading
+
+ return (
-
-
-
-
-
-
+ {showPrimary ? (
+ <>
+
+
+
+
+
+
- {!selectedPoint && (
-
- )}
- {selectedPoint && (
-
- {formatDatetime(selectedPoint.datetime)}
-
- )}
-
-
+ {!selectedPoint && (
+
+ )}
+ {selectedPoint && (
+
+ {formatDatetime(selectedPoint.datetime)}
+
+ )}
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
- {!canSign ? : }
-
-
- ) : (
-
- {!canSign ? (
+ {!canSign ? : }
+ >
+ ) : !canSign ? (
) : (
<>
@@ -150,6 +156,12 @@ export const AccountOverviewHeader = ({
>
)}
+
+ {isLoading && (
+
+
+
+ )}
)
}
diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeaderSkeleton.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeaderSkeleton.tsx
new file mode 100644
index 000000000..6a218fba1
--- /dev/null
+++ b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeaderSkeleton.tsx
@@ -0,0 +1,72 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import { PWView } from '@components/core'
+import { useStyles } from './styles'
+
+const PERIOD_BUTTON_COUNT = 5
+const ACTION_BUTTON_COUNT = 4
+
+export const AccountOverviewHeaderSkeleton = () => {
+ const styles = useStyles()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: PERIOD_BUTTON_COUNT }, (_, i) => (
+
+ ))}
+
+
+
+ {Array.from({ length: ACTION_BUTTON_COUNT }, (_, i) => (
+
+ ))}
+
+
+ )
+}
diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/AccountOverview.spec.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/AccountOverview.spec.tsx
index bfdd9c9b6..63636c52e 100644
--- a/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/AccountOverview.spec.tsx
+++ b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/AccountOverview.spec.tsx
@@ -65,6 +65,10 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => {
isRefetching: false,
isError: false,
})),
+ useAccountBalancesHistoryQuery: vi.fn(() => ({
+ data: undefined,
+ isPending: false,
+ })),
usePortfolioTotals: vi.fn(() => ({
portfolioUsdValue: new Decimal('200'),
accountUsdValues: new Map(),
diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverview.spec.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverview.spec.tsx
index 1682fffd0..a7ead9c74 100644
--- a/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverview.spec.tsx
+++ b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverview.spec.tsx
@@ -15,13 +15,26 @@ import { renderHook, act } from '@testing-library/react'
import { useAccountOverview } from '../useAccountOverview'
import { WalletAccount } from '@perawallet/wallet-core-accounts'
-const { mockSetSelectedAccount, mockSetCanSelectAccount } = vi.hoisted(() => ({
+const {
+ mockSetSelectedAccount,
+ mockSetCanSelectAccount,
+ mockBalancesPending,
+ mockHistoryPending,
+} = vi.hoisted(() => ({
mockSetSelectedAccount: vi.fn(),
mockSetCanSelectAccount: vi.fn(),
+ mockBalancesPending: { value: false },
+ mockHistoryPending: { value: false },
}))
vi.mock('@perawallet/wallet-core-accounts', () => ({
useSelectedAccount: vi.fn(() => ({ address: 'selected-address' })),
+ useAccountBalancesQuery: vi.fn(() => ({
+ isPending: mockBalancesPending.value,
+ })),
+ useAccountBalancesHistoryQuery: vi.fn(() => ({
+ isPending: mockHistoryPending.value,
+ })),
}))
vi.mock('@modules/transactions/hooks', () => ({
@@ -31,15 +44,24 @@ vi.mock('@modules/transactions/hooks', () => ({
})),
}))
-describe('useAccountOverview', () => {
- const mockAccount = { address: 'test-address' } as WalletAccount
+const mockAccount = { address: 'test-address' } as WalletAccount
+const renderUseAccountOverview = (
+ onSwipeEnabledChange?: (enabled: boolean) => void,
+) =>
+ renderHook(() =>
+ useAccountOverview({ account: mockAccount, onSwipeEnabledChange }),
+ )
+
+describe('useAccountOverview', () => {
beforeEach(() => {
vi.clearAllMocks()
+ mockBalancesPending.value = false
+ mockHistoryPending.value = false
})
it('opens send funds modal when openSendFunds is called', () => {
- const { result } = renderHook(() => useAccountOverview(mockAccount))
+ const { result } = renderUseAccountOverview()
expect(result.current.isSendFundsVisible).toBe(false)
@@ -51,7 +73,7 @@ describe('useAccountOverview', () => {
})
it('closes send funds modal when handleCloseSendFunds is called', () => {
- const { result } = renderHook(() => useAccountOverview(mockAccount))
+ const { result } = renderUseAccountOverview()
act(() => {
result.current.openSendFunds()
@@ -67,7 +89,7 @@ describe('useAccountOverview', () => {
})
it('opens receive funds modal and sets selected account when openReceiveFunds is called', () => {
- const { result } = renderHook(() => useAccountOverview(mockAccount))
+ const { result } = renderUseAccountOverview()
expect(result.current.isReceiveFundsVisible).toBe(false)
@@ -83,7 +105,7 @@ describe('useAccountOverview', () => {
})
it('closes receive funds modal when handleCloseReceiveFunds is called', () => {
- const { result } = renderHook(() => useAccountOverview(mockAccount))
+ const { result } = renderUseAccountOverview()
act(() => {
result.current.openReceiveFunds()
@@ -99,7 +121,7 @@ describe('useAccountOverview', () => {
})
it('opens account options when openAccountOptions is called', () => {
- const { result } = renderHook(() => useAccountOverview(mockAccount))
+ const { result } = renderUseAccountOverview()
expect(result.current.isAccountOptionsVisible).toBe(false)
@@ -111,7 +133,7 @@ describe('useAccountOverview', () => {
})
it('closes account options when handleCloseAccountOptions is called', () => {
- const { result } = renderHook(() => useAccountOverview(mockAccount))
+ const { result } = renderUseAccountOverview()
act(() => {
result.current.openAccountOptions()
@@ -127,13 +149,13 @@ describe('useAccountOverview', () => {
})
it('starts with scrolling enabled', () => {
- const { result } = renderHook(() => useAccountOverview(mockAccount))
+ const { result } = renderUseAccountOverview()
expect(result.current.scrollingEnabled).toBe(true)
})
it('updates scrolling state when onScrollEnabledChange is called', () => {
- const { result } = renderHook(() => useAccountOverview(mockAccount))
+ const { result } = renderUseAccountOverview()
act(() => {
result.current.onScrollEnabledChange(false)
@@ -147,4 +169,56 @@ describe('useAccountOverview', () => {
expect(result.current.scrollingEnabled).toBe(true)
})
+
+ it('forwards scrollingEnabled changes to onSwipeEnabledChange', () => {
+ const onSwipeEnabledChange = vi.fn()
+ const { result } = renderUseAccountOverview(onSwipeEnabledChange)
+
+ expect(onSwipeEnabledChange).toHaveBeenLastCalledWith(true)
+
+ act(() => {
+ result.current.onScrollEnabledChange(false)
+ })
+
+ expect(onSwipeEnabledChange).toHaveBeenLastCalledWith(false)
+ })
+
+ it('isLoading is true while balances or history are pending and stays false once both have completed', () => {
+ mockBalancesPending.value = true
+ mockHistoryPending.value = true
+ const { result, rerender } = renderUseAccountOverview()
+
+ expect(result.current.isLoading).toBe(true)
+
+ mockBalancesPending.value = false
+ rerender()
+ expect(result.current.isLoading).toBe(true)
+
+ mockHistoryPending.value = false
+ rerender()
+ expect(result.current.isLoading).toBe(false)
+
+ // Sticky: once cleared, isLoading does not flip back to true.
+ mockBalancesPending.value = true
+ rerender()
+ expect(result.current.isLoading).toBe(false)
+ })
+
+ it('exposes a contextValue containing the account and modal openers', () => {
+ const { result } = renderUseAccountOverview()
+
+ expect(result.current.contextValue.account).toBe(mockAccount)
+ expect(typeof result.current.contextValue.openSendFunds).toBe(
+ 'function',
+ )
+ expect(typeof result.current.contextValue.openReceiveFunds).toBe(
+ 'function',
+ )
+ expect(typeof result.current.contextValue.openAccountOptions).toBe(
+ 'function',
+ )
+ expect(typeof result.current.contextValue.onScrollEnabledChange).toBe(
+ 'function',
+ )
+ })
})
diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverviewHeader.spec.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverviewHeader.spec.tsx
index 80b916024..f78da48c9 100644
--- a/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverviewHeader.spec.tsx
+++ b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverviewHeader.spec.tsx
@@ -40,6 +40,10 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => {
isRefetching: false,
isError: false,
})),
+ useAccountBalancesHistoryQuery: vi.fn(() => ({
+ data: undefined,
+ isPending: false,
+ })),
usePortfolioTotals: vi.fn(() => ({
portfolioUsdValue: new Decimal('200'),
accountUsdValues: new Map(),
diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/styles.ts b/apps/mobile/src/modules/accounts/components/AccountOverview/styles.ts
index 875603bad..5d946c09c 100644
--- a/apps/mobile/src/modules/accounts/components/AccountOverview/styles.ts
+++ b/apps/mobile/src/modules/accounts/components/AccountOverview/styles.ts
@@ -11,8 +11,10 @@
*/
import { makeStyles } from '@rneui/themed'
+import { CHART_HEIGHT } from '@constants/ui'
-const HEADER_MIN_HEIGHT = 220
+const HEADER_HEIGHT = 412
+const SKELETON_BORDER_RADIUS = 8
export const useStyles = makeStyles(theme => {
return {
@@ -120,7 +122,66 @@ export const useStyles = makeStyles(theme => {
textAlign: 'center',
},
headerContainer: {
- minHeight: HEADER_MIN_HEIGHT, // Fixed value to prevent "bouncing" on load
+ // Fixed height so the page doesn't reflow as queries resolve and
+ // the skeleton overlay matches the loaded layout exactly.
+ height: HEADER_HEIGHT,
+ },
+ skeletonOverlay: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ // Solid background so internal mount animations (ExpandablePanel,
+ // LineChart, currency-display loading swap) running underneath
+ // are fully occluded until the overlay unmounts.
+ backgroundColor: theme.colors.background,
+ },
+ skeletonRoot: {
+ flex: 1,
+ },
+ skeletonBlock: {
+ backgroundColor: theme.colors.layerGrayLighter,
+ borderRadius: SKELETON_BORDER_RADIUS,
+ },
+ skeletonPrimaryValue: {
+ height: theme.spacing.xxl,
+ width: '50%',
+ },
+ skeletonSecondaryValue: {
+ height: theme.spacing.xl,
+ width: '35%',
+ },
+ skeletonTrend: {
+ height: theme.spacing.xl,
+ width: '20%',
+ },
+ skeletonChart: {
+ width: '100%',
+ height: CHART_HEIGHT,
+ },
+ skeletonPeriodRow: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: theme.spacing.sm,
+ marginTop: theme.spacing.xs,
+ },
+ skeletonPeriodButton: {
+ width: theme.spacing['3xl'],
+ height: theme.spacing.xl,
+ },
+ skeletonButtonRow: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: theme.spacing.md,
+ marginTop: theme.spacing.xl,
+ },
+ skeletonActionButton: {
+ width: theme.spacing['4xl'],
+ height: theme.spacing['4xl'],
+ borderRadius: theme.spacing['3xl'],
},
}
})
diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverview.ts b/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverview.ts
index a3d7980af..9a3c4d55d 100644
--- a/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverview.ts
+++ b/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverview.ts
@@ -10,13 +10,26 @@
limitations under the License
*/
-import { useCallback, useState } from 'react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
import {
+ useAccountBalancesHistoryQuery,
+ useAccountBalancesQuery,
useSelectedAccount,
WalletAccount,
} from '@perawallet/wallet-core-accounts'
+import { HistoryPeriod } from '@perawallet/wallet-core-shared'
import { useModalState } from '@hooks/useModalState'
import { useReceiveFunds } from '@modules/transactions/hooks'
+import { UseAccountOverviewModalResult } from './AccountOverviewModalContext'
+
+// Matches the default in useChartInteraction so balances + history queries
+// share keys with the header's chart and we don't double-fetch.
+const INITIAL_HISTORY_PERIOD: HistoryPeriod = 'one-week'
+
+export type UseAccountOverviewParams = {
+ account: WalletAccount
+ onSwipeEnabledChange?: (enabled: boolean) => void
+}
export type UseAccountOverviewResult = {
isSendFundsVisible: boolean
@@ -30,11 +43,14 @@ export type UseAccountOverviewResult = {
handleCloseAccountOptions: () => void
scrollingEnabled: boolean
onScrollEnabledChange: (enabled: boolean) => void
+ isLoading: boolean
+ contextValue: UseAccountOverviewModalResult
}
-export const useAccountOverview = (
- _account: WalletAccount,
-): UseAccountOverviewResult => {
+export const useAccountOverview = ({
+ account,
+ onSwipeEnabledChange,
+}: UseAccountOverviewParams): UseAccountOverviewResult => {
const selectedAccount = useSelectedAccount()
const { setSelectedAccount, setCanSelectAccount } = useReceiveFunds()
@@ -71,6 +87,44 @@ export const useAccountOverview = (
const [scrollingEnabled, setScrollingEnabled] = useState(true)
+ useEffect(() => {
+ onSwipeEnabledChange?.(scrollingEnabled)
+ }, [scrollingEnabled, onSwipeEnabledChange])
+
+ // Combine balance + history pending into one sticky `isLoading` flag so the
+ // skeleton header reveals as a single beat and doesn't reappear when the
+ // user later changes period.
+ const { isPending: isBalancesPending } = useAccountBalancesQuery(
+ account ? [account] : [],
+ )
+ const { isPending: isHistoryPending } = useAccountBalancesHistoryQuery(
+ account ? [account.address] : [],
+ INITIAL_HISTORY_PERIOD,
+ )
+ const [hasCompletedInitialLoad, setHasCompletedInitialLoad] =
+ useState(false)
+ useEffect(() => {
+ if (
+ !hasCompletedInitialLoad &&
+ !isBalancesPending &&
+ !isHistoryPending
+ ) {
+ setHasCompletedInitialLoad(true)
+ }
+ }, [hasCompletedInitialLoad, isBalancesPending, isHistoryPending])
+ const isLoading = !hasCompletedInitialLoad
+
+ const contextValue = useMemo(
+ () => ({
+ account,
+ openSendFunds,
+ openReceiveFunds,
+ openAccountOptions,
+ onScrollEnabledChange: setScrollingEnabled,
+ }),
+ [account, openSendFunds, openReceiveFunds, openAccountOptions],
+ )
+
return {
isSendFundsVisible,
openSendFunds,
@@ -83,5 +137,7 @@ export const useAccountOverview = (
handleCloseAccountOptions,
scrollingEnabled,
onScrollEnabledChange: setScrollingEnabled,
+ isLoading,
+ contextValue,
}
}
diff --git a/apps/mobile/src/modules/accounts/components/BackupReminderBanner/BackupReminderBanner.tsx b/apps/mobile/src/modules/accounts/components/BackupReminderBanner/BackupReminderBanner.tsx
index 7742292a4..bc538c131 100644
--- a/apps/mobile/src/modules/accounts/components/BackupReminderBanner/BackupReminderBanner.tsx
+++ b/apps/mobile/src/modules/accounts/components/BackupReminderBanner/BackupReminderBanner.tsx
@@ -10,52 +10,104 @@
limitations under the License
*/
-import { memo } from 'react'
+import { memo, useEffect, useState } from 'react'
+import Animated, {
+ withTiming,
+ type EntryAnimationsValues,
+} from 'react-native-reanimated'
import type { WalletAccount } from '@perawallet/wallet-core-accounts'
import { PWIcon, PWText, PWTouchableOpacity, PWView } from '@components/core'
import { useLanguage } from '@hooks/useLanguage'
+import {
+ BACKUP_REMINDER_BANNER_REVEAL_DELAY,
+ BACKUP_REMINDER_BANNER_REVEAL_DURATION,
+} from '@constants/ui'
import { useBackupReminderBanner } from './useBackupReminderBanner'
import { useStyles } from './styles'
+// Entering animation: grow height from 0 to natural while fading in. Uses the
+// `targetHeight` reanimated provides so the banner ends at its real height
+// without us hardcoding it.
+const expandAndFadeEntering = (values: EntryAnimationsValues) => {
+ 'worklet'
+ return {
+ initialValues: {
+ opacity: 0,
+ height: 0,
+ },
+ animations: {
+ opacity: withTiming(1, {
+ duration: BACKUP_REMINDER_BANNER_REVEAL_DURATION,
+ }),
+ height: withTiming(values.targetHeight, {
+ duration: BACKUP_REMINDER_BANNER_REVEAL_DURATION,
+ }),
+ },
+ }
+}
+
type BackupReminderBannerProps = {
account: WalletAccount
+ isLoading?: boolean
}
const BackupReminderBannerComponent = ({
account,
+ isLoading = false,
}: BackupReminderBannerProps) => {
const styles = useStyles()
const { t } = useLanguage()
const { isVisible, onPress } = useBackupReminderBanner(account)
+ // The reveal-delay timer starts only after the parent finishes its initial
+ // load, so the banner animates in as a second beat instead of riding the
+ // skeleton-to-content swap.
+ const [hasDelayed, setHasDelayed] = useState(false)
+
+ useEffect(() => {
+ if (isLoading || hasDelayed) return
+ const timer = setTimeout(
+ () => setHasDelayed(true),
+ BACKUP_REMINDER_BANNER_REVEAL_DELAY,
+ )
+ return () => clearTimeout(timer)
+ }, [isLoading, hasDelayed])
- if (!isVisible) return null
+ if (!isVisible || !hasDelayed) return null
return (
-
-
- {t('backup.banner.text')}
-
- {t('backup.banner.cta')}
-
-
+
+ {t('backup.banner.text')}
+
+
+ {t('backup.banner.cta')}
+
+
+
+
)
}
// Memoized: this banner lives in the asset-list header and re-renders on every
-// parent render. Skipping when `account` reference is stable saves a Zustand
-// selector + TanStack subscription pass per render.
+// parent render. Skipping when inputs are stable saves a Zustand selector +
+// TanStack subscription pass per render.
export const BackupReminderBanner = memo(
BackupReminderBannerComponent,
- (prev, next) => prev.account === next.account,
+ (prev, next) =>
+ prev.account === next.account && prev.isLoading === next.isLoading,
)
diff --git a/apps/mobile/src/modules/accounts/components/BackupReminderBanner/__tests__/BackupReminderBanner.spec.tsx b/apps/mobile/src/modules/accounts/components/BackupReminderBanner/__tests__/BackupReminderBanner.spec.tsx
index 5b851b04d..6a1b80440 100644
--- a/apps/mobile/src/modules/accounts/components/BackupReminderBanner/__tests__/BackupReminderBanner.spec.tsx
+++ b/apps/mobile/src/modules/accounts/components/BackupReminderBanner/__tests__/BackupReminderBanner.spec.tsx
@@ -10,8 +10,8 @@
limitations under the License
*/
-import { describe, test, expect, vi } from 'vitest'
-import React from 'react'
+import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
+import React, { act } from 'react'
import { render, screen, fireEvent } from '@test-utils/render'
import {
AccountTypes,
@@ -36,6 +36,7 @@ vi.mock('@hooks/useLanguage', () => ({
}))
import { BackupReminderBanner } from '../BackupReminderBanner'
+import { BACKUP_REMINDER_BANNER_REVEAL_DELAY } from '@constants/ui'
const account: WalletAccount = {
type: AccountTypes.algo25,
@@ -43,18 +44,41 @@ const account: WalletAccount = {
keyPairId: 'kp',
}
+const advancePastRevealDelay = () => {
+ act(() => {
+ vi.advanceTimersByTime(BACKUP_REMINDER_BANNER_REVEAL_DELAY)
+ })
+}
+
describe('BackupReminderBanner', () => {
+ beforeEach(() => {
+ vi.useFakeTimers()
+ })
+
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
test('renders nothing when isVisible is false', () => {
mockUseHook.mockReturnValue({ isVisible: false, onPress: vi.fn() })
const { queryByTestId } = render(
,
)
+ advancePastRevealDelay()
expect(queryByTestId('backup_reminder_banner')).toBeNull()
})
- test('renders warning text and CTA when isVisible is true', () => {
+ test('renders nothing before the reveal delay elapses', () => {
+ mockUseHook.mockReturnValue({ isVisible: true, onPress: vi.fn() })
+ render()
+
+ expect(screen.queryByTestId('backup_reminder_banner')).toBeNull()
+ })
+
+ test('renders warning text and CTA after the reveal delay', () => {
mockUseHook.mockReturnValue({ isVisible: true, onPress: vi.fn() })
render()
+ advancePastRevealDelay()
expect(
screen.getByText('You need to backup the passphrase'),
@@ -66,6 +90,7 @@ describe('BackupReminderBanner', () => {
const onPress = vi.fn()
mockUseHook.mockReturnValue({ isVisible: true, onPress })
render()
+ advancePastRevealDelay()
fireEvent.click(screen.getByTestId('backup_reminder_banner_cta'))
expect(onPress).toHaveBeenCalledTimes(1)
diff --git a/apps/mobile/src/modules/accounts/components/BackupReminderBanner/styles.ts b/apps/mobile/src/modules/accounts/components/BackupReminderBanner/styles.ts
index ca9a0832b..4c7104d34 100644
--- a/apps/mobile/src/modules/accounts/components/BackupReminderBanner/styles.ts
+++ b/apps/mobile/src/modules/accounts/components/BackupReminderBanner/styles.ts
@@ -14,6 +14,11 @@ import { makeStyles } from '@rneui/themed'
import { getTypography } from '@theme/typography'
export const useStyles = makeStyles(theme => ({
+ enterWrapper: {
+ // Clip while the entering animation grows height from 0; otherwise
+ // children overflow visibly during the animation.
+ overflow: 'hidden',
+ },
container: {
flexDirection: 'row',
alignItems: 'center',
diff --git a/apps/mobile/src/modules/assets/components/AssetItem/AccountAssetItemView.tsx b/apps/mobile/src/modules/assets/components/AssetItem/AccountAssetItemView.tsx
index dacee7e5b..eb57f50e8 100644
--- a/apps/mobile/src/modules/assets/components/AssetItem/AccountAssetItemView.tsx
+++ b/apps/mobile/src/modules/assets/components/AssetItem/AccountAssetItemView.tsx
@@ -30,6 +30,7 @@ import {
useAssetsQuery,
} from '@perawallet/wallet-core-assets'
import { AssetWithAccountBalance } from '@perawallet/wallet-core-accounts'
+import { useLanguage } from '@hooks/useLanguage'
import { getVerificationIcon } from '@modules/assets/utils/verification'
import { useStyles } from './styles'
import { useMemo } from 'react'
@@ -49,6 +50,7 @@ export const AccountAssetItemView = ({
...rest
}: AccountAssetItemViewProps) => {
const styles = useStyles()
+ const { t } = useLanguage()
// Use pre-fetched asset data when available to avoid N+1 queries.
// Falls back to individual fetch for callers that don't populate accountBalance.asset.
@@ -83,6 +85,7 @@ export const AccountAssetItemView = ({
}, [asset, isAlgo])
const isFavorited = asset?.peraMetadata?.isFavorited ?? false
+ const isDeleted = asset?.peraMetadata?.isDeleted === true
const item = useMemo(() => {
if (asset && isCollectible(asset)) {
@@ -169,14 +172,23 @@ export const AccountAssetItemView = ({
/>
) : null}
-
+ {isDeleted ? (
- {secondaryText}
+ {t('asset.deleted_label')}
-
+ ) : (
+
+
+ {secondaryText}
+
+
+ )}
{
color: theme.colors.error,
flexShrink: 1,
},
+ deletedLabel: {
+ color: theme.colors.negative,
+ },
primaryUnit: {
flexShrink: 1,
},
diff --git a/apps/mobile/src/modules/assets/components/AssetTitle/AssetTitle.tsx b/apps/mobile/src/modules/assets/components/AssetTitle/AssetTitle.tsx
index 40b7cb425..f386bc86a 100644
--- a/apps/mobile/src/modules/assets/components/AssetTitle/AssetTitle.tsx
+++ b/apps/mobile/src/modules/assets/components/AssetTitle/AssetTitle.tsx
@@ -13,6 +13,7 @@
import { ALGO_ASSET_ID, PeraAsset } from '@perawallet/wallet-core-assets'
import { PWIcon, PWText, PWView } from '@components/core'
import { CopyableText } from '@components/CopyableText'
+import { useLanguage } from '@hooks/useLanguage'
import { useStyles } from './styles'
import { AssetIcon } from '../AssetIcon'
import { useMemo } from 'react'
@@ -30,6 +31,7 @@ export const AssetTitle = ({
nameVariant = 'h4',
}: AssetTitleProps) => {
const styles = useStyles()
+ const { t } = useLanguage()
const isAlgo = useMemo(
() => asset.assetId === ALGO_ASSET_ID,
@@ -41,6 +43,8 @@ export const AssetTitle = ({
[asset.peraMetadata?.verificationTier],
)
+ const isDeleted = asset.peraMetadata?.isDeleted === true
+
return (
)}
- {showId && (
-
-
- {asset.assetId}
-
-
+ {isDeleted ? (
+
+ {t('asset.deleted_label')}
+
+ ) : (
+ showId && (
+
+
+ {asset.assetId}
+
+
+ )
)}
diff --git a/apps/mobile/src/modules/assets/components/AssetTitle/styles.ts b/apps/mobile/src/modules/assets/components/AssetTitle/styles.ts
index 720a8d982..6f651ddf9 100644
--- a/apps/mobile/src/modules/assets/components/AssetTitle/styles.ts
+++ b/apps/mobile/src/modules/assets/components/AssetTitle/styles.ts
@@ -29,6 +29,10 @@ export const useStyles = makeStyles(theme => {
paddingRight: theme.spacing.xs,
flexShrink: 1,
}
+ const deletedLabel = {
+ color: theme.colors.negative,
+ lineHeight: theme.spacing.lg,
+ }
return {
container: {
flexDirection: 'row',
@@ -49,5 +53,6 @@ export const useStyles = makeStyles(theme => {
id,
name,
suspiciousName,
+ deletedLabel,
}
})
diff --git a/apps/mobile/src/modules/assets/components/holdings/AssetWealthChart/AssetWealthChart.tsx b/apps/mobile/src/modules/assets/components/holdings/AssetWealthChart/AssetWealthChart.tsx
index b7b9ed7e8..87b7fa301 100644
--- a/apps/mobile/src/modules/assets/components/holdings/AssetWealthChart/AssetWealthChart.tsx
+++ b/apps/mobile/src/modules/assets/components/holdings/AssetWealthChart/AssetWealthChart.tsx
@@ -32,6 +32,7 @@ import {
CHART_FOCUS_DEBOUNCE_TIME,
CHART_HEIGHT,
} from '@constants/ui'
+import { getChartYAxisRange } from '@utils/chart'
type DataPoint = {
timestamp: Date
@@ -72,16 +73,10 @@ export const AssetWealthChart = ({
}) ?? []) as DataPoint[]
}, [data])
- const yAxisOffsets = useMemo(() => {
- if (dataPoints.length === 0) return [-1, 1]
- const minValue = Math.min(...dataPoints.map(p => p.value))
- const maxValue = Math.max(...dataPoints.map(p => p.value))
- if (minValue === 0 && maxValue === 0) {
- return [-1, 1]
- } else {
- return [minValue - minValue / 10, maxValue + maxValue / 10]
- }
- }, [dataPoints])
+ const yAxisRange = useMemo(
+ () => getChartYAxisRange(dataPoints),
+ [dataPoints],
+ )
const onFocus = useCallback(
({
@@ -114,18 +109,14 @@ export const AssetWealthChart = ({
],
)
- if (isPending) {
- return (
-
- )
- }
-
return (
- {!dataPoints?.length ? (
+ {isPending ? (
+
+ ) : !dataPoints?.length ? (
({
container: {
- flex: 1,
+ height: CHART_HEIGHT,
backgroundColor: theme.colors.background,
},
}))
diff --git a/apps/mobile/src/modules/assets/components/market/AssetPriceChart/AssetPriceChart.tsx b/apps/mobile/src/modules/assets/components/market/AssetPriceChart/AssetPriceChart.tsx
index 0c37a82e0..757f3c635 100644
--- a/apps/mobile/src/modules/assets/components/market/AssetPriceChart/AssetPriceChart.tsx
+++ b/apps/mobile/src/modules/assets/components/market/AssetPriceChart/AssetPriceChart.tsx
@@ -30,6 +30,7 @@ import {
CHART_HEIGHT,
} from '@constants/ui'
import { useLanguage } from '@hooks/useLanguage'
+import { getChartYAxisRange } from '@utils/chart'
export type AssetPriceChartProps = {
asset: PeraAsset
@@ -61,16 +62,10 @@ export const AssetPriceChart = ({
[data],
)
- const yAxisOffsets = useMemo(() => {
- if (dataPoints.length === 0) return [-1, 1]
- const minValue = Math.min(...dataPoints.map(p => p.value))
- const maxValue = Math.max(...dataPoints.map(p => p.value))
- if (minValue === 0 && maxValue === 0) {
- return [-1, 1]
- } else {
- return [minValue - minValue / 10, maxValue + maxValue / 10]
- }
- }, [dataPoints])
+ const yAxisRange = useMemo(
+ () => getChartYAxisRange(dataPoints),
+ [dataPoints],
+ )
const onFocus = useCallback(
({
@@ -103,18 +98,14 @@ export const AssetPriceChart = ({
],
)
- if (isPending) {
- return (
-
- )
- }
-
return (
- {!dataPoints?.length ? (
+ {isPending ? (
+
+ ) : !dataPoints?.length ? (
({
container: {
- flex: 1,
+ height: CHART_HEIGHT,
backgroundColor: theme.colors.background,
},
}))
diff --git a/apps/mobile/src/utils/__tests__/chart.spec.ts b/apps/mobile/src/utils/__tests__/chart.spec.ts
new file mode 100644
index 000000000..dda584f2f
--- /dev/null
+++ b/apps/mobile/src/utils/__tests__/chart.spec.ts
@@ -0,0 +1,88 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+import { describe, it, expect } from 'vitest'
+import { getChartYAxisRange } from '../chart'
+
+// Mirrors gifted-charts' position formula: (value - yAxisOffset) / maxValue.
+const peakPosition = (
+ { yAxisOffset, maxValue }: { yAxisOffset: number; maxValue: number },
+ dataMax: number,
+): number => (dataMax - yAxisOffset) / maxValue
+
+const toPoints = (values: number[]) => values.map(value => ({ value }))
+
+describe('getChartYAxisRange', () => {
+ it('returns a symmetric range for empty data', () => {
+ const result = getChartYAxisRange([])
+ expect(result.yAxisOffset).toBe(-1)
+ expect(result.maxValue).toBe(2)
+ })
+
+ it('returns a symmetric range when every value is zero', () => {
+ const result = getChartYAxisRange(toPoints([0, 0, 0]))
+ expect(result.yAxisOffset).toBe(-1)
+ expect(result.maxValue).toBe(2)
+ })
+
+ it('places the peak at ~91.7% for a flat-baseline dataset with small variation', () => {
+ const result = getChartYAxisRange(toPoints([999.5, 1000, 1000.5]))
+ expect(peakPosition(result, 1000.5)).toBeCloseTo(11 / 12, 5)
+ })
+
+ it('places the peak at ~91.7% for a wide-variation positive dataset', () => {
+ const result = getChartYAxisRange(toPoints([100, 500, 1000]))
+ expect(peakPosition(result, 1000)).toBeCloseTo(11 / 12, 5)
+ })
+
+ it('produces a non-zero range that brackets the constant for flat positive data', () => {
+ const { yAxisOffset, maxValue } = getChartYAxisRange(
+ toPoints([100, 100, 100]),
+ )
+ expect(yAxisOffset).toBeLessThan(100)
+ expect(yAxisOffset + maxValue).toBeGreaterThan(100)
+ expect(maxValue).toBeGreaterThan(0)
+ })
+
+ it('produces a non-zero range that brackets the constant for flat negative data', () => {
+ const { yAxisOffset, maxValue } = getChartYAxisRange(
+ toPoints([-100, -100, -100]),
+ )
+ expect(yAxisOffset).toBeLessThan(-100)
+ expect(yAxisOffset + maxValue).toBeGreaterThan(-100)
+ expect(maxValue).toBeGreaterThan(0)
+ })
+
+ it('produces a valid range for all-negative variation', () => {
+ const result = getChartYAxisRange(toPoints([-1000, -750, -500]))
+ const { yAxisOffset, maxValue } = result
+ expect(maxValue).toBeGreaterThan(0)
+ expect(yAxisOffset).toBeLessThan(-1000)
+ expect(yAxisOffset + maxValue).toBeGreaterThan(-500)
+ expect(peakPosition(result, -500)).toBeCloseTo(11 / 12, 5)
+ })
+
+ it('produces a valid range for mixed-sign data spanning zero', () => {
+ const result = getChartYAxisRange(toPoints([-200, 0, 800]))
+ const { yAxisOffset, maxValue } = result
+ expect(yAxisOffset).toBeLessThan(-200)
+ expect(yAxisOffset + maxValue).toBeGreaterThan(800)
+ expect(peakPosition(result, 800)).toBeCloseTo(11 / 12, 5)
+ })
+
+ it('treats a single data point as flat and returns a valid bracketing range', () => {
+ const { yAxisOffset, maxValue } = getChartYAxisRange(toPoints([42]))
+ expect(yAxisOffset).toBeLessThan(42)
+ expect(yAxisOffset + maxValue).toBeGreaterThan(42)
+ expect(maxValue).toBeGreaterThan(0)
+ })
+})
diff --git a/apps/mobile/src/utils/chart.ts b/apps/mobile/src/utils/chart.ts
new file mode 100644
index 000000000..466d6ad77
--- /dev/null
+++ b/apps/mobile/src/utils/chart.ts
@@ -0,0 +1,49 @@
+/*
+ Copyright 2022-2025 Pera Wallet, LDA
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License
+ */
+
+const PADDING_RATIO = 0.1
+
+// gifted-charts positions each point at (value - yAxisOffset) / maxValue,
+// so maxValue is the chart's *range*, not its top. The chart spans
+// [yAxisOffset, yAxisOffset + maxValue] in data-value space.
+export type ChartYAxisRange = {
+ yAxisOffset: number
+ maxValue: number
+}
+
+const FALLBACK: ChartYAxisRange = { yAxisOffset: -1, maxValue: 2 }
+
+export const getChartYAxisRange = (
+ dataPoints: readonly { value: number }[],
+): ChartYAxisRange => {
+ if (dataPoints.length === 0) return FALLBACK
+
+ let dataMin = Infinity
+ let dataMax = -Infinity
+ for (const point of dataPoints) {
+ if (point.value < dataMin) dataMin = point.value
+ if (point.value > dataMax) dataMax = point.value
+ }
+
+ const range = dataMax - dataMin
+ if (range > 0) {
+ const padding = range * PADDING_RATIO
+ return { yAxisOffset: dataMin - padding, maxValue: range + 2 * padding }
+ }
+
+ if (dataMax !== 0) {
+ const padding = Math.abs(dataMax) * PADDING_RATIO
+ return { yAxisOffset: dataMax - padding, maxValue: 2 * padding }
+ }
+
+ return FALLBACK
+}
diff --git a/packages/assets/src/api/assets/__tests__/endpoints.test.ts b/packages/assets/src/api/assets/__tests__/endpoints.test.ts
index f6bc5110b..29eb331c5 100644
--- a/packages/assets/src/api/assets/__tests__/endpoints.test.ts
+++ b/packages/assets/src/api/assets/__tests__/endpoints.test.ts
@@ -59,7 +59,7 @@ describe('assets endpoints', () => {
queryClientMock.mockReset()
})
- test('fetchAssets calls /v1/assets/ with comma-joined asset_ids', async () => {
+ test('fetchAssets calls /v1/assets/ with comma-joined asset_ids and include_deleted', async () => {
queryClientMock.mockResolvedValue({ data: validAssetsResponse })
await fetchAssets(['1', '2', '3'], 'mainnet')
@@ -67,7 +67,7 @@ describe('assets endpoints', () => {
expect(queryClientMock).toHaveBeenCalledWith(
expect.objectContaining({
url: '/v1/assets/',
- params: { asset_ids: '1,2,3' },
+ params: { asset_ids: '1,2,3', include_deleted: true },
}),
)
})
diff --git a/packages/assets/src/api/assets/endpoints.ts b/packages/assets/src/api/assets/endpoints.ts
index 002ae8fed..8a980bf27 100644
--- a/packages/assets/src/api/assets/endpoints.ts
+++ b/packages/assets/src/api/assets/endpoints.ts
@@ -30,6 +30,7 @@ export const fetchAssets = async (assetIDs: string[], network: Network) => {
url: `/v1/assets/`,
params: {
asset_ids: assetIDs.join(','),
+ include_deleted: true,
},
})