diff --git a/package.json b/package.json index 8819aa39..a40e7a63 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,10 @@ "@dfinity/principal": "^0.9.3", "@hookform/error-message": "^2.0.0", "@psychedelic/dab-js": "1.4.12", - "@psychedelic/plug-controller": "0.24.9", + "@psychedelic/plug-controller": "0.25.0", "@react-native-async-storage/async-storage": "^1.17.10", + "@react-native-clipboard/clipboard": "^1.11.1", "@react-native-community/blur": "^4.2.0", - "@react-native-community/clipboard": "^1.5.1", "@react-native-masked-view/masked-view": "^0.2.7", "@react-navigation/bottom-tabs": "^6.4.0", "@react-navigation/elements": "^1.3.6", diff --git a/src/components/common/AccountInfo/index.tsx b/src/components/common/AccountInfo/index.tsx index 598ae126..ab851c2c 100644 --- a/src/components/common/AccountInfo/index.tsx +++ b/src/components/common/AccountInfo/index.tsx @@ -1,4 +1,4 @@ -import Clipboard from '@react-native-community/clipboard'; +import Clipboard from '@react-native-clipboard/clipboard'; import React, { useEffect, useState } from 'react'; import { View } from 'react-native'; diff --git a/src/components/common/Copy/index.tsx b/src/components/common/Copy/index.tsx index dbe4db39..6db428cf 100644 --- a/src/components/common/Copy/index.tsx +++ b/src/components/common/Copy/index.tsx @@ -1,4 +1,4 @@ -import Clipboard from '@react-native-community/clipboard'; +import Clipboard from '@react-native-clipboard/clipboard'; import { t } from 'i18next'; import React, { useEffect, useState } from 'react'; import { StyleProp, View, ViewStyle } from 'react-native'; diff --git a/src/components/common/Toast/index.tsx b/src/components/common/Toast/index.tsx index 4348e8ef..1fc50083 100644 --- a/src/components/common/Toast/index.tsx +++ b/src/components/common/Toast/index.tsx @@ -23,7 +23,7 @@ export enum ToastTypes { export interface ToastProps { title: string; - message: string; + message?: string; type: 'success' | 'error' | 'info'; id: string; } @@ -65,9 +65,11 @@ function Toast({ title, message, type, id }: ToastProps) { - - {message} - + {message && ( + + {message} + + )} ); } diff --git a/src/components/common/Toast/styles.ts b/src/components/common/Toast/styles.ts index f6082c75..6371aaf6 100644 --- a/src/components/common/Toast/styles.ts +++ b/src/components/common/Toast/styles.ts @@ -11,11 +11,11 @@ export default StyleSheet.create({ }, headerContainer: { flexDirection: 'row', - marginBottom: 8, alignItems: 'center', justifyContent: 'space-between', }, message: { + marginTop: 8, color: Colors.White.Pure, opacity: 0.8, }, diff --git a/src/components/common/Touchable/index.tsx b/src/components/common/Touchable/index.tsx index 6355aa5a..d452c06a 100644 --- a/src/components/common/Touchable/index.tsx +++ b/src/components/common/Touchable/index.tsx @@ -16,7 +16,7 @@ import scales from '@/utils/animationScales'; interface Props { children?: React.ReactNode; - onPress?: () => void; + onPress?: (param?: any) => void; onLongPress?: () => void; hapticType?: HapticFeedbackTypes; scale?: number; diff --git a/src/components/formatters/UsdFormat/index.tsx b/src/components/formatters/UsdFormat/index.tsx index 7245f61d..0d5b2400 100644 --- a/src/components/formatters/UsdFormat/index.tsx +++ b/src/components/formatters/UsdFormat/index.tsx @@ -9,9 +9,16 @@ interface Props { style?: StyleProp; decimalScale?: number; suffix?: string; + numberOfLines?: number; } -function UsdFormat({ value, style, decimalScale = 2, suffix }: Props) { +function UsdFormat({ + value, + style, + decimalScale = 2, + suffix, + numberOfLines, +}: Props) { return ( textValue ? ( - + {textValue} ) : null diff --git a/src/hooks/useCustomToast.ts b/src/hooks/useCustomToast.ts index 74d1c996..009fef33 100644 --- a/src/hooks/useCustomToast.ts +++ b/src/hooks/useCustomToast.ts @@ -6,7 +6,7 @@ import { ToastTypes } from '@/components/common/Toast'; function useCustomToast() { const toast = useToast(); - const showSuccess = useCallback((title: string, message: string) => { + const showSuccess = useCallback((title: string, message?: string) => { toast.show(`${ToastTypes.success}-${title}`, { data: { type: ToastTypes.success, @@ -16,7 +16,7 @@ function useCustomToast() { }); }, []); - const showError = useCallback((title: string, message: string) => { + const showError = useCallback((title: string, message?: string) => { toast.show(`${ToastTypes.error}-${title}`, { data: { type: ToastTypes.error, @@ -26,7 +26,7 @@ function useCustomToast() { }); }, []); - const showInfo = useCallback((title: string, message: string) => { + const showInfo = useCallback((title: string, message?: string) => { toast.show(`${ToastTypes.info}-${title}`, { data: { type: ToastTypes.info, diff --git a/src/interfaces/redux.ts b/src/interfaces/redux.ts index 8cffb272..b4025e4a 100644 --- a/src/interfaces/redux.ts +++ b/src/interfaces/redux.ts @@ -1,5 +1,6 @@ import WalletConnect from '@walletconnect/client'; +import { Nullable } from './general'; import { ICNSData } from './icns'; import { WCWhiteListItem } from './walletConnect'; @@ -53,40 +54,20 @@ export interface CanisterInfo { symbol?: string; } -interface Currency { - symbol: string; - decimals: number; -} - -export interface TransactionDetails { - status: string; //check if this is correct - fee: { - amount: string; - currency: Currency; - }; - from: string; - amount: string; - currency: Currency; - to: string; - caller: string; -} - export interface Transaction { - amount: string | number; - type: string; //TODO: Add types here SEND/RECEIVE. Check ACTIVITY_TYPES - symbol: string; - hash: string; + type: string; to: string; from: string; - date: Date; - image: string; - value?: string | number; - status?: number | string; - icon?: string; - canisterId?: string; - plug?: any; - canisterInfo?: CanisterInfo; - details?: TransactionDetails; + hash: string; + amount: Nullable; + value?: Nullable; + status: number; + date: bigint; + symbol: string; + logo: string; + canisterId: string; + details?: { [key: string]: any }; + canisterInfo?: Object; } export interface Asset { diff --git a/src/redux/slices/user.ts b/src/redux/slices/user.ts index 2925f9f3..3996e439 100644 --- a/src/redux/slices/user.ts +++ b/src/redux/slices/user.ts @@ -2,7 +2,6 @@ import { Principal } from '@dfinity/principal'; import { Address } from '@psychedelic/plug-controller/dist/interfaces/contact_registry'; import { createAsyncThunk, createSlice, isAnyOf } from '@reduxjs/toolkit'; -import { JELLY_CANISTER_ID } from '@/constants/canister'; import { ENABLE_NFTS } from '@/constants/nfts'; import { CollectionInfo, @@ -35,7 +34,6 @@ import { DEFAULT_TRANSACTION, formatContact, formatContactForController, - formatTransaction, TRANSACTION_STATUS, } from '../utils'; @@ -184,25 +182,11 @@ export const getTransactions = createAsyncThunk< try { const { icpPrice } = params; const instance = KeyRing.getInstance(); - const currentWalletId = instance?.currentWalletId; - const state = await instance?.getState(); - const currentWallet = state.wallets[currentWalletId]; - const response = await instance?.getTransactions({}); - let parsedTrx = - response?.transactions?.map(formatTransaction(icpPrice, currentWallet)) || - []; - - if (!ENABLE_NFTS) { - parsedTrx = parsedTrx.filter( - item => - !( - item?.symbol === 'NFT' || - item?.details.canisterId === JELLY_CANISTER_ID - ) - ); - } + const { transactions } = await instance?.getTransactions({ + icpPrice, + }); - return parsedTrx; + return transactions; } catch (e: any) { return rejectWithValue(e.message); } diff --git a/src/redux/utils.js b/src/redux/utils.js index f6758c68..2c2c184a 100644 --- a/src/redux/utils.js +++ b/src/redux/utils.js @@ -4,14 +4,11 @@ import Flatted from 'flatted'; import { t } from 'i18next'; import { TOKEN_IMAGES, TOKENS } from '@/constants/assets'; -import { ACTIVITY_STATUS } from '@/constants/business'; import { KEYRING_KEYS_IN_STORAGE, KEYRING_STORAGE_KEY, } from '@/constants/keyring'; -import { formatAssetBySymbol, parseToFloatAmount } from '@/utils/currencies'; import { validateAccountId, validatePrincipalId } from '@/utils/ids'; -import { recursiveParseBigint } from '@/utils/objects'; import { clear } from './slices/keyring'; import { @@ -82,113 +79,6 @@ export const getNewAccountData = async (dispatch, icpPrice) => { dispatch(getContacts()); }; -const parseTransactionObject = transactionObject => { - const { amount, currency, token, sonicData, canisterInfo } = - transactionObject; - - const { decimals } = { - ...currency, - ...token, - ...(sonicData?.token ?? {}), - ...(canisterInfo?.tokenRegistryInfo?.details ?? {}), - }; - // TODO: Decimals are currently not in DAB. Remove once they are added. - const parsedAmount = parseToFloatAmount( - amount, - decimals || TOKENS[sonicData?.token?.details?.symbol]?.decimals - ); - - return { - ...transactionObject, - amount: parsedAmount, - }; -}; - -const parseTransaction = transaction => { - const { details } = transaction; - const { fee } = details; - - const parsedDetails = parseTransactionObject(details); - let parsedFee = fee; - - if (fee instanceof Object && ('token' in fee || 'currency' in fee)) { - parsedFee = parseTransactionObject(fee); - } - - return { - ...transaction, - details: { - ...parsedDetails, - fee: parsedFee, - }, - }; -}; - -const getTransactionSymbol = details => { - if (!details) { - return ''; - } - if ('tokenRegistryInfo' in (details?.canisterInfo || [])) { - return details?.canisterInfo.tokenRegistryInfo.symbol; - } - if ('nftRegistryInfo' in (details?.canisterInfo || [])) { - return 'NFT'; - } - return ( - details?.currency?.symbol ?? - details?.sonicData?.token?.details?.symbol ?? - details?.details?.name ?? - '' - ); -}; - -const getTransactionType = (type, isOwnTx) => { - if (!type) { - return ''; - } - if (type.includes('transfer')) { - return isOwnTx ? 'SEND' : 'RECEIVE'; - } - if (type.includes('Liquidity')) { - return `${type.includes('removeLiquidity') ? 'Remove' : 'Add'} Liquidity`; - } - return type.toUpperCase(); -}; - -export const formatTransaction = (icpPrice, currentWallet) => trx => { - const { principal, accountId } = currentWallet; - - let parsedTransaction = recursiveParseBigint(parseTransaction(trx)); - const { details, hash, caller, timestamp } = parsedTransaction || {}; - const isOwnTx = [principal, accountId].includes(caller); - - const symbol = getTransactionSymbol(details); - const asset = formatAssetBySymbol(details?.amount, symbol, icpPrice); - const type = getTransactionType(parsedTransaction?.type, isOwnTx); - - const transaction = { - amount: asset.amount, - value: asset.value, - icon: asset.icon, - type, - hash, - to: details?.to?.icns || details?.to?.principal, - from: details?.from?.icns || details?.from?.principal || caller, - date: new Date(timestamp), - status: ACTIVITY_STATUS[details?.status], - image: details?.canisterInfo?.icon || TOKEN_IMAGES[symbol] || '', - symbol, - canisterId: details?.canisterId, - plug: null, - canisterInfo: details?.canisterInfo, - details: { - ...details, - caller, - }, - }; - return transaction; -}; - export const formatContact = contact => { const [id] = Object.values(contact.value); diff --git a/src/screens/flows/Deposit/components/IDDetails/index.js b/src/screens/flows/Deposit/components/IDDetails/index.js index ff59355d..79282674 100644 --- a/src/screens/flows/Deposit/components/IDDetails/index.js +++ b/src/screens/flows/Deposit/components/IDDetails/index.js @@ -1,4 +1,4 @@ -import Clipboard from '@react-native-community/clipboard'; +import Clipboard from '@react-native-clipboard/clipboard'; import React, { useEffect, useState } from 'react'; import CopiedToast from '@/commonComponents/CopiedToast'; diff --git a/src/screens/flows/Send/index.tsx b/src/screens/flows/Send/index.tsx index b7eaf18f..b75bf455 100644 --- a/src/screens/flows/Send/index.tsx +++ b/src/screens/flows/Send/index.tsx @@ -17,6 +17,7 @@ import { getICPPrice } from '@/redux/slices/icp'; import { sendToken, setTransaction, transferNFT } from '@/redux/slices/user'; import { formatCollections, FormattedCollection } from '@/utils/assets'; import { + isOwnAddress, validateAccountId, validateICNSName, validatePrincipalId, @@ -186,12 +187,9 @@ function Send({ route }: ScreenProps) { } const savedContact = contacts?.find(c => c.id === text); - const isOwnAddress = - text === currentWallet?.principal || - text === currentWallet?.accountId || - text === currentWallet?.icnsData?.reverseResolvedName; + const isOwn = isOwnAddress(text, currentWallet!); - if (savedContact && !isOwnAddress) { + if (savedContact && !isOwn) { setReceiver({ ...savedContact, isValid: true, @@ -199,7 +197,7 @@ function Send({ route }: ScreenProps) { scrollToTop(); } else { const isValid = - !isOwnAddress && (validatePrincipalId(text) || validateAccountId(text)); + !isOwn && (validatePrincipalId(text) || validateAccountId(text)); setReceiver({ id: text, isValid }); } }; diff --git a/src/screens/tabs/Profile/index.js b/src/screens/tabs/Profile/index.tsx similarity index 72% rename from src/screens/tabs/Profile/index.js rename to src/screens/tabs/Profile/index.tsx index beebb26a..053fafcb 100644 --- a/src/screens/tabs/Profile/index.js +++ b/src/screens/tabs/Profile/index.tsx @@ -1,20 +1,25 @@ -import { useNavigation, useScrollToTop } from '@react-navigation/native'; +import { useScrollToTop } from '@react-navigation/native'; import { FlashList } from '@shopify/flash-list'; -import React, { useRef } from 'react'; -import { useTranslation } from 'react-i18next'; +import { t } from 'i18next'; +import React, { useEffect, useRef, useState } from 'react'; import { RefreshControl, View } from 'react-native'; +import { Modalize } from 'react-native-modalize'; import { shallowEqual } from 'react-redux'; -import EmptyState from '@/commonComponents/EmptyState'; -import ErrorState from '@/commonComponents/ErrorState'; -import Header from '@/commonComponents/Header'; -import UserIcon from '@/commonComponents/UserIcon'; import Button from '@/components/buttons/Button'; -import { Touchable } from '@/components/common'; -import Text from '@/components/common/Text'; +import { + EmptyState, + ErrorState, + Header, + Text, + Touchable, + UserIcon, +} from '@/components/common'; import Icon from '@/components/icons'; import { ERROR_TYPES } from '@/constants/general'; import { Colors } from '@/constants/theme'; +import { ScreenProps } from '@/interfaces/navigation'; +import { Transaction } from '@/interfaces/redux'; import { Container, Separator } from '@/layout'; import Routes from '@/navigation/Routes'; import { useAppDispatch, useAppSelector } from '@/redux/hooks'; @@ -25,29 +30,43 @@ import ActivityItem, { import animationScales from '@/utils/animationScales'; import Accounts from './modals/Accounts'; +import ActivityDetail from './modals/ActivityDetail'; import styles from './styles'; -const Profile = () => { - const { t } = useTranslation(); - const navigation = useNavigation(); - const dispatch = useAppDispatch(); - const modalRef = useRef(null); +function Profile({ navigation }: ScreenProps) { + const accountsModalRef = useRef(null); + const activityDetailModalRef = useRef(null); const transactionListRef = useRef(null); + const [selectedTransaction, setSelectedTransaction] = useState(); + useScrollToTop(transactionListRef); + const dispatch = useAppDispatch(); const reverseResolvedName = useAppSelector( state => state.keyring.currentWallet?.icnsData?.reverseResolvedName ); - const { currentWallet } = useAppSelector(state => state.keyring); const { icpPrice } = useAppSelector(state => state.icp); const { transactions, transactionsLoading, transactionsError } = useAppSelector(state => state.user, shallowEqual); + useEffect(() => { + if (selectedTransaction) { + activityDetailModalRef.current?.open(); + } + }, [selectedTransaction]); + const onRefresh = () => { dispatch(getTransactions({ icpPrice })); }; - const renderTransaction = ({ item }) => ; + const renderTransaction = ({ item }: { item: Transaction }) => ( + { + setSelectedTransaction(item); + }} + /> + ); return ( <> @@ -66,7 +85,7 @@ const Profile = () => { { text={t('common.change')} buttonStyle={styles.buttonStyle} textStyle={styles.buttonTextStyle} - onPress={modalRef.current?.open} + onPress={() => accountsModalRef.current?.open()} /> @@ -120,9 +139,14 @@ const Profile = () => { /> )} - + + setSelectedTransaction(undefined)} + /> ); -}; +} export default Profile; diff --git a/src/screens/tabs/Profile/modals/Accounts/index.tsx b/src/screens/tabs/Profile/modals/Accounts/index.tsx index 4135defc..282f5859 100644 --- a/src/screens/tabs/Profile/modals/Accounts/index.tsx +++ b/src/screens/tabs/Profile/modals/Accounts/index.tsx @@ -1,4 +1,4 @@ -import Clipboard from '@react-native-community/clipboard'; +import Clipboard from '@react-native-clipboard/clipboard'; import { t } from 'i18next'; import React, { RefObject, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Alert, Platform, View } from 'react-native'; diff --git a/src/screens/tabs/Profile/modals/ActivityDetail/index.tsx b/src/screens/tabs/Profile/modals/ActivityDetail/index.tsx new file mode 100644 index 00000000..6cc0f653 --- /dev/null +++ b/src/screens/tabs/Profile/modals/ActivityDetail/index.tsx @@ -0,0 +1,133 @@ +import Clipboard from '@react-native-clipboard/clipboard'; +import { t } from 'i18next'; +import React, { RefObject } from 'react'; +import { View } from 'react-native'; +import { Modalize } from 'react-native-modalize'; + +import { + ActionButton, + Header, + Modal, + Text, + Touchable, +} from '@/components/common'; +import useCustomToast from '@/hooks/useCustomToast'; +import { Contact, Transaction } from '@/interfaces/redux'; +import { useAppSelector } from '@/redux/hooks'; +import ActivityItem from '@/screens/tabs/components/ActivityItem'; +import { isOwnAddress, validateICNSName } from '@/utils/ids'; +import shortAddress from '@/utils/shortAddress'; +import { capitalize } from '@/utils/strings'; + +import styles from './styles'; + +interface Props { + modalRef: RefObject; + activity: Transaction; + onClosed: () => void; +} + +interface RowProps { + title: string; + value: string; + onPress?: (address: string) => void; + showSelf?: boolean; + hideRow?: boolean; + contact?: Contact; +} + +const formatAddress = (address: string) => + validateICNSName(address) + ? address + : shortAddress(address, { + leftSize: 5, + rightSize: 9, + separator: '...', + replace: [], + }); + +function ActivityDetail({ modalRef, activity, onClosed }: Props) { + const { currentWallet } = useAppSelector(state => state.keyring); + const { contacts } = useAppSelector(state => state.user); + const { showInfo } = useCustomToast(); + + const closeModal = () => modalRef?.current?.close(); + const handleOnCopy = (address: string) => () => { + Clipboard.setString(address); + showInfo(t('activity.details.copied')); + }; + + const ROWS = + activity && currentWallet + ? [ + { + title: t('activity.details.trxType'), + value: capitalize(activity.type?.toLocaleLowerCase()), + }, + { + title: t('activity.details.from'), + value: formatAddress(activity.from), + onPress: handleOnCopy(activity.from), + showSelf: isOwnAddress(activity.from, currentWallet), + hideRow: activity.type === 'MINT', + contact: contacts.find(c => c.id === activity.from), + }, + { + title: t('activity.details.to'), + value: formatAddress(activity.to), + onPress: handleOnCopy(activity.to), + showSelf: isOwnAddress(activity.to, currentWallet), + contact: contacts.find(c => c.id === activity.to), + }, + ] + : []; + + const renderRow = ( + { onPress, hideRow, title, value, showSelf, contact }: RowProps, + index: number + ) => { + const isTouchable = !!onPress; + + return !hideRow ? ( + + + {title} + + + + {value} + {(showSelf || !!contact) && ( + + {showSelf ? t('activity.details.you') : ` (${contact?.name})`} + + )} + + + + ) : null; + }; + + return ( + + } + center={{t('activity.details.title')}} + /> + }> + + + {ROWS.map(renderRow)} + + + ); +} + +export default ActivityDetail; diff --git a/src/screens/tabs/Profile/modals/ActivityDetail/styles.ts b/src/screens/tabs/Profile/modals/ActivityDetail/styles.ts new file mode 100644 index 00000000..444ff4a8 --- /dev/null +++ b/src/screens/tabs/Profile/modals/ActivityDetail/styles.ts @@ -0,0 +1,36 @@ +import { StyleSheet } from 'react-native'; + +import { Colors } from '@/constants/theme'; + +const ITEM_HEIGHT = 95; + +export default StyleSheet.create({ + container: { + flex: 1, + paddingHorizontal: 20, + }, + activityItem: { + paddingHorizontal: 0, + borderTopWidth: 1, + borderBottomWidth: 1, + borderColor: Colors.Divider[1], + height: ITEM_HEIGHT, + marginBottom: 24, + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + rowTitle: { + color: Colors.Gray.Pure, + marginBottom: 24, + }, + rowValue: { + color: Colors.White.Primary, + marginBottom: 24, + }, + link: { + color: Colors.ActionBlue, + }, +}); diff --git a/src/screens/tabs/Profile/screens/Contacts/index.tsx b/src/screens/tabs/Profile/screens/Contacts/index.tsx index 1197cfba..3b697229 100644 --- a/src/screens/tabs/Profile/screens/Contacts/index.tsx +++ b/src/screens/tabs/Profile/screens/Contacts/index.tsx @@ -1,4 +1,4 @@ -import Clipboard from '@react-native-community/clipboard'; +import Clipboard from '@react-native-clipboard/clipboard'; import { t } from 'i18next'; import React, { Fragment, useMemo, useRef, useState } from 'react'; import { Platform, RefreshControl, View } from 'react-native'; diff --git a/src/screens/tabs/Tokens/index.js b/src/screens/tabs/Tokens/index.js index f4547d73..e1eaa51e 100644 --- a/src/screens/tabs/Tokens/index.js +++ b/src/screens/tabs/Tokens/index.js @@ -1,4 +1,4 @@ -import Clipboard from '@react-native-community/clipboard'; +import Clipboard from '@react-native-clipboard/clipboard'; import { useScrollToTop } from '@react-navigation/native'; import { t } from 'i18next'; import React, { useMemo, useRef, useState } from 'react'; diff --git a/src/screens/tabs/components/ActivityIcon/index.js b/src/screens/tabs/components/ActivityIcon/index.js deleted file mode 100644 index 32ce23ab..00000000 --- a/src/screens/tabs/components/ActivityIcon/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { Image, StyleSheet, View } from 'react-native'; - -import Icon from '@/components/icons'; -import { ACTIVITY_IMAGES } from '@/constants/business'; - -import { parseImageName } from '../utils'; -import styles from './styles'; - -const ActivityIcon = ({ image, type }) => { - return ( - - {type && ( - - )} - {image?.includes('http') ? ( - - ) : ( - - )} - - ); -}; - -export default ActivityIcon; diff --git a/src/screens/tabs/components/ActivityIcon/index.tsx b/src/screens/tabs/components/ActivityIcon/index.tsx new file mode 100644 index 00000000..b20d4c9d --- /dev/null +++ b/src/screens/tabs/components/ActivityIcon/index.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; + +import { Image } from '@/components/common'; +import Icon from '@/components/icons'; + +import { getNativeTokensLogo, getTypeIcon } from '../utils'; +import styles from './styles'; + +interface Props { + logo?: string; + type?: string; + symbol: string; +} + +const ActivityIcon = ({ logo, type, symbol }: Props) => { + return ( + + {type && } + {logo ? ( + + ) : ( + + )} + + ); +}; + +export default ActivityIcon; diff --git a/src/screens/tabs/components/ActivityIcon/styles.js b/src/screens/tabs/components/ActivityIcon/styles.ts similarity index 100% rename from src/screens/tabs/components/ActivityIcon/styles.js rename to src/screens/tabs/components/ActivityIcon/styles.ts diff --git a/src/screens/tabs/components/ActivityItem/index.js b/src/screens/tabs/components/ActivityItem/index.js deleted file mode 100644 index 5626d3a6..00000000 --- a/src/screens/tabs/components/ActivityItem/index.js +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { View } from 'react-native'; - -import Text from '@/components/common/Text'; -import { VISIBLE_DECIMALS } from '@/constants/business'; -import { FontStyles } from '@/constants/theme'; -import UsdFormat from '@/formatters/UsdFormat'; -import { formatDate } from '@/utils/dates'; -import { formatToMaxDecimals } from '@/utils/number'; -import shortAddress from '@/utils/shortAddress'; - -import ActivityIcon from '../ActivityIcon'; -import { getCanisterName, getStatus, getSubtitle, getTitle } from '../utils'; -import styles, { HEIGHT } from './styles'; - -export const ITEM_HEIGHT = HEIGHT; - -const ActivityItem = ({ - type, - to, - from, - amount, - value, - status, - date, - plug, - swapData, - symbol, - image, - canisterId, - details, - canisterInfo, -}) => { - const { t } = useTranslation(); - const isSonic = !!details?.sonicData; - const isSwap = type === 'SWAP'; - const isLiquidity = type.includes('Liquidity'); - - return ( - - - - - {getTitle(type, symbol, swapData, plug)} - - - {getStatus(status, styles)} - {formatDate(date, 'MMM Do')} - {getSubtitle(type, to, from, canisterId)} - - - - {details?.tokenId && !isSonic ? ( - <> - - {details?.tokenId?.length > 5 - ? shortAddress(details?.tokenId) - : `#${details?.tokenId}`} - - - {getCanisterName(canisterInfo, canisterId)} - - - ) : isSwap || isLiquidity ? ( - {t('common.comingSoon')} - ) : ( - <> - {amount ? ( - {`${formatToMaxDecimals( - Number(amount), - VISIBLE_DECIMALS - )} ${symbol}`} - ) : null} - {value ? ( - - ) : null} - - )} - - - ); -}; - -export default ActivityItem; diff --git a/src/screens/tabs/components/ActivityItem/index.tsx b/src/screens/tabs/components/ActivityItem/index.tsx new file mode 100644 index 00000000..533b6fca --- /dev/null +++ b/src/screens/tabs/components/ActivityItem/index.tsx @@ -0,0 +1,93 @@ +import { t } from 'i18next'; +import React from 'react'; +import { StyleProp, View, ViewStyle } from 'react-native'; + +import { Text, Touchable } from '@/components/common'; +import { VISIBLE_DECIMALS } from '@/constants/business'; +import { FontStyles } from '@/constants/theme'; +import UsdFormat from '@/formatters/UsdFormat'; +import { Transaction } from '@/interfaces/redux'; +import { formatDate } from '@/utils/dates'; +import { formatToMaxDecimals } from '@/utils/number'; +import shortAddress from '@/utils/shortAddress'; + +import ActivityIcon from '../ActivityIcon'; +import { getCanisterName, getStatus, getTitle } from '../utils'; +import styles, { HEIGHT } from './styles'; + +export const ITEM_HEIGHT = HEIGHT; + +interface Props extends Transaction { + onPress?: (trx: Transaction) => void; + style?: StyleProp; + hideAddress?: boolean; +} + +const ActivityItem = ({ + type, + amount, + value, + status, + date, + symbol, + logo, + canisterId, + details, + style, + canisterInfo, + onPress, +}: Props) => { + const isSonic = !!details?.sonicData; + const isLiquidity = type.includes('Liquidity'); + + return ( + + + + + + {getTitle(type, symbol)} + + + {getStatus(status, styles)} + {formatDate(date, 'MMM Do')} + + + + {details?.tokenId && !isSonic ? ( + <> + + {details?.tokenId?.length > 5 + ? shortAddress(details?.tokenId) + : `#${details?.tokenId}`} + + + {getCanisterName(canisterInfo, canisterId)} + + + ) : isLiquidity ? ( + {t('common.comingSoon')} + ) : ( + <> + {amount ? ( + {`${formatToMaxDecimals( + Number(amount), + VISIBLE_DECIMALS + )} ${symbol}`} + ) : null} + {value ? ( + + ) : null} + + )} + + + + ); +}; + +export default ActivityItem; diff --git a/src/screens/tabs/components/ActivityItem/styles.js b/src/screens/tabs/components/ActivityItem/styles.ts similarity index 82% rename from src/screens/tabs/components/ActivityItem/styles.js rename to src/screens/tabs/components/ActivityItem/styles.ts index c51dee38..f67bf498 100644 --- a/src/screens/tabs/components/ActivityItem/styles.js +++ b/src/screens/tabs/components/ActivityItem/styles.ts @@ -14,17 +14,21 @@ export default StyleSheet.create({ }, leftContainer: { justifyContent: 'space-evenly', + maxWidth: '50%', }, rightContainer: { marginLeft: 'auto', alignItems: 'flex-end', justifyContent: 'space-evenly', - }, - canisterName: { - maxWidth: '50%', + paddingLeft: 5, + maxWidth: '32%', }, title: { - maxWidth: '78%', + width: '100%', ...FontStyles.Normal, }, + text: { + ...FontStyles.SmallGray, + width: '100%', + }, }); diff --git a/src/screens/tabs/components/utils.js b/src/screens/tabs/components/utils.js index 48531a02..fae1cc6c 100644 --- a/src/screens/tabs/components/utils.js +++ b/src/screens/tabs/components/utils.js @@ -2,25 +2,45 @@ import { t } from 'i18next'; import React from 'react'; import Text from '@/components/common/Text'; -import { ACTIVITY_STATUS } from '@/constants/business'; +import { TOKENS } from '@/constants/assets'; +import { + ACTIVITY_IMAGES, + ACTIVITY_STATUS, + ACTIVITY_TYPES, +} from '@/constants/business'; import { JELLY_CANISTER_ID } from '@/constants/canister'; -import { validateICNSName } from '@/utils/ids'; -import shortAddress from '@/utils/shortAddress'; import { capitalize } from '@/utils/strings.js'; -export const parseImageName = name => name.replace('.png', '').toLowerCase(); +export const getNativeTokensLogo = symbol => { + switch (symbol) { + case TOKENS.ICP.symbol: + return TOKENS.ICP.icon; + case TOKENS.WICP.symbol: + return TOKENS.WICP.icon; + case TOKENS.XTC.symbol: + return TOKENS.XTC.icon; + default: + return 'unknown'; + } +}; -export const getTitle = (type, symbol, swapData, plug) => { +export const getTypeIcon = type => { + switch (type) { + case ACTIVITY_TYPES.RECEIVE: + return ACTIVITY_IMAGES.RECEIVE; + case ACTIVITY_TYPES.BURN: + return ACTIVITY_IMAGES.BURN; + case ACTIVITY_TYPES.SEND: + return ACTIVITY_IMAGES.SEND; + case ACTIVITY_TYPES.MINT: + return ACTIVITY_IMAGES.MINT; + default: + return 'actionActivity'; + } +}; + +export const getTitle = (type, symbol) => { switch (type) { - case 'SWAP': - return swapData?.currency.name - ? t('transactionTypes.swapFor', { - from: symbol, - to: swapData?.currency.name, - }) - : t('transactionTypes.swap'); - case 'PLUG': - return t('common.pluggedInto', { name: plug.name }); case 'DIRECTBUY': return t('transactionTypes.buyNTF'); case 'MAKELISTING': @@ -63,21 +83,6 @@ export const getStatus = (status, styles) => { } }; -export const getSubtitle = (type, to, from) => { - const toText = t('activity.subtitleTo', { - value: validateICNSName(to) ? to : shortAddress(to), - }); - const fromText = t('activity.subtitleFrom', { - value: validateICNSName(from) ? from : shortAddress(from), - }); - - return { - SEND: toText, - BURN: toText, - RECEIVE: fromText, - }[type]; -}; - export const getCanisterName = (canisterInfo, canisterId) => { // TODO: change this when jelly supports multi-collections if (canisterId === JELLY_CANISTER_ID) { diff --git a/src/translations/en/index.js b/src/translations/en/index.js index 7981f400..0484951b 100644 --- a/src/translations/en/index.js +++ b/src/translations/en/index.js @@ -141,6 +141,14 @@ const translations = { }, }, activity: { + details: { + title: 'Activity Detail', + trxType: 'Transaction Type:', + from: 'From:', + to: 'To:', + you: ' (you)', + copied: 'Address copied in clipboard', + }, [ACTIVITY_STATUS.COMPLETED]: 'Completed', [ACTIVITY_STATUS.PENDING]: 'Pending', [ACTIVITY_STATUS.REVERTED]: 'Failed', diff --git a/src/utils/ids.ts b/src/utils/ids.ts index d93397f4..9782a2d9 100644 --- a/src/utils/ids.ts +++ b/src/utils/ids.ts @@ -6,6 +6,7 @@ import { CANISTER_MAX_LENGTH, ICNS_REGEX, } from '@/constants/addresses'; +import { Wallet } from '@/interfaces/redux'; export const validateICNSName = (name: string) => ICNS_REGEX.test(name); @@ -27,3 +28,9 @@ export const validateCanisterId = (text: string) => { return false; } }; + +export const isOwnAddress = (address: string, currentWallet: Wallet) => + validateICNSName(address) + ? address === currentWallet.icnsData?.reverseResolvedName + : address === currentWallet.principal || + address === currentWallet.accountId; diff --git a/yarn.lock b/yarn.lock index 71424b49..9afe4b18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1828,10 +1828,10 @@ cross-fetch "^3.1.4" crypto-js "^4.1.1" -"@psychedelic/plug-controller@0.24.9": - version "0.24.9" - resolved "https://npm.pkg.github.com/download/@Psychedelic/plug-controller/0.24.9/ef37d22d00554de71b00782dbd7a440c2c9ee6f6#ef37d22d00554de71b00782dbd7a440c2c9ee6f6" - integrity sha512-PJd5oN+b30VOkjfge6sIqpN9K2w9Xawcc2Tw+ByAXIpLFNVUPOmcOm2LOuMgPo++xtlqAonGsDAGqQ4KUq/4Zw== +"@psychedelic/plug-controller@0.25.0": + version "0.25.0" + resolved "https://npm.pkg.github.com/download/@Psychedelic/plug-controller/0.25.0/44dd9d6b554615d07390d389f397fbe8a65f8ebd#44dd9d6b554615d07390d389f397fbe8a65f8ebd" + integrity sha512-4o1k3Z698JraEuN++d5l+vGPf11ax5m0PXvRk99Bn/i6uQ9KQD09HYF/mA3dF8++stnHqgtC1XCTID2wFIXJEg== dependencies: "@dfinity/agent" "0.9.3" "@dfinity/candid" "0.9.3" @@ -1868,6 +1868,11 @@ dependencies: merge-options "^3.0.4" +"@react-native-clipboard/clipboard@^1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.11.1.tgz#d3a9e685ce2383b1e92b89a334896c5575cc103d" + integrity sha512-nvSIIHzybVWqYxcJE5hpT17ekxAAg383Ggzw5WrYHtkKX61N1AwaKSNmXs5xHV7pmKSOe/yWjtSwxIzfW51I5Q== + "@react-native-community/blur@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.2.0.tgz#f100d0ba220ecfed26be3c0ad2ceffa5eee17533" @@ -2058,11 +2063,6 @@ prompts "^2.4.0" semver "^6.3.0" -"@react-native-community/clipboard@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@react-native-community/clipboard/-/clipboard-1.5.1.tgz#32abb3ea2eb91ee3f9c5fb1d32d5783253c9fabe" - integrity sha512-AHAmrkLEH5UtPaDiRqoULERHh3oNv7Dgs0bTC0hO5Z2GdNokAMPT5w8ci8aMcRemcwbtdHjxChgtjbeA38GBdA== - "@react-native-community/eslint-config@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@react-native-community/eslint-config/-/eslint-config-3.1.0.tgz#80f9471bae00d0676b98436bbb3a596eca2d69ab"