From 2cbfa9593fa700501c3076a61830770c195ae802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Rodriguez=20V=C3=A9lez?= Date: Fri, 7 Jun 2024 13:21:49 -0400 Subject: [PATCH] REF: WalletAddresses to TSX (#6670) --- ...AddressTypeTabs.js => AddressTypeTabs.tsx} | 54 +++-- navigation/DetailViewScreensStack.tsx | 8 +- screen/wallets/WalletAddresses.tsx | 224 ++++++++++++++++++ screen/wallets/addresses.js | 176 -------------- tests/unit/addresses.test.ts | 15 +- 5 files changed, 270 insertions(+), 207 deletions(-) rename components/addresses/{AddressTypeTabs.js => AddressTypeTabs.tsx} (55%) create mode 100644 screen/wallets/WalletAddresses.tsx delete mode 100644 screen/wallets/addresses.js diff --git a/components/addresses/AddressTypeTabs.js b/components/addresses/AddressTypeTabs.tsx similarity index 55% rename from components/addresses/AddressTypeTabs.js rename to components/addresses/AddressTypeTabs.tsx index f043876be..6ce64e708 100644 --- a/components/addresses/AddressTypeTabs.js +++ b/components/addresses/AddressTypeTabs.tsx @@ -1,44 +1,57 @@ import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; - +import { StyleSheet, Text, View, Pressable, LayoutAnimation, Platform, UIManager, ViewStyle, TextStyle } from 'react-native'; import loc from '../../loc'; import { useTheme } from '../themes'; export const TABS = { EXTERNAL: 'receive', INTERNAL: 'change', -}; +} as const; -const AddressTypeTabs = ({ currentTab, setCurrentTab }) => { +type TabKey = keyof typeof TABS; +type TABS_VALUES = (typeof TABS)[keyof typeof TABS]; + +if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { + UIManager.setLayoutAnimationEnabledExperimental(true); +} + +interface AddressTypeTabsProps { + currentTab: TABS_VALUES; + setCurrentTab: (tab: TABS_VALUES) => void; + customTabText?: { [key in TabKey]?: string }; +} + +const AddressTypeTabs: React.FC = ({ currentTab, setCurrentTab, customTabText }) => { const { colors } = useTheme(); const stylesHook = StyleSheet.create({ activeTab: { backgroundColor: colors.modal, - }, + } as ViewStyle, activeText: { fontWeight: 'bold', color: colors.foregroundColor, - }, + } as TextStyle, inactiveTab: { fontWeight: 'normal', color: colors.foregroundColor, - }, + } as TextStyle, backTabs: { backgroundColor: colors.buttonDisabledBackgroundColor, - }, + } as ViewStyle, }); const tabs = Object.entries(TABS).map(([key, value]) => { return { - key, + key: key as TabKey, value, - name: loc.addresses[`type_${value}`], + name: customTabText?.[key as TabKey] || loc.addresses[`type_${value}`], }; }); - const changeToTab = tabKey => { + const changeToTab = (tabKey: TabKey) => { if (tabKey in TABS) { + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setCurrentTab(TABS[tabKey]); } }; @@ -47,15 +60,13 @@ const AddressTypeTabs = ({ currentTab, setCurrentTab }) => { const tabsButtons = tabs.map(tab => { const isActive = tab.value === currentTab; - const tabStyle = isActive ? stylesHook.activeTab : stylesHook.inactiveTab; + const tabStyle = isActive ? stylesHook.activeTab : undefined; const textStyle = isActive ? stylesHook.activeText : stylesHook.inactiveTab; return ( - changeToTab(tab.key)} style={[styles.tab, tabStyle]}> - changeToTab(tab.key)} style={textStyle}> - {tab.name} - - + changeToTab(tab.key)} style={[styles.tab, tabStyle]}> + {tab.name} + ); }); @@ -76,22 +87,23 @@ const styles = StyleSheet.create({ flex: 1, flexDirection: 'row', justifyContent: 'center', - }, + } as ViewStyle, backTabs: { padding: 4, marginVertical: 8, borderRadius: 8, - }, + } as ViewStyle, tabs: { flex: 1, flexDirection: 'row', justifyContent: 'center', - }, + } as ViewStyle, tab: { borderRadius: 6, paddingVertical: 8, paddingHorizontal: 16, - }, + justifyContent: 'center', + } as ViewStyle, }); export { AddressTypeTabs }; diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 03d818394..a524ec4de 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -24,7 +24,7 @@ import TransactionDetails from '../screen/transactions/TransactionDetails'; import RBFBumpFee from '../screen/transactions/RBFBumpFee'; import RBFCancel from '../screen/transactions/RBFCancel'; import TransactionStatus from '../screen/transactions/TransactionStatus'; -import WalletAddresses from '../screen/wallets/addresses'; +import WalletAddresses from '../screen/wallets/WalletAddresses'; import WalletDetails from '../screen/wallets/details'; import GenerateWord from '../screen/wallets/generateWord'; import LdkViewLogs from '../screen/wallets/ldkViewLogs'; @@ -241,7 +241,11 @@ const DetailViewStackScreensStack = () => { gestureEnabled: false, }} /> - + { + switch (action.type) { + case SET_SHOW_ADDRESSES: + return { ...state, showAddresses: action.payload }; + case SET_ADDRESSES: + return { ...state, addresses: action.payload }; + case SET_CURRENT_TAB: + return { ...state, currentTab: action.payload }; + case SET_SEARCH: + return { ...state, search: action.payload }; + default: + return state; + } +}; + +export const totalBalance = ({ c, u } = { c: 0, u: 0 }) => c + u; + +export const getAddress = (wallet: any, index: number, isInternal: boolean): Address => { + let address: string; + let balance = 0; + let transactions = 0; + + if (isInternal) { + address = wallet._getInternalAddressByIndex(index); + balance = totalBalance(wallet._balances_by_internal_index[index]); + transactions = wallet._txs_by_internal_index[index]?.length; + } else { + address = wallet._getExternalAddressByIndex(index); + balance = totalBalance(wallet._balances_by_external_index[index]); + transactions = wallet._txs_by_external_index[index]?.length; + } + + return { + key: address, + index, + address, + isInternal, + balance, + transactions, + }; +}; + +export const sortByAddressIndex = (a: Address, b: Address) => { + return a.index > b.index ? 1 : -1; +}; + +export const filterByAddressType = ( + type: (typeof TABS)[keyof typeof TABS], + isInternal: boolean, + currentType: (typeof TABS)[keyof typeof TABS], +) => { + return currentType === type ? isInternal === true : isInternal === false; +}; + +type NavigationProps = NativeStackNavigationProp; +type RouteProps = RouteProp; + +const WalletAddresses: React.FC = () => { + const [{ showAddresses, addresses, currentTab, search }, dispatch] = useReducer(reducer, initialState); + + const { wallets } = useStorage(); + const { walletID } = useRoute().params; + + const addressList = useRef>(null); + const wallet = wallets.find((w: any) => w.getID() === walletID); + + const balanceUnit = wallet?.getPreferredBalanceUnit(); + const isWatchOnly = wallet?.type === WatchOnlyWallet.type; + const walletInstance = isWatchOnly ? wallet._hdWalletInstance : wallet; + const allowSignVerifyMessage = wallet && 'allowSignVerifyMessage' in wallet && wallet.allowSignVerifyMessage(); + + const { colors } = useTheme(); + const { setOptions } = useExtendedNavigation(); + const { enableBlur, disableBlur } = usePrivacy(); + + const stylesHook = StyleSheet.create({ + root: { + backgroundColor: colors.elevated, + }, + }); + + const filteredAddresses = useMemo( + () => addresses.filter(address => filterByAddressType(TABS.INTERNAL, address.isInternal, currentTab)).sort(sortByAddressIndex), + [addresses, currentTab], + ); + + useEffect(() => { + if (showAddresses && addressList.current) { + addressList.current.scrollToIndex({ animated: false, index: 0 }); + } + }, [showAddresses]); + + useLayoutEffect(() => { + setOptions({ + headerSearchBarOptions: { + onChangeText: (event: { nativeEvent: { text: string } }) => dispatch({ type: SET_SEARCH, payload: event.nativeEvent.text }), + }, + }); + }, [setOptions]); + + const getAddresses = useCallback(() => { + const newAddresses: Address[] = []; + // @ts-ignore: idk what to do + for (let index = 0; index <= (walletInstance?.next_free_change_address_index ?? 0); index++) { + const address = getAddress(walletInstance, index, true); + newAddresses.push(address); + } + + // @ts-ignore: idk what to do + for (let index = 0; index < (walletInstance?.next_free_address_index ?? 0) + (walletInstance?.gap_limit ?? 0); index++) { + const address = getAddress(walletInstance, index, false); + newAddresses.push(address); + } + dispatch({ type: SET_ADDRESSES, payload: newAddresses }); + dispatch({ type: SET_SHOW_ADDRESSES, payload: true }); + }, [walletInstance]); + + useFocusEffect( + useCallback(() => { + enableBlur(); + getAddresses(); + return () => { + disableBlur(); + }; + }, [enableBlur, disableBlur, getAddresses]), + ); + + const data = + search.length > 0 ? filteredAddresses.filter(item => item.address.toLowerCase().includes(search.toLowerCase())) : filteredAddresses; + + const renderRow = useCallback( + ({ item }: { item: Address }) => { + return balanceUnit && allowSignVerifyMessage ? ( + + ) : null; + }, + [balanceUnit, walletID, allowSignVerifyMessage], + ); + + if (!wallet) { + return ( + + + + ); + } + + return ( + + 0 ? null : } + centerContent={!showAddresses} + contentInsetAdjustmentBehavior="automatic" + ListHeaderComponent={ + dispatch({ type: SET_CURRENT_TAB, payload: tab })} + /> + } + /> + + ); +}; + +export default WalletAddresses; + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, +}); diff --git a/screen/wallets/addresses.js b/screen/wallets/addresses.js deleted file mode 100644 index 8e189fed6..000000000 --- a/screen/wallets/addresses.js +++ /dev/null @@ -1,176 +0,0 @@ -import { useFocusEffect, useNavigation, useRoute } from '@react-navigation/native'; -import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { ActivityIndicator, FlatList, StyleSheet, View } from 'react-native'; -import { WatchOnlyWallet } from '../../class'; -import { AddressItem } from '../../components/addresses/AddressItem'; -import { AddressTypeTabs, TABS } from '../../components/addresses/AddressTypeTabs'; -import navigationStyle from '../../components/navigationStyle'; -import { useTheme } from '../../components/themes'; -import usePrivacy from '../../hooks/usePrivacy'; -import loc from '../../loc'; -import { useStorage } from '../../hooks/context/useStorage'; - -export const totalBalance = ({ c, u } = { c: 0, u: 0 }) => c + u; - -export const getAddress = (wallet, index, isInternal) => { - let address; - let balance = 0; - let transactions = 0; - - if (isInternal) { - address = wallet._getInternalAddressByIndex(index); - balance = totalBalance(wallet._balances_by_internal_index[index]); - transactions = wallet._txs_by_internal_index[index]?.length; - } else { - address = wallet._getExternalAddressByIndex(index); - balance = totalBalance(wallet._balances_by_external_index[index]); - transactions = wallet._txs_by_external_index[index]?.length; - } - - return { - key: address, - index, - address, - isInternal, - balance, - transactions, - }; -}; - -export const sortByAddressIndex = (a, b) => { - if (a.index > b.index) { - return 1; - } - return -1; -}; - -export const filterByAddressType = (type, isInternal, currentType) => { - if (currentType === type) { - return isInternal === true; - } - return isInternal === false; -}; - -const WalletAddresses = () => { - const [showAddresses, setShowAddresses] = useState(false); - - const [addresses, setAddresses] = useState([]); - - const [currentTab, setCurrentTab] = useState(TABS.EXTERNAL); - - const { wallets } = useStorage(); - - const { walletID } = useRoute().params; - - const addressList = useRef(); - - const wallet = wallets.find(w => w.getID() === walletID); - - const balanceUnit = wallet.getPreferredBalanceUnit(); - - const isWatchOnly = wallet.type === WatchOnlyWallet.type; - - const walletInstance = isWatchOnly ? wallet._hdWalletInstance : wallet; - - const allowSignVerifyMessage = 'allowSignVerifyMessage' in wallet && wallet.allowSignVerifyMessage(); - - const { colors } = useTheme(); - - const { setOptions } = useNavigation(); - - const [search, setSearch] = React.useState(''); - - const { enableBlur, disableBlur } = usePrivacy(); - - const stylesHook = StyleSheet.create({ - root: { - backgroundColor: colors.elevated, - }, - }); - - // computed property - const filteredAddresses = addresses - .filter(address => filterByAddressType(TABS.INTERNAL, address.isInternal, currentTab)) - .sort(sortByAddressIndex); - - useEffect(() => { - if (showAddresses) { - addressList.current.scrollToIndex({ animated: false, index: 0 }); - } - }, [showAddresses]); - - useLayoutEffect(() => { - setOptions({ - headerSearchBarOptions: { - onChangeText: event => setSearch(event.nativeEvent.text), - }, - }); - }, [setOptions]); - - const getAddresses = () => { - const newAddresses = []; - - for (let index = 0; index <= walletInstance.next_free_change_address_index; index++) { - const address = getAddress(walletInstance, index, true); - - newAddresses.push(address); - } - - for (let index = 0; index < walletInstance.next_free_address_index + walletInstance.gap_limit; index++) { - const address = getAddress(walletInstance, index, false); - - newAddresses.push(address); - } - - setAddresses(newAddresses); - setShowAddresses(true); - }; - - useFocusEffect( - useCallback(() => { - enableBlur(); - getAddresses(); - return () => { - disableBlur(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []), - ); - - const data = - search.length > 0 ? filteredAddresses.filter(item => item.address.toLowerCase().includes(search.toLowerCase())) : filteredAddresses; - - const renderRow = item => { - return ; - }; - - return ( - - 0 ? null : } - centerContent={!showAddresses} - contentInsetAdjustmentBehavior="automatic" - ListHeaderComponent={} - /> - - ); -}; - -WalletAddresses.navigationOptions = navigationStyle({ - title: loc.addresses.addresses_title, - statusBarStyle: 'auto', -}); - -export default WalletAddresses; - -const styles = StyleSheet.create({ - root: { - flex: 1, - }, -}); diff --git a/tests/unit/addresses.test.ts b/tests/unit/addresses.test.ts index 9a96abd57..be8dde22d 100644 --- a/tests/unit/addresses.test.ts +++ b/tests/unit/addresses.test.ts @@ -1,7 +1,6 @@ import assert from 'assert'; - +import { filterByAddressType, getAddress, sortByAddressIndex, totalBalance } from '../../screen/wallets/WalletAddresses'; import { TABS } from '../../components/addresses/AddressTypeTabs'; -import { filterByAddressType, getAddress, sortByAddressIndex, totalBalance } from '../../screen/wallets/addresses'; jest.mock('../../blue_modules/currency', () => { return { @@ -16,11 +15,11 @@ jest.mock('../../blue_modules/BlueElectrum', () => { }); const mockAddressesList = [ - { index: 2, isInternal: false, key: 'third_external_address' }, - { index: 0, isInternal: true, key: 'first_internal_address' }, - { index: 1, isInternal: false, key: 'second_external_address' }, - { index: 1, isInternal: true, key: 'second_internal_address' }, - { index: 0, isInternal: false, key: 'first_external_address' }, + { index: 2, isInternal: false, key: 'third_external_address', address: '', balance: 0, transactions: 0 }, + { index: 0, isInternal: true, key: 'first_internal_address', address: '', balance: 0, transactions: 0 }, + { index: 1, isInternal: false, key: 'second_external_address', address: '', balance: 0, transactions: 0 }, + { index: 1, isInternal: true, key: 'second_internal_address', address: '', balance: 0, transactions: 0 }, + { index: 0, isInternal: false, key: 'first_external_address', address: '', balance: 0, transactions: 0 }, ]; describe('Addresses', () => { @@ -42,7 +41,7 @@ describe('Addresses', () => { }); it('Filter by type', () => { - let currentTab = TABS.EXTERNAL; + let currentTab: (typeof TABS)[keyof typeof TABS] = TABS.EXTERNAL; const externalAddresses = mockAddressesList.filter(address => filterByAddressType(TABS.INTERNAL, address.isInternal, currentTab));