diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverview.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverview.tsx index 739833897..849e282ca 100644 --- a/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverview.tsx +++ b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverview.tsx @@ -10,7 +10,7 @@ limitations under the License */ -import { useEffect } from 'react' +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,6 +20,10 @@ import { useAccountOverview } from './useAccountOverview' import { PWView } from '@components/core' import { AccountAssetList } from '../AccountAssetList' import { AccountOptionsBottomSheet } from '../AccountOptionsBottomSheet' +import { + AccountOverviewModalContext, + AccountOverviewModalContextValue, +} from './AccountOverviewModalContext' export type AccountOverviewProps = { account: WalletAccount @@ -34,81 +38,72 @@ export const AccountOverview = ({ }: AccountOverviewProps) => { const styles = useStyles() const { - portfolioAlgoValue, - isPending, - period, - setPeriod, - selectedPoint, - scrollingEnabled, - preferredCurrency, - hasBalance, - togglePrivacyMode, - handleChartSelectionChange, isSendFundsVisible, - handleOpenSendFunds, + openSendFunds, handleCloseSendFunds, - handleSwap, - handleMore, - handleBuyAlgo, - handleReceive, - handleCopyAddress, - handleShowQR, 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, + ], + ) + return ( - - - } - /> + + + + } + /> - + - + - - + + + ) } diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeader.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeader.tsx index 24bab0525..59e0b0fc8 100644 --- a/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeader.tsx +++ b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewHeader.tsx @@ -14,7 +14,6 @@ import { PWText, PWTouchableOpacity, PWView } from '@components/core' import { DEFAULT_PRECISION, formatDatetime, - HistoryPeriod, } from '@perawallet/wallet-core-shared' import { CurrencyDisplay } from '@components/CurrencyDisplay' import Decimal from 'decimal.js' @@ -23,11 +22,7 @@ import { ButtonPanel } from '../ButtonPanel' import { useStyles } from './styles' import { WealthTrend } from '@components/WealthTrend' import { ChartPeriodSelection } from '@components/ChartPeriodSelection' -import { - AccountBalanceHistoryItem, - isWatchAccount, - WalletAccount, -} from '@perawallet/wallet-core-accounts' +import { isWatchAccount, WalletAccount } from '@perawallet/wallet-core-accounts' import { useLanguage } from '@hooks/useLanguage' import { NoFundsButtonPanel } from '../NoFundsButtonPanel' @@ -35,51 +30,29 @@ import { WatchAccountButtonPanel } from '../WatchAccountButtonPanel' import { ALGO_ASSET } from '@perawallet/wallet-core-assets' import { PreferredCurrencyDisplay } from '@components/PreferredCurrencyDisplay' import { ExpandablePanel } from '@components/ExpandablePanel' +import { useAccountOverviewHeader } from './useAccountOverviewHeader' export type AccountOverviewHeaderProps = { account: WalletAccount - hasBalance: boolean - portfolioAlgoValue: Decimal - isPending: boolean - period: HistoryPeriod - setPeriod: (period: HistoryPeriod) => void - selectedPoint: AccountBalanceHistoryItem | null - preferredCurrency: string - togglePrivacyMode: () => void - handleChartSelectionChange: ( - selected: AccountBalanceHistoryItem | null, - ) => void - handleSwap: () => void - handleOpenSendFunds: () => void - handleMore: () => void - handleBuyAlgo: () => void - handleReceive: () => void - handleCopyAddress: () => void - handleShowQR: () => void chartVisible: boolean } export const AccountOverviewHeader = ({ account, - hasBalance, - portfolioAlgoValue, - isPending, - period, - setPeriod, - selectedPoint, - togglePrivacyMode, - handleChartSelectionChange, - handleSwap, - handleOpenSendFunds, - handleMore, - handleBuyAlgo, - handleReceive, - handleCopyAddress, - handleShowQR, chartVisible, }: AccountOverviewHeaderProps) => { const styles = useStyles() const { t } = useLanguage() + const { + portfolioAlgoValue, + isPending, + period, + setPeriod, + selectedPoint, + hasBalance, + togglePrivacyMode, + handleChartSelectionChange, + } = useAccountOverviewHeader(account) return hasBalance || isPending ? ( @@ -151,29 +124,16 @@ export const AccountOverviewHeader = ({ {isWatchAccount(account) ? ( - + ) : ( - + )} ) : ( {isWatchAccount(account) ? ( - + ) : ( <> @@ -190,11 +150,7 @@ export const AccountOverviewHeader = ({ {t('account_details.no_balance.get_started')} - + )} diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewModalContext.tsx b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewModalContext.tsx new file mode 100644 index 000000000..1d3f5c9cb --- /dev/null +++ b/apps/mobile/src/modules/accounts/components/AccountOverview/AccountOverviewModalContext.tsx @@ -0,0 +1,43 @@ +/* + 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 { createContext, useContext } from 'react' +import { WalletAccount } from '@perawallet/wallet-core-accounts' + +export type AccountOverviewModalContextValue = { + account: WalletAccount + openSendFunds: () => void + openReceiveFunds: () => void + openAccountOptions: () => void + onScrollEnabledChange: (enabled: boolean) => void +} + +export const AccountOverviewModalContext = + createContext(null) + +export type UseAccountOverviewModalResult = { + account: WalletAccount + openSendFunds: () => void + openReceiveFunds: () => void + openAccountOptions: () => void + onScrollEnabledChange: (enabled: boolean) => void +} + +export const useAccountOverviewModal = (): UseAccountOverviewModalResult => { + const context = useContext(AccountOverviewModalContext) + if (context === null) { + throw new Error( + 'useAccountOverviewModal must be used within AccountOverviewModalContext.Provider', + ) + } + return context +} 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 062d9694d..d52e95efa 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 @@ -23,6 +23,7 @@ const { mockShowToast } = vi.hoisted(() => ({ mockShowToast: vi.fn() })) vi.mock('@hooks/useAppNavigation', () => ({ useAppNavigation: () => ({ navigate: mockNavigate, + replace: vi.fn(), }), })) @@ -163,35 +164,54 @@ vi.mock('@components/WealthTrend', () => ({ WealthTrend: () => null, })) -// Mock sub-components to keep test focused +// Mock sub-components — they now use internal hooks that consume context. +// We use the real context via useContext so button presses trigger real modal state changes. vi.mock('../../ButtonPanel', () => ({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ButtonPanel: (props: any) => ( -
- -
- ), -})) -vi.mock('../../NoFundsButtonPanel', () => ({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - NoFundsButtonPanel: (props: any) => ( -
- - - -
- ), -})) -vi.mock('../../WatchAccountButtonPanel', () => ({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - WatchAccountButtonPanel: (props: any) => ( -
- - - -
- ), + ButtonPanel: () =>
, })) + +vi.mock('../../NoFundsButtonPanel', async () => { + const React = await import('react') + const { AccountOverviewModalContext } = + await import('../AccountOverviewModalContext') + return { + NoFundsButtonPanel: () => { + const context = React.useContext(AccountOverviewModalContext) + return ( +
+ + + +
+ ) + }, + } +}) + +vi.mock('../../WatchAccountButtonPanel', async () => { + const React = await import('react') + const { AccountOverviewModalContext } = + await import('../AccountOverviewModalContext') + return { + WatchAccountButtonPanel: () => { + const context = React.useContext(AccountOverviewModalContext) + return ( +
+ + + +
+ ) + }, + } +}) + vi.mock('../../AccountAssetList', () => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any AccountAssetList: ({ header, children }: any) => ( @@ -238,7 +258,6 @@ describe('AccountOverview', () => { chartVisible={true} />, ) - // Primary ALGO value and secondary preferred currency value expect(screen.getByTestId('currency-display')).toBeTruthy() expect(screen.getByTestId('preferred-currency-display')).toBeTruthy() expect(screen.getAllByText('100').length).toBeGreaterThanOrEqual(1) 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 e4149f8f1..1682fffd0 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 @@ -12,82 +12,22 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { renderHook, act } from '@testing-library/react' -import Decimal from 'decimal.js' import { useAccountOverview } from '../useAccountOverview' import { WalletAccount } from '@perawallet/wallet-core-accounts' -const { mockNavigate, mockReplace } = vi.hoisted(() => ({ - mockNavigate: vi.fn(), - mockReplace: vi.fn(), -})) -const { mockShowToast } = vi.hoisted(() => ({ mockShowToast: vi.fn() })) - -vi.mock('@hooks/useAppNavigation', () => ({ - useAppNavigation: () => ({ - navigate: mockNavigate, - replace: mockReplace, - }), -})) - -vi.mock('@hooks/useToast', () => ({ - useToast: () => ({ - showToast: mockShowToast, - }), -})) - -vi.mock('@hooks/useLanguage', () => ({ - useLanguage: () => ({ - t: (key: string) => key, - }), +const { mockSetSelectedAccount, mockSetCanSelectAccount } = vi.hoisted(() => ({ + mockSetSelectedAccount: vi.fn(), + mockSetCanSelectAccount: vi.fn(), })) vi.mock('@perawallet/wallet-core-accounts', () => ({ - useAccountBalancesQuery: vi.fn(() => ({ - portfolioAlgoValue: new Decimal('100'), - isPending: false, - accountBalances: new Map(), - isFetched: true, - isRefetching: false, - isError: false, - })), - usePortfolioTotals: vi.fn(() => ({ - portfolioUsdValue: new Decimal('200'), - accountUsdValues: new Map(), - isPending: false, - })), - useSelectedAccount: vi.fn(() => undefined), -})) - -vi.mock('@perawallet/wallet-core-currencies', () => ({ - useCurrency: vi.fn(() => ({ - preferredCurrency: 'USD', - fallbackCurrency: 'USD', - usdToPreferred: vi.fn((amount: Decimal) => amount), - algoUsdPrice: new Decimal(0), - })), -})) - -vi.mock('@perawallet/wallet-core-settings', () => ({ - useSettings: vi.fn(() => ({ - privacyMode: false, - setPrivacyMode: vi.fn(), - })), + useSelectedAccount: vi.fn(() => ({ address: 'selected-address' })), })) vi.mock('@modules/transactions/hooks', () => ({ useReceiveFunds: vi.fn(() => ({ - setSelectedAccount: vi.fn(), - setCanSelectAccount: vi.fn(), - })), -})) - -vi.mock('@hooks/useChartInteraction', () => ({ - useChartInteraction: vi.fn(() => ({ - period: 'one-week' as const, - setPeriod: vi.fn(), - selectedPoint: null, - setSelectedPoint: vi.fn(), - clearSelection: vi.fn(), + setSelectedAccount: mockSetSelectedAccount, + setCanSelectAccount: mockSetCanSelectAccount, })), })) @@ -98,222 +38,113 @@ describe('useAccountOverview', () => { vi.clearAllMocks() }) - it('returns portfolio values from account balances query', () => { - const { result } = renderHook(() => useAccountOverview(mockAccount)) - - expect(result.current.portfolioAlgoValue.toString()).toBe('100') - expect(result.current.portfolioPreferredValue.toString()).toBe('200') - expect(result.current.preferredCurrency).toBe('USD') - }) - - it('determines hasBalance correctly when balance is greater than zero', () => { - const { result } = renderHook(() => useAccountOverview(mockAccount)) - - expect(result.current.hasBalance).toBe(true) - }) - - it('determines hasBalance correctly when balance is zero', async () => { - const { useAccountBalancesQuery } = - await import('@perawallet/wallet-core-accounts') - vi.mocked(useAccountBalancesQuery).mockReturnValue({ - portfolioAlgoValue: new Decimal('0'), - isPending: false, - accountBalances: new Map(), - isFetched: true, - isRefetching: false, - isError: false, - }) - + it('opens send funds modal when openSendFunds is called', () => { const { result } = renderHook(() => useAccountOverview(mockAccount)) - expect(result.current.hasBalance).toBe(false) - }) - - it('toggles privacy mode when togglePrivacyMode is called', async () => { - const setPrivacyMode = vi.fn() - const { useSettings } = await import('@perawallet/wallet-core-settings') - vi.mocked(useSettings).mockReturnValue({ - privacyMode: false, - setPrivacyMode, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any) - - const { result } = renderHook(() => useAccountOverview(mockAccount)) + expect(result.current.isSendFundsVisible).toBe(false) act(() => { - result.current.togglePrivacyMode() + result.current.openSendFunds() }) - expect(setPrivacyMode).toHaveBeenCalledWith(true) + expect(result.current.isSendFundsVisible).toBe(true) }) - it('navigates to Swap screen when handleSwap is called', () => { + it('closes send funds modal when handleCloseSendFunds is called', () => { const { result } = renderHook(() => useAccountOverview(mockAccount)) act(() => { - result.current.handleSwap() + result.current.openSendFunds() }) - expect(mockReplace).toHaveBeenCalledWith('TabBar', { screen: 'Swap' }) - }) - - it('navigates to Fund screen when handleBuyAlgo is called', () => { - const { result } = renderHook(() => useAccountOverview(mockAccount)) + expect(result.current.isSendFundsVisible).toBe(true) act(() => { - result.current.handleBuyAlgo() + result.current.handleCloseSendFunds() }) - expect(mockNavigate).toHaveBeenCalledWith('TabBar', { screen: 'Fund' }) + expect(result.current.isSendFundsVisible).toBe(false) }) - it('opens account options when handleMore is called', () => { + it('opens receive funds modal and sets selected account when openReceiveFunds is called', () => { const { result } = renderHook(() => useAccountOverview(mockAccount)) - expect(result.current.isAccountOptionsVisible).toBe(false) - - act(() => { - result.current.handleMore() - }) - - expect(result.current.isAccountOptionsVisible).toBe(true) - }) - - it('closes account options when handleCloseAccountOptions is called', () => { - const { result } = renderHook(() => useAccountOverview(mockAccount)) - - act(() => { - result.current.handleMore() - }) - - expect(result.current.isAccountOptionsVisible).toBe(true) + expect(result.current.isReceiveFundsVisible).toBe(false) act(() => { - result.current.handleCloseAccountOptions() + result.current.openReceiveFunds() }) - expect(result.current.isAccountOptionsVisible).toBe(false) - }) - - it('opens send funds modal when handleOpenSendFunds is called', () => { - const { result } = renderHook(() => useAccountOverview(mockAccount)) - - expect(result.current.isSendFundsVisible).toBe(false) - - act(() => { - result.current.handleOpenSendFunds() + expect(result.current.isReceiveFundsVisible).toBe(true) + expect(mockSetCanSelectAccount).toHaveBeenCalledWith(false) + expect(mockSetSelectedAccount).toHaveBeenCalledWith({ + address: 'selected-address', }) - - expect(result.current.isSendFundsVisible).toBe(true) }) - it('closes send funds modal when handleCloseSendFunds is called', () => { + it('closes receive funds modal when handleCloseReceiveFunds is called', () => { const { result } = renderHook(() => useAccountOverview(mockAccount)) act(() => { - result.current.handleOpenSendFunds() + result.current.openReceiveFunds() }) - expect(result.current.isSendFundsVisible).toBe(true) + expect(result.current.isReceiveFundsVisible).toBe(true) act(() => { - result.current.handleCloseSendFunds() + result.current.handleCloseReceiveFunds() }) - expect(result.current.isSendFundsVisible).toBe(false) + expect(result.current.isReceiveFundsVisible).toBe(false) }) - it('opens receive funds modal when handleReceive is called', () => { + it('opens account options when openAccountOptions is called', () => { const { result } = renderHook(() => useAccountOverview(mockAccount)) - expect(result.current.isReceiveFundsVisible).toBe(false) + expect(result.current.isAccountOptionsVisible).toBe(false) act(() => { - result.current.handleReceive() + result.current.openAccountOptions() }) - expect(result.current.isReceiveFundsVisible).toBe(true) + expect(result.current.isAccountOptionsVisible).toBe(true) }) - it('closes receive funds modal when handleCloseReceiveFunds is called', () => { + it('closes account options when handleCloseAccountOptions is called', () => { const { result } = renderHook(() => useAccountOverview(mockAccount)) act(() => { - result.current.handleReceive() + result.current.openAccountOptions() }) - expect(result.current.isReceiveFundsVisible).toBe(true) + expect(result.current.isAccountOptionsVisible).toBe(true) act(() => { - result.current.handleCloseReceiveFunds() + result.current.handleCloseAccountOptions() }) - expect(result.current.isReceiveFundsVisible).toBe(false) + expect(result.current.isAccountOptionsVisible).toBe(false) }) - it('disables scrolling when a chart point is selected', async () => { - const setSelectedPoint = vi.fn() - const { useChartInteraction } = - await import('@hooks/useChartInteraction') - vi.mocked(useChartInteraction).mockReturnValue({ - period: 'one-week' as const, - setPeriod: vi.fn(), - selectedPoint: null, - setSelectedPoint, - clearSelection: vi.fn(), - }) - + it('starts with scrolling enabled', () => { const { result } = renderHook(() => useAccountOverview(mockAccount)) expect(result.current.scrollingEnabled).toBe(true) - - const mockPoint = { - datetime: new Date(), - algoValue: new Decimal('100'), - preferredValue: new Decimal('200'), - round: 12345, - } - - act(() => { - result.current.handleChartSelectionChange(mockPoint) - }) - - expect(setSelectedPoint).toHaveBeenCalledWith(mockPoint) - expect(result.current.scrollingEnabled).toBe(false) }) - it('enables scrolling when chart selection is cleared', async () => { - const setSelectedPoint = vi.fn() - const { useChartInteraction } = - await import('@hooks/useChartInteraction') - vi.mocked(useChartInteraction).mockReturnValue({ - period: 'one-week' as const, - setPeriod: vi.fn(), - selectedPoint: null, - setSelectedPoint, - clearSelection: vi.fn(), - }) - + it('updates scrolling state when onScrollEnabledChange is called', () => { const { result } = renderHook(() => useAccountOverview(mockAccount)) - const mockPoint = { - datetime: new Date(), - algoValue: new Decimal('100'), - preferredValue: new Decimal('200'), - round: 12345, - } - act(() => { - result.current.handleChartSelectionChange(mockPoint) + result.current.onScrollEnabledChange(false) }) expect(result.current.scrollingEnabled).toBe(false) act(() => { - result.current.handleChartSelectionChange(null) + result.current.onScrollEnabledChange(true) }) - expect(setSelectedPoint).toHaveBeenCalledWith(null) expect(result.current.scrollingEnabled).toBe(true) }) }) 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 new file mode 100644 index 000000000..a9c9ec212 --- /dev/null +++ b/apps/mobile/src/modules/accounts/components/AccountOverview/__tests__/useAccountOverviewHeader.spec.tsx @@ -0,0 +1,207 @@ +/* + 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, vi, beforeEach } from 'vitest' +import { renderHook, act } from '@testing-library/react' +import React from 'react' +import Decimal from 'decimal.js' +import { useAccountOverviewHeader } from '../useAccountOverviewHeader' +import { WalletAccount } from '@perawallet/wallet-core-accounts' +import { + AccountOverviewModalContext, + AccountOverviewModalContextValue, +} from '../AccountOverviewModalContext' + +const { mockOnScrollEnabledChange } = vi.hoisted(() => ({ + mockOnScrollEnabledChange: vi.fn(), +})) + +vi.mock('@perawallet/wallet-core-accounts', () => ({ + useAccountBalancesQuery: vi.fn(() => ({ + portfolioAlgoValue: new Decimal('100'), + isPending: false, + accountBalances: new Map(), + isFetched: true, + isRefetching: false, + isError: false, + })), + usePortfolioTotals: vi.fn(() => ({ + portfolioUsdValue: new Decimal('200'), + accountUsdValues: new Map(), + isPending: false, + })), +})) + +vi.mock('@perawallet/wallet-core-currencies', () => ({ + useCurrency: vi.fn(() => ({ + preferredCurrency: 'USD', + fallbackCurrency: 'USD', + usdToPreferred: vi.fn((amount: Decimal) => amount), + algoUsdPrice: new Decimal(0), + })), +})) + +vi.mock('@perawallet/wallet-core-settings', () => ({ + useSettings: vi.fn(() => ({ + privacyMode: false, + setPrivacyMode: vi.fn(), + })), +})) + +vi.mock('@hooks/useChartInteraction', () => ({ + useChartInteraction: vi.fn(() => ({ + period: 'one-week' as const, + setPeriod: vi.fn(), + selectedPoint: null, + setSelectedPoint: vi.fn(), + clearSelection: vi.fn(), + })), +})) + +const mockContextValue: AccountOverviewModalContextValue = { + account: { address: 'test-address' } as WalletAccount, + openSendFunds: vi.fn(), + openReceiveFunds: vi.fn(), + openAccountOptions: vi.fn(), + onScrollEnabledChange: mockOnScrollEnabledChange, +} + +const wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + +) + +describe('useAccountOverviewHeader', () => { + const mockAccount = { address: 'test-address' } as WalletAccount + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('returns portfolio values from account balances query', () => { + const { result } = renderHook( + () => useAccountOverviewHeader(mockAccount), + { wrapper }, + ) + + expect(result.current.portfolioAlgoValue.toString()).toBe('100') + expect(result.current.portfolioPreferredValue.toString()).toBe('200') + }) + + it('determines hasBalance correctly when balance is greater than zero', () => { + const { result } = renderHook( + () => useAccountOverviewHeader(mockAccount), + { wrapper }, + ) + + expect(result.current.hasBalance).toBe(true) + }) + + it('determines hasBalance correctly when balance is zero', async () => { + const { useAccountBalancesQuery } = + await import('@perawallet/wallet-core-accounts') + vi.mocked(useAccountBalancesQuery).mockReturnValue({ + portfolioAlgoValue: new Decimal('0'), + isPending: false, + accountBalances: new Map(), + isFetched: true, + isRefetching: false, + isError: false, + }) + + const { result } = renderHook( + () => useAccountOverviewHeader(mockAccount), + { wrapper }, + ) + + expect(result.current.hasBalance).toBe(false) + }) + + it('toggles privacy mode when togglePrivacyMode is called', async () => { + const setPrivacyMode = vi.fn() + const { useSettings } = await import('@perawallet/wallet-core-settings') + vi.mocked(useSettings).mockReturnValue({ + privacyMode: false, + setPrivacyMode, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any) + + const { result } = renderHook( + () => useAccountOverviewHeader(mockAccount), + { wrapper }, + ) + + act(() => { + result.current.togglePrivacyMode() + }) + + expect(setPrivacyMode).toHaveBeenCalledWith(true) + }) + + it('calls onScrollEnabledChange(false) when a chart point is selected', async () => { + const setSelectedPoint = vi.fn() + const { useChartInteraction } = + await import('@hooks/useChartInteraction') + vi.mocked(useChartInteraction).mockReturnValue({ + period: 'one-week' as const, + setPeriod: vi.fn(), + selectedPoint: null, + setSelectedPoint, + clearSelection: vi.fn(), + }) + + const { result } = renderHook( + () => useAccountOverviewHeader(mockAccount), + { wrapper }, + ) + + const mockPoint = { + datetime: new Date(), + algoValue: new Decimal('100'), + preferredValue: new Decimal('200'), + round: 12345, + } + + act(() => { + result.current.handleChartSelectionChange(mockPoint) + }) + + expect(setSelectedPoint).toHaveBeenCalledWith(mockPoint) + expect(mockOnScrollEnabledChange).toHaveBeenCalledWith(false) + }) + + it('calls onScrollEnabledChange(true) when chart selection is cleared', async () => { + const setSelectedPoint = vi.fn() + const { useChartInteraction } = + await import('@hooks/useChartInteraction') + vi.mocked(useChartInteraction).mockReturnValue({ + period: 'one-week' as const, + setPeriod: vi.fn(), + selectedPoint: null, + setSelectedPoint, + clearSelection: vi.fn(), + }) + + const { result } = renderHook( + () => useAccountOverviewHeader(mockAccount), + { wrapper }, + ) + + act(() => { + result.current.handleChartSelectionChange(null) + }) + + expect(setSelectedPoint).toHaveBeenCalledWith(null) + expect(mockOnScrollEnabledChange).toHaveBeenCalledWith(true) + }) +}) diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverview.ts b/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverview.ts index a88b944d0..fb0009538 100644 --- a/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverview.ts +++ b/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverview.ts @@ -10,105 +10,40 @@ limitations under the License */ -import { useCallback, useMemo, useState } from 'react' -import Decimal from 'decimal.js' +import { useCallback, useState } from 'react' import { - AccountBalanceHistoryItem, - useAccountBalancesQuery, - usePortfolioTotals, useSelectedAccount, WalletAccount, } from '@perawallet/wallet-core-accounts' -import { useCurrency } from '@perawallet/wallet-core-currencies' -import { useSettings } from '@perawallet/wallet-core-settings' -import { useChartInteraction } from '@hooks/useChartInteraction' -import { HistoryPeriod } from '@perawallet/wallet-core-shared' -import { useAppNavigation } from '@hooks/useAppNavigation' import { useModalState } from '@hooks/useModalState' import { useReceiveFunds } from '@modules/transactions/hooks' -import { useClipboard } from '@hooks/useClipboard' -import { useToast } from '@hooks/useToast' -import { useLanguage } from '@hooks/useLanguage' export type UseAccountOverviewResult = { - portfolioAlgoValue: Decimal - portfolioPreferredValue: Decimal - isPending: boolean - period: HistoryPeriod - setPeriod: (period: HistoryPeriod) => void - selectedPoint: AccountBalanceHistoryItem | null - scrollingEnabled: boolean - preferredCurrency: string - hasBalance: boolean - togglePrivacyMode: () => void - handleChartSelectionChange: ( - selected: AccountBalanceHistoryItem | null, - ) => void isSendFundsVisible: boolean - handleOpenSendFunds: () => void + openSendFunds: () => void handleCloseSendFunds: () => void - handleSwap: () => void - handleMore: () => void - handleBuyAlgo: () => void - handleReceive: () => void - handleCopyAddress: () => void - handleShowQR: () => void isReceiveFundsVisible: boolean + openReceiveFunds: () => void handleCloseReceiveFunds: () => void isAccountOptionsVisible: boolean + openAccountOptions: () => void handleCloseAccountOptions: () => void + scrollingEnabled: boolean + onScrollEnabledChange: (enabled: boolean) => void } export const useAccountOverview = ( - account: WalletAccount, + _account: WalletAccount, ): UseAccountOverviewResult => { - const { preferredCurrency, usdToPreferred } = useCurrency() - const { portfolioAlgoValue, accountBalances, isPending } = - useAccountBalancesQuery(account ? [account] : []) - const { portfolioUsdValue } = usePortfolioTotals(accountBalances) - const portfolioPreferredValue = useMemo( - () => usdToPreferred(portfolioUsdValue), - [usdToPreferred, portfolioUsdValue], - ) - const { period, setPeriod, selectedPoint, setSelectedPoint } = - useChartInteraction() - const [scrollingEnabled, setScrollingEnabled] = useState(true) - const { privacyMode, setPrivacyMode } = useSettings() const selectedAccount = useSelectedAccount() const { setSelectedAccount, setCanSelectAccount } = useReceiveFunds() - const togglePrivacyMode = useCallback(() => { - setPrivacyMode(!privacyMode) - }, [privacyMode, setPrivacyMode]) - - const handleChartSelectionChange = useCallback( - (selected: AccountBalanceHistoryItem | null) => { - setSelectedPoint(selected) - - if (selected) { - setScrollingEnabled(false) - } else { - setScrollingEnabled(true) - } - }, - [setSelectedPoint], - ) - - const navigation = useAppNavigation() const { isOpen: isSendFundsVisible, - open: handleOpenSendFunds, + open: openSendFunds, close: handleCloseSendFunds, } = useModalState() - const handleSwap = useCallback(() => { - navigation.replace('TabBar', { screen: 'Swap' }) - }, [navigation]) - - const handleBuyAlgo = useCallback(() => { - navigation.navigate('TabBar', { screen: 'Fund' }) - }, [navigation]) - const { isOpen: isReceiveFundsVisible, open: handleOpenReceiveFunds, @@ -117,36 +52,11 @@ export const useAccountOverview = ( const { isOpen: isAccountOptionsVisible, - open: handleOpenAccountOptions, + open: openAccountOptions, close: handleCloseAccountOptions, } = useModalState() - const handleReceive = useCallback(() => { - if (selectedAccount) { - setCanSelectAccount(false) - setSelectedAccount(selectedAccount) - } - handleOpenReceiveFunds() - }, [selectedAccount, handleOpenReceiveFunds]) - - const handleMore = useCallback(() => { - handleOpenAccountOptions() - }, [handleOpenAccountOptions]) - - const { copyToClipboard } = useClipboard() - const { showToast } = useToast() - const { t } = useLanguage() - - const handleCopyAddress = useCallback(() => { - copyToClipboard(account.address) - showToast({ - title: t('account_options.copy_address'), - body: '', - type: 'success', - }) - }, [copyToClipboard, account.address, showToast, t]) - - const handleShowQR = useCallback(() => { + const openReceiveFunds = useCallback(() => { if (selectedAccount) { setCanSelectAccount(false) setSelectedAccount(selectedAccount) @@ -159,30 +69,26 @@ export const useAccountOverview = ( setSelectedAccount, ]) + const [scrollingEnabled, setScrollingEnabled] = useState(true) + + const onScrollEnabledChange = useCallback( + (enabled: boolean) => { + setScrollingEnabled(enabled) + }, + [setScrollingEnabled], + ) + return { - portfolioAlgoValue, - portfolioPreferredValue, - isPending, - period, - setPeriod, - selectedPoint, - scrollingEnabled, - preferredCurrency, - hasBalance: portfolioAlgoValue.gt(0), - togglePrivacyMode, - handleChartSelectionChange, isSendFundsVisible, - handleOpenSendFunds, + openSendFunds, handleCloseSendFunds, - handleSwap, - handleMore, - handleBuyAlgo, - handleReceive, - handleCopyAddress, - handleShowQR, isReceiveFundsVisible, + openReceiveFunds, handleCloseReceiveFunds, isAccountOptionsVisible, + openAccountOptions, handleCloseAccountOptions, + scrollingEnabled, + onScrollEnabledChange, } } diff --git a/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverviewHeader.ts b/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverviewHeader.ts new file mode 100644 index 000000000..bcda337cf --- /dev/null +++ b/apps/mobile/src/modules/accounts/components/AccountOverview/useAccountOverviewHeader.ts @@ -0,0 +1,82 @@ +/* + 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 { useCallback, useMemo } from 'react' +import Decimal from 'decimal.js' +import { + AccountBalanceHistoryItem, + useAccountBalancesQuery, + usePortfolioTotals, + WalletAccount, +} from '@perawallet/wallet-core-accounts' +import { useCurrency } from '@perawallet/wallet-core-currencies' +import { useSettings } from '@perawallet/wallet-core-settings' +import { useChartInteraction } from '@hooks/useChartInteraction' +import { HistoryPeriod } from '@perawallet/wallet-core-shared' +import { useAccountOverviewModal } from './AccountOverviewModalContext' + +export type UseAccountOverviewHeaderResult = { + portfolioAlgoValue: Decimal + portfolioPreferredValue: Decimal + isPending: boolean + period: HistoryPeriod + setPeriod: (period: HistoryPeriod) => void + selectedPoint: AccountBalanceHistoryItem | null + hasBalance: boolean + togglePrivacyMode: () => void + handleChartSelectionChange: ( + selected: AccountBalanceHistoryItem | null, + ) => void +} + +export const useAccountOverviewHeader = ( + account: WalletAccount, +): UseAccountOverviewHeaderResult => { + const { usdToPreferred } = useCurrency() + const { portfolioAlgoValue, accountBalances, isPending } = + useAccountBalancesQuery(account ? [account] : []) + const { portfolioUsdValue } = usePortfolioTotals(accountBalances) + const portfolioPreferredValue = useMemo( + () => usdToPreferred(portfolioUsdValue), + [usdToPreferred, portfolioUsdValue], + ) + + const { period, setPeriod, selectedPoint, setSelectedPoint } = + useChartInteraction() + + const { onScrollEnabledChange } = useAccountOverviewModal() + + const { privacyMode, setPrivacyMode } = useSettings() + const togglePrivacyMode = useCallback(() => { + setPrivacyMode(!privacyMode) + }, [privacyMode, setPrivacyMode]) + + const handleChartSelectionChange = useCallback( + (selected: AccountBalanceHistoryItem | null) => { + setSelectedPoint(selected) + onScrollEnabledChange(selected === null) + }, + [setSelectedPoint, onScrollEnabledChange], + ) + + return { + portfolioAlgoValue, + portfolioPreferredValue, + isPending, + period, + setPeriod, + selectedPoint, + hasBalance: portfolioAlgoValue.gt(0), + togglePrivacyMode, + handleChartSelectionChange, + } +} diff --git a/apps/mobile/src/modules/accounts/components/ButtonPanel/ButtonPanel.tsx b/apps/mobile/src/modules/accounts/components/ButtonPanel/ButtonPanel.tsx index 4fd34d6f7..4acf8b258 100644 --- a/apps/mobile/src/modules/accounts/components/ButtonPanel/ButtonPanel.tsx +++ b/apps/mobile/src/modules/accounts/components/ButtonPanel/ButtonPanel.tsx @@ -14,22 +14,13 @@ import { useStyles } from './styles' import { PWView } from '@components/core' import { RoundButton } from '@components/RoundButton' import { useLanguage } from '@hooks/useLanguage' +import { useButtonPanel } from './useButtonPanel' -export type ButtonPanelProps = { - onSwap: () => void - onSend: () => void - onReceive: () => void - onMore: () => void -} - -export const ButtonPanel = ({ - onSwap, - onSend, - onReceive, - onMore, -}: ButtonPanelProps) => { +export const ButtonPanel = () => { const themeStyle = useStyles() const { t } = useLanguage() + const { handleSwap, handleSend, handleReceive, handleMore } = + useButtonPanel() return ( diff --git a/apps/mobile/src/modules/accounts/components/ButtonPanel/__tests__/ButtonPanel.spec.tsx b/apps/mobile/src/modules/accounts/components/ButtonPanel/__tests__/ButtonPanel.spec.tsx index 98bc71c1a..8147ed309 100644 --- a/apps/mobile/src/modules/accounts/components/ButtonPanel/__tests__/ButtonPanel.spec.tsx +++ b/apps/mobile/src/modules/accounts/components/ButtonPanel/__tests__/ButtonPanel.spec.tsx @@ -14,17 +14,22 @@ import { render, screen, fireEvent } from '@test-utils/render' import { describe, it, expect, vi } from 'vitest' import { ButtonPanel } from '../ButtonPanel' -vi.mock('@react-navigation/native', async importOriginal => { - const actual = - await importOriginal() - return { - ...actual, - useNavigation: () => ({ - replace: vi.fn(), - push: vi.fn(), - }), - } -}) +const { mockHandleSwap, mockHandleSend, mockHandleReceive, mockHandleMore } = + vi.hoisted(() => ({ + mockHandleSwap: vi.fn(), + mockHandleSend: vi.fn(), + mockHandleReceive: vi.fn(), + mockHandleMore: vi.fn(), + })) + +vi.mock('../useButtonPanel', () => ({ + useButtonPanel: () => ({ + handleSwap: mockHandleSwap, + handleSend: mockHandleSend, + handleReceive: mockHandleReceive, + handleMore: mockHandleMore, + }), +})) vi.mock('@components/core', () => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -58,14 +63,7 @@ vi.mock('@hooks/useLanguage', () => ({ describe('ButtonPanel', () => { it('renders all buttons correctly', () => { - render( - , - ) + render() expect( screen.getByText('account_details.button_panel.swap'), ).toBeTruthy() @@ -81,74 +79,35 @@ describe('ButtonPanel', () => { }) it('does not render stake button', () => { - render( - , - ) + render() expect(() => screen.getByText('account_details.button_panel.stake'), ).toThrow() }) - it('calls onSwap when swap button is pressed', () => { - const onSwap = vi.fn() - render( - , - ) + it('calls handleSwap when swap button is pressed', () => { + render() fireEvent.click(screen.getByText('account_details.button_panel.swap')) - expect(onSwap).toHaveBeenCalledOnce() + expect(mockHandleSwap).toHaveBeenCalledOnce() }) - it('calls onSend when send button is pressed', () => { - const onSend = vi.fn() - render( - , - ) + it('calls handleSend when send button is pressed', () => { + render() fireEvent.click(screen.getByText('account_details.button_panel.send')) - expect(onSend).toHaveBeenCalledOnce() + expect(mockHandleSend).toHaveBeenCalledOnce() }) - it('calls onReceive when receive button is pressed', () => { - const onReceive = vi.fn() - render( - , - ) + it('calls handleReceive when receive button is pressed', () => { + render() fireEvent.click( screen.getByText('account_details.button_panel.receive'), ) - expect(onReceive).toHaveBeenCalledOnce() + expect(mockHandleReceive).toHaveBeenCalledOnce() }) - it('calls onMore when more button is pressed', () => { - const onMore = vi.fn() - render( - , - ) + it('calls handleMore when more button is pressed', () => { + render() fireEvent.click(screen.getByText('account_details.button_panel.more')) - expect(onMore).toHaveBeenCalledOnce() + expect(mockHandleMore).toHaveBeenCalledOnce() }) }) diff --git a/apps/mobile/src/modules/accounts/components/ButtonPanel/index.ts b/apps/mobile/src/modules/accounts/components/ButtonPanel/index.ts index c12f091be..a411b59ff 100644 --- a/apps/mobile/src/modules/accounts/components/ButtonPanel/index.ts +++ b/apps/mobile/src/modules/accounts/components/ButtonPanel/index.ts @@ -11,4 +11,3 @@ */ export { ButtonPanel } from './ButtonPanel' -export type { ButtonPanelProps } from './ButtonPanel' diff --git a/apps/mobile/src/modules/accounts/components/ButtonPanel/useButtonPanel.ts b/apps/mobile/src/modules/accounts/components/ButtonPanel/useButtonPanel.ts new file mode 100644 index 000000000..aa10c179e --- /dev/null +++ b/apps/mobile/src/modules/accounts/components/ButtonPanel/useButtonPanel.ts @@ -0,0 +1,39 @@ +/* + 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 { useCallback } from 'react' +import { useAppNavigation } from '@hooks/useAppNavigation' +import { useAccountOverviewModal } from '../AccountOverview/AccountOverviewModalContext' + +export type UseButtonPanelResult = { + handleSwap: () => void + handleSend: () => void + handleReceive: () => void + handleMore: () => void +} + +export const useButtonPanel = (): UseButtonPanelResult => { + const navigation = useAppNavigation() + const { openSendFunds, openReceiveFunds, openAccountOptions } = + useAccountOverviewModal() + + const handleSwap = useCallback(() => { + navigation.replace('TabBar', { screen: 'Swap' }) + }, [navigation]) + + return { + handleSwap, + handleSend: openSendFunds, + handleReceive: openReceiveFunds, + handleMore: openAccountOptions, + } +} diff --git a/apps/mobile/src/modules/accounts/components/NoFundsButtonPanel/NoFundsButtonPanel.tsx b/apps/mobile/src/modules/accounts/components/NoFundsButtonPanel/NoFundsButtonPanel.tsx index 157e20ed5..52625a32a 100644 --- a/apps/mobile/src/modules/accounts/components/NoFundsButtonPanel/NoFundsButtonPanel.tsx +++ b/apps/mobile/src/modules/accounts/components/NoFundsButtonPanel/NoFundsButtonPanel.tsx @@ -14,20 +14,12 @@ import { useStyles } from './styles' import { PWView } from '@components/core' import { RoundButton } from '@components/RoundButton' import { useLanguage } from '@hooks/useLanguage' +import { useNoFundsButtonPanel } from './useNoFundsButtonPanel' -export type NoFundsButtonPanelProps = { - onBuyAlgo: () => void - onReceive: () => void - onMore: () => void -} - -export const NoFundsButtonPanel = ({ - onBuyAlgo, - onReceive, - onMore, -}: NoFundsButtonPanelProps) => { +export const NoFundsButtonPanel = () => { const themeStyle = useStyles() const { t } = useLanguage() + const { handleBuyAlgo, handleReceive, handleMore } = useNoFundsButtonPanel() return ( @@ -35,19 +27,19 @@ export const NoFundsButtonPanel = ({ title={t('account_details.no_balance.buy_algo')} icon='algo' variant='primary' - onPress={onBuyAlgo} + onPress={handleBuyAlgo} /> ) diff --git a/apps/mobile/src/modules/accounts/components/NoFundsButtonPanel/useNoFundsButtonPanel.ts b/apps/mobile/src/modules/accounts/components/NoFundsButtonPanel/useNoFundsButtonPanel.ts new file mode 100644 index 000000000..94bead16e --- /dev/null +++ b/apps/mobile/src/modules/accounts/components/NoFundsButtonPanel/useNoFundsButtonPanel.ts @@ -0,0 +1,36 @@ +/* + 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 { useCallback } from 'react' +import { useAppNavigation } from '@hooks/useAppNavigation' +import { useAccountOverviewModal } from '../AccountOverview/AccountOverviewModalContext' + +export type UseNoFundsButtonPanelResult = { + handleBuyAlgo: () => void + handleReceive: () => void + handleMore: () => void +} + +export const useNoFundsButtonPanel = (): UseNoFundsButtonPanelResult => { + const navigation = useAppNavigation() + const { openReceiveFunds, openAccountOptions } = useAccountOverviewModal() + + const handleBuyAlgo = useCallback(() => { + navigation.navigate('TabBar', { screen: 'Fund' }) + }, [navigation]) + + return { + handleBuyAlgo, + handleReceive: openReceiveFunds, + handleMore: openAccountOptions, + } +} diff --git a/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/WatchAccountButtonPanel.tsx b/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/WatchAccountButtonPanel.tsx index 3799a77c9..ba786e557 100644 --- a/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/WatchAccountButtonPanel.tsx +++ b/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/WatchAccountButtonPanel.tsx @@ -14,20 +14,13 @@ import { useStyles } from './styles' import { PWView } from '@components/core' import { RoundButton } from '@components/RoundButton' import { useLanguage } from '@hooks/useLanguage' +import { useWatchAccountButtonPanel } from './useWatchAccountButtonPanel' -export type WatchAccountButtonPanelProps = { - onCopyAddress: () => void - onShowQR: () => void - onMore: () => void -} - -export const WatchAccountButtonPanel = ({ - onCopyAddress, - onShowQR, - onMore, -}: WatchAccountButtonPanelProps) => { +export const WatchAccountButtonPanel = () => { const themeStyle = useStyles() const { t } = useLanguage() + const { handleCopyAddress, handleShowQR, handleMore } = + useWatchAccountButtonPanel() return ( @@ -35,21 +28,21 @@ export const WatchAccountButtonPanel = ({ title={t('account_details.watch_button_panel.copy_address')} icon='copy' variant='primary' - onPress={onCopyAddress} + onPress={handleCopyAddress} testID='copy_address_button' /> diff --git a/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/__tests__/WatchAccountButtonPanel.spec.tsx b/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/__tests__/WatchAccountButtonPanel.spec.tsx index 163ef55da..ab433aebf 100644 --- a/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/__tests__/WatchAccountButtonPanel.spec.tsx +++ b/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/__tests__/WatchAccountButtonPanel.spec.tsx @@ -14,6 +14,22 @@ import { render, screen, fireEvent } from '@test-utils/render' import { describe, it, expect, vi } from 'vitest' import { WatchAccountButtonPanel } from '../WatchAccountButtonPanel' +const { mockHandleCopyAddress, mockHandleShowQR, mockHandleMore } = vi.hoisted( + () => ({ + mockHandleCopyAddress: vi.fn(), + mockHandleShowQR: vi.fn(), + mockHandleMore: vi.fn(), + }), +) + +vi.mock('../useWatchAccountButtonPanel', () => ({ + useWatchAccountButtonPanel: () => ({ + handleCopyAddress: mockHandleCopyAddress, + handleShowQR: mockHandleShowQR, + handleMore: mockHandleMore, + }), +})) + vi.mock('@components/core', () => ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any PWView: ({ children, style }: any) =>
{children}
, @@ -46,13 +62,7 @@ vi.mock('@hooks/useLanguage', () => ({ describe('WatchAccountButtonPanel', () => { it('renders all buttons correctly', () => { - render( - , - ) + render() expect( screen.getByText('account_details.watch_button_panel.copy_address'), ).toBeTruthy() @@ -65,13 +75,7 @@ describe('WatchAccountButtonPanel', () => { }) it('does not render send or swap buttons', () => { - render( - , - ) + render() expect(() => screen.getByText('account_details.button_panel.swap'), ).toThrow() @@ -80,48 +84,27 @@ describe('WatchAccountButtonPanel', () => { ).toThrow() }) - it('calls onCopyAddress when copy address button is pressed', () => { - const onCopyAddress = vi.fn() - render( - , - ) + it('calls handleCopyAddress when copy address button is pressed', () => { + render() fireEvent.click( screen.getByText('account_details.watch_button_panel.copy_address'), ) - expect(onCopyAddress).toHaveBeenCalledOnce() + expect(mockHandleCopyAddress).toHaveBeenCalledOnce() }) - it('calls onShowQR when show QR button is pressed', () => { - const onShowQR = vi.fn() - render( - , - ) + it('calls handleShowQR when show QR button is pressed', () => { + render() fireEvent.click( screen.getByText('account_details.watch_button_panel.show_qr'), ) - expect(onShowQR).toHaveBeenCalledOnce() + expect(mockHandleShowQR).toHaveBeenCalledOnce() }) - it('calls onMore when more button is pressed', () => { - const onMore = vi.fn() - render( - , - ) + it('calls handleMore when more button is pressed', () => { + render() fireEvent.click( screen.getByText('account_details.watch_button_panel.more'), ) - expect(onMore).toHaveBeenCalledOnce() + expect(mockHandleMore).toHaveBeenCalledOnce() }) }) diff --git a/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/index.ts b/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/index.ts index 3f7ec8f94..68f06efbe 100644 --- a/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/index.ts +++ b/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/index.ts @@ -11,4 +11,3 @@ */ export { WatchAccountButtonPanel } from './WatchAccountButtonPanel' -export type { WatchAccountButtonPanelProps } from './WatchAccountButtonPanel' diff --git a/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/useWatchAccountButtonPanel.ts b/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/useWatchAccountButtonPanel.ts new file mode 100644 index 000000000..40586148d --- /dev/null +++ b/apps/mobile/src/modules/accounts/components/WatchAccountButtonPanel/useWatchAccountButtonPanel.ts @@ -0,0 +1,47 @@ +/* + 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 { useCallback } from 'react' +import { useClipboard } from '@hooks/useClipboard' +import { useToast } from '@hooks/useToast' +import { useLanguage } from '@hooks/useLanguage' +import { useAccountOverviewModal } from '../AccountOverview/AccountOverviewModalContext' + +export type UseWatchAccountButtonPanelResult = { + handleCopyAddress: () => void + handleShowQR: () => void + handleMore: () => void +} + +export const useWatchAccountButtonPanel = + (): UseWatchAccountButtonPanelResult => { + const { account, openReceiveFunds, openAccountOptions } = + useAccountOverviewModal() + const { copyToClipboard } = useClipboard() + const { showToast } = useToast() + const { t } = useLanguage() + + const handleCopyAddress = useCallback(() => { + copyToClipboard(account.address) + showToast({ + title: t('account_options.copy_address'), + body: '', + type: 'success', + }) + }, [copyToClipboard, account.address, showToast, t]) + + return { + handleCopyAddress, + handleShowQR: openReceiveFunds, + handleMore: openAccountOptions, + } + }