From ea7b4462a3bc453bc65f0c99c68e8283aaa1c28c Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 22 Jul 2024 21:11:58 +0700 Subject: [PATCH 01/31] init custom wallet --- .../Widget/CustomWalletLayout/ActivityTab.tsx | 23 +++ .../Widget/CustomWalletLayout/EmptyTab.tsx | 22 +++ .../Widget/CustomWalletLayout/NftTab.tsx | 90 +++++++++ .../CustomWalletLayout/TokenList/Item.tsx | 78 ++++++++ .../TokenList/ListEmpty.tsx | 23 +++ .../TokenList/Separator.tsx | 31 +++ .../CustomWalletLayout/TokenList/index.tsx | 75 ++++++++ .../Widget/CustomWalletLayout/TokenTab.tsx | 25 +++ .../CustomWalletLayout/WalletCard/Address.tsx | 88 +++++++++ .../CustomWalletLayout/WalletCard/Balance.tsx | 63 +++++++ .../CustomWalletLayout/WalletCard/index.tsx | 87 +++++++++ .../CustomWalletLayout/WalletCard/shared.ts | 9 + .../Widget/CustomWalletLayout/index.tsx | 176 ++++++++++++++++++ .../Widget/CustomWalletLayout/shared.ts | 51 +++++ packages/core/utils/widget.ts | 1 + 15 files changed, 842 insertions(+) create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/ActivityTab.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/EmptyTab.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Item.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/ListEmpty.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Separator.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/index.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/TokenTab.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Address.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Balance.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/index.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/shared.ts create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/shared.ts diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/ActivityTab.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/ActivityTab.tsx new file mode 100644 index 000000000..3603d28aa --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/ActivityTab.tsx @@ -0,0 +1,23 @@ +import type { FC } from 'react'; +import { StyleSheet } from 'react-native'; +import type { Networks } from '@walless/core'; +import HistoryFeature from 'features/History'; + +interface Props { + network: Networks; +} + +const ActivityTab: FC = ({ network }) => { + return ; +}; + +export default ActivityTab; + +const styles = StyleSheet.create({ + container: { + flex: 1, + marginVertical: 16, + borderRadius: 10, + overflow: 'hidden', + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/EmptyTab.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/EmptyTab.tsx new file mode 100644 index 000000000..8a793246e --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/EmptyTab.tsx @@ -0,0 +1,22 @@ +import type { FC } from 'react'; +import { StyleSheet } from 'react-native'; +import { Text, View } from '@walless/gui'; + +export const EmptyTab: FC = () => { + return ( + + Not available yet + + ); +}; + +export default EmptyTab; + +const styles = StyleSheet.create({ + text: { + textAlign: 'center', + color: '#566674', + fontSize: 13, + marginTop: 120, + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx new file mode 100644 index 000000000..56713b005 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx @@ -0,0 +1,90 @@ +import type { FC } from 'react'; +import { ScrollView, StyleSheet } from 'react-native'; +import type { Networks } from '@walless/core'; +import { Text, View } from '@walless/gui'; +import CollectionCard from 'components/CollectionCard'; +import type { WrappedCollection } from 'utils/hooks'; +import { useLazyGridLayout, useNfts } from 'utils/hooks'; +import { navigate } from 'utils/navigation'; + +interface Props { + network: Networks; +} + +export const NftTab: FC = ({ network }) => { + const { collections } = useNfts(network); + const { onGridContainerLayout, width } = useLazyGridLayout({ + referenceWidth: 150, + gap: gridGap, + }); + + const handlePressItem = (ele: WrappedCollection) => { + const collectionId = ele._id.split('/')[2]; + + navigate('Dashboard', { + screen: 'Explore', + params: { + screen: 'Collection', + params: { + screen: 'Default', + params: { id: collectionId }, + }, + }, + }); + }; + + return ( + onGridContainerLayout(e.nativeEvent.layout)} + > + {collections.length === 0 && ( + + You do not have any NFT yet + + )} + + {width > 0 && + collections.map((ele, index) => { + return ( + handlePressItem(ele)} + size={width} + /> + ); + })} + + + ); +}; + +export default NftTab; + +const gridGap = 18; +const styles = StyleSheet.create({ + container: { + marginTop: 16, + marginBottom: 32, + borderRadius: 12, + overflow: 'hidden', + }, + contentContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: gridGap, + overflow: 'hidden', + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + }, + emptyText: { + marginTop: 120, + fontSize: 13, + color: '#566674', + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Item.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Item.tsx new file mode 100644 index 000000000..85a78087d --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Item.tsx @@ -0,0 +1,78 @@ +import type { FC } from 'react'; +import type { StyleProp, ViewStyle } from 'react-native'; +import { Image, StyleSheet } from 'react-native'; +import { Hoverable, Text, View } from '@walless/gui'; +import type { TokenDocument } from '@walless/store'; +import assets from 'utils/assets'; +import { formatQuote } from 'utils/format'; + +interface Props { + style?: StyleProp; + token: TokenDocument; + onPress?: () => void; +} + +export const TokenItem: FC = ({ style, token, onPress }) => { + const { symbol, image, quotes, balance } = token; + const unitQuote = quotes?.usd; + const totalQuote = unitQuote && unitQuote * balance; + const iconSource = image ? { uri: image } : assets.misc.unknownToken; + + const itemName = symbol || 'Unknown'; + + return ( + + + + {itemName} + {formatQuote(unitQuote)} + + + {balance} + + {formatQuote(totalQuote, unitQuote ? 0 : '-')} + + + + ); +}; + +export default TokenItem; + +const iconSize = 32; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#131C24', + paddingVertical: 8, + paddingHorizontal: 12, + }, + iconImg: { + width: iconSize, + height: iconSize, + borderRadius: iconSize / 2, + overflow: 'hidden', + backgroundColor: 'rgba(255, 255, 255, 0.1)', + }, + infoContainer: { + flex: 1, + paddingVertical: 4, + paddingHorizontal: 12, + gap: 2, + }, + balanceContainer: { + paddingVertical: 4, + alignItems: 'flex-end', + gap: 2, + }, + primaryText: { + color: 'white', + fontWeight: '500', + }, + secondaryText: { + color: '#566674', + fontSize: 13, + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/ListEmpty.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/ListEmpty.tsx new file mode 100644 index 000000000..549092060 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/ListEmpty.tsx @@ -0,0 +1,23 @@ +import { StyleSheet } from 'react-native'; +import { Text, View } from '@walless/gui'; + +export const ListEmpty = () => { + return ( + + You do not have any tokens yet. + + ); +}; + +export default ListEmpty; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + paddingTop: 120, + }, + text: { + fontSize: 13, + color: '#566674', + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Separator.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Separator.tsx new file mode 100644 index 000000000..2a14a0aaf --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Separator.tsx @@ -0,0 +1,31 @@ +import type { FC } from 'react'; +import type { StyleProp, ViewStyle } from 'react-native'; +import { StyleSheet } from 'react-native'; +import { View } from '@walless/gui'; + +type Props = { + style?: StyleProp; +}; + +const Separator: FC = ({ style }) => { + return ( + + + + ); +}; + +export default Separator; + +const styles = StyleSheet.create({ + container: { + backgroundColor: '#131C24', + paddingHorizontal: 12, + }, + inner: { + width: '100%', + height: 1, + backgroundColor: '#566674', + opacity: 0.4, + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/index.tsx new file mode 100644 index 000000000..37038bd3e --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/index.tsx @@ -0,0 +1,75 @@ +import type { ComponentType, ReactElement } from 'react'; +import type { ListRenderItem, StyleProp, ViewStyle } from 'react-native'; +import { FlatList, StyleSheet } from 'react-native'; +import type { Token } from '@walless/core'; +import type { TokenDocument } from '@walless/store'; + +import TokenItem from './Item'; +import ListEmpty from './ListEmpty'; +import Separator from './Separator'; + +interface Props { + style?: StyleProp; + itemStyle?: StyleProp; + separateStyle?: StyleProp; + contentContainerStyle?: StyleProp; + items: TokenDocument[]; + ListHeaderComponent?: ComponentType> | ReactElement; + onPressItem?: (item: TokenDocument) => void; +} + +export const TokenList = ({ + style, + itemStyle, + separateStyle, + contentContainerStyle, + items, + ListHeaderComponent, + onPressItem, +}: Props) => { + const renderItem: ListRenderItem> = ({ item, index }) => { + const handlePressItem = () => { + onPressItem?.(item); + }; + + return ( + + ); + }; + + return ( + item._id} + ItemSeparatorComponent={() => } + ListEmptyComponent={ListEmpty} + /> + ); +}; + +export default TokenList; + +const styles = StyleSheet.create({ + firstItem: { + borderTopLeftRadius: 12, + borderTopRightRadius: 12, + }, + lastItem: { + borderBottomLeftRadius: 12, + borderBottomRightRadius: 12, + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenTab.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenTab.tsx new file mode 100644 index 000000000..aedbd5f96 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenTab.tsx @@ -0,0 +1,25 @@ +import type { FC } from 'react'; +import { StyleSheet } from 'react-native'; +import type { Networks } from '@walless/core'; +import { useTokens } from 'utils/hooks'; + +import TokenList from './TokenList'; + +interface Props { + network: Networks; +} + +export const TokenTab: FC = ({ network }) => { + const { tokens } = useTokens(network); + + return ; +}; + +export default TokenTab; + +const styles = StyleSheet.create({ + tokenListContainer: { + marginVertical: 16, + overflow: 'hidden', + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Address.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Address.tsx new file mode 100644 index 000000000..7e4176c7b --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Address.tsx @@ -0,0 +1,88 @@ +import type { FC } from 'react'; +import type { ViewStyle } from 'react-native'; +import { Image, StyleSheet } from 'react-native'; +import { shortenAddress } from '@walless/core'; +import { Hoverable, Text, View } from '@walless/gui'; +import { Copy } from '@walless/icons'; +import type { PublicKeyDocument } from '@walless/store'; + +import type { CardSkin } from './shared'; + +interface Props { + index: number; + skin: CardSkin; + item: PublicKeyDocument; + onCopyAddress?: (value: string) => void; +} + +export const WalletAddress: FC = ({ + index, + item, + skin, + onCopyAddress, +}) => { + const { iconSrc, iconColor = '#ffffff', iconSize } = skin; + const iconContainerStyle: ViewStyle = { + width: iconWrapperSize, + height: iconWrapperSize, + borderRadius: iconWrapperSize / 2, + backgroundColor: iconColor, + alignItems: 'center', + justifyContent: 'center', + }; + const iconStyle = { + width: iconSize, + height: iconSize, + borderRadius: iconSize / 2, + }; + + const handleCopy = () => { + item._id && onCopyAddress?.(item._id); + }; + + return ( + + + + + + + {`Wallet #${index + 1}: ${shortenAddress(item._id as string)}`} + + + + + + + + ); +}; + +export default WalletAddress; + +const iconWrapperSize = 22; +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignSelf: 'flex-start', + alignItems: 'center', + padding: 5, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + borderRadius: 30, + }, + addressText: { + marginHorizontal: 5, + color: 'white', + }, + iconWrapper: { + width: iconWrapperSize, + height: iconWrapperSize, + alignItems: 'center', + justifyContent: 'center', + }, + iconInner: { + opacity: 0.4, + backgroundColor: '#FFFFFF', + borderRadius: iconWrapperSize / 2, + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Balance.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Balance.tsx new file mode 100644 index 000000000..70985c880 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Balance.tsx @@ -0,0 +1,63 @@ +import type { FC } from 'react'; +import { StyleSheet } from 'react-native'; +import { Hoverable, Text, View } from '@walless/gui'; +import { Eye, EyeOff } from '@walless/icons'; +import { getValuationDisplay } from 'utils/helper'; + +interface Props { + onHide: (next: boolean) => void; + hideBalance: boolean; + valuation?: number; +} + +export const WalletBalance: FC = ({ + onHide, + hideBalance, + valuation = 0, +}) => { + const balanceTextStyle = [ + styles.balanceText, + hideBalance && styles.protectedBalance, + ]; + + return ( + + + onHide?.(!hideBalance)}> + {hideBalance ? : } + + + {getValuationDisplay(valuation, hideBalance)} + + + + ); +}; + +export default WalletBalance; + +const styles = StyleSheet.create({ + container: { + marginVertical: 8, + }, + balanceContainer: { + flexDirection: 'row', + alignItems: 'center', + paddingLeft: 5, + paddingBottom: 20, + gap: 10, + }, + balanceText: { + height: 42, + fontSize: 35, + fontWeight: '500', + color: 'white', + }, + protectedBalance: { + paddingTop: 7, + }, + estimationText: { + opacity: 0.6, + marginLeft: 34, + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/index.tsx new file mode 100644 index 000000000..a1bbc4777 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/index.tsx @@ -0,0 +1,87 @@ +import type { FC } from 'react'; +import type { ImageStyle, ViewStyle } from 'react-native'; +import { Image, ImageBackground, StyleSheet } from 'react-native'; +import { View } from '@walless/gui'; +import type { PublicKeyDocument } from '@walless/store'; + +import WalletAddress from './Address'; +import WalletBalance from './Balance'; +import type { CardSkin } from './shared'; + +interface Props { + style?: ViewStyle; + width?: number; + index?: number; + item: PublicKeyDocument; + skin: CardSkin; + valuation?: number; + hideBalance: boolean; + onCopyAddress?: (value: string) => void; + onChangePrivateSetting?: (value: boolean) => void; +} + +export const WalletCard: FC = ({ + width = 312, + index = 0, + item, + skin, + valuation = 0, + hideBalance, + onCopyAddress, + onChangePrivateSetting, +}) => { + const preferedHeight = (width * 145) / 318; + const height = Math.min(preferedHeight, 200); + const containerStyle: ImageStyle = { + width, + height, + aspectRatio: width / height, + borderRadius: 12, + paddingHorizontal: 18, + justifyContent: 'center', + overflow: 'hidden', + }; + + const handleHide = (next: boolean) => { + onChangePrivateSetting?.(next); + }; + + return ( + + + + {skin.largeIconSrc && ( + + + + )} + + ); +}; + +export default WalletCard; + +const styles = StyleSheet.create({ + markContainer: { + position: 'absolute', + top: 0, + bottom: 0, + right: -34, + justifyContent: 'center', + }, + markImage: { + width: 130, + height: 130, + }, +}); + +export * from './shared'; diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/shared.ts b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/shared.ts new file mode 100644 index 000000000..fe8eda641 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/shared.ts @@ -0,0 +1,9 @@ +import type { ImageSourcePropType } from 'react-native'; + +export interface CardSkin { + backgroundSrc: ImageSourcePropType; + largeIconSrc?: ImageSourcePropType; + iconSrc: ImageSourcePropType; + iconSize: number; + iconColor: string; +} diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx new file mode 100644 index 000000000..ae95be084 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -0,0 +1,176 @@ +import type { FC } from 'react'; +import { useMemo, useState } from 'react'; +import type { + LayoutChangeEvent, + LayoutRectangle, + ViewStyle, +} from 'react-native'; +import { StyleSheet, View } from 'react-native'; +import Animated from 'react-native-reanimated'; +import { Networks } from '@walless/core'; +import type { SlideOption } from '@walless/gui'; +import { Slider, SliderTabs } from '@walless/gui'; +import type { TabAble, TabItemStyle } from '@walless/gui/components/SliderTabs'; +import FeatureButtons from 'components/FeatureButtons'; +import { showCopiedModal } from 'modals/Notification'; +import { showReceiveModal } from 'modals/Receive'; +import { showSendTokenModal } from 'modals/SendToken'; +import { showSwapModal } from 'modals/Swap'; +import { buyToken } from 'utils/buy'; +import { useOpacityAnimated, usePublicKeys, useTokens } from 'utils/hooks'; +import { copy } from 'utils/system'; + +import TokenTab from '../BuiltInNetwork/TokenTab'; + +import ActivityTab from './ActivityTab'; +import NftTab from './NftTab'; +import { getWalletCardSkin, layoutTabs } from './shared'; +import WalletCard from './WalletCard'; + +interface Props { + id: string; +} + +export const CustomWalletLayout: FC = ({ id }) => { + const network = id as Networks; + const [activeTabIndex, setActiveTabIndex] = useState(0); + const keys = usePublicKeys(network); + const [headerLayout, setHeaderLayout] = useState(); + const { valuation } = useTokens(network); + const cardSkin = useMemo(() => getWalletCardSkin(network), [network]); + const opacityAnimated = useOpacityAnimated({ from: 0, to: 1 }); + + const container: ViewStyle = { + ...styles.container, + }; + + const bottomSliderItems: SlideOption[] = useMemo(() => { + return [ + { + id: 'tokens', + component: () => , + }, + { + id: 'collectibles', + component: () => , + }, + { + id: 'activities', + component: () => , + }, + ]; + }, []); + + const activatedStyle: TabItemStyle = { + containerStyle: { + backgroundColor: '#0694D3', + }, + textStyle: { + color: 'white', + fontWeight: '500', + }, + }; + + const deactivatedStyle: TabItemStyle = { + containerStyle: { + backgroundColor: 'transparent', + }, + textStyle: { + color: '#566674', + fontWeight: '400', + }, + }; + + const handleTabPress = (item: TabAble) => { + const idx = layoutTabs.indexOf(item); + setActiveTabIndex(idx); + }; + + const onHeaderLayout = ({ nativeEvent }: LayoutChangeEvent) => { + setHeaderLayout(nativeEvent.layout); + }; + + const handlePressSend = () => { + showSendTokenModal({ network: id as Networks }); + }; + + const handlePressReceive = () => { + showReceiveModal({ network: id as Networks }); + }; + + const handlePressSwap = () => { + showSwapModal({ network: id as Networks }); + }; + + const handlePressBuy = () => { + buyToken(id as Networks); + }; + + const handleCopyAddress = (value: string) => { + copy(value); + showCopiedModal(); + }; + + return ( + + + {headerLayout?.width && + keys.map((item, index) => { + return ( + + ); + })} + + + + + + + + + ); +}; + +export default CustomWalletLayout; + +const headingSpacing = 18; +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingTop: 12, + paddingHorizontal: 18, + }, + headerContainer: { + alignItems: 'center', + gap: headingSpacing, + paddingBottom: headingSpacing, + }, + sliderContainer: { + flex: 1, + overflow: 'hidden', + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/shared.ts b/apps/wallet/src/features/Widget/CustomWalletLayout/shared.ts new file mode 100644 index 000000000..6784568e4 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/shared.ts @@ -0,0 +1,51 @@ +import { Networks } from '@walless/core'; +import type { TabAble } from '@walless/gui/components/SliderTabs/TabItem'; +import assets from 'utils/assets'; + +import type { CardSkin } from './WalletCard'; + +export const getWalletCardSkin = (id: Networks): CardSkin => { + let asset; + let iconSize = 16; + let iconColor = '#242424'; + + if (id === Networks.solana) { + asset = assets.widget.solana; + iconColor = '#000000'; + } else if (id === Networks.tezos) { + asset = assets.widget.tezos; + iconColor = '#2D7DF8'; + } else if (id === Networks.sui) { + iconColor = '#FFFFFF'; + iconSize = 12; + asset = assets.widget.sui; + } else if (id === Networks.aptos) { + iconColor = '#000000'; + asset = assets.widget.aptos; + } else { + throw Error('Unsupported network'); + } + + return { + backgroundSrc: asset.widgetMeta.cardBackground, + iconSrc: asset.widgetMeta.cardIcon, + largeIconSrc: asset.widgetMeta.cardMark, + iconColor, + iconSize, + }; +}; + +export const layoutTabs: TabAble[] = [ + { + id: 'tokens', + title: 'Tokens', + }, + { + id: 'collectibles', + title: 'Collectibles', + }, + { + id: 'activities', + title: 'Activities', + }, +]; diff --git a/packages/core/utils/widget.ts b/packages/core/utils/widget.ts index 31b35d919..d692473f7 100644 --- a/packages/core/utils/widget.ts +++ b/packages/core/utils/widget.ts @@ -24,6 +24,7 @@ export enum WidgetType { GAME = 'Game', DEFI = 'DeFi', NFT = 'NFT', + COMMUNITY = 'Community', } export interface Widget { From 59ead0bb65c4f9ead6073a04d8a9e599e5b0c681 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Sat, 27 Jul 2024 16:10:06 +0700 Subject: [PATCH 02/31] construct a custom wallet --- .../components/WidgetButtons/ButtonItem.tsx | 62 ++++++++++++ .../src/components/WidgetButtons/index.tsx | 40 ++++++++ apps/wallet/src/features/Explorer/index.tsx | 20 ++++ .../Widget/BuiltInNetwork/TokenTab.tsx | 15 ++- .../features/Widget/BuiltInNetwork/index.tsx | 4 +- .../Widget/CustomWalletLayout/ActivityTab.tsx | 23 ----- .../Advertisement/AdvertisementItem.tsx | 76 +++++++++++++++ .../Advertisement/index.tsx | 95 +++++++++++++++++++ .../Widget/CustomWalletLayout/EmptyTab.tsx | 22 ----- .../CustomWalletLayout/FeatureButtons.tsx | 77 +++++++++++++++ .../Widget/CustomWalletLayout/NftTab.tsx | 31 +++--- .../CustomWalletLayout/TokenList/Item.tsx | 78 --------------- .../TokenList/ListEmpty.tsx | 23 ----- .../TokenList/Separator.tsx | 31 ------ .../CustomWalletLayout/TokenList/index.tsx | 75 --------------- .../Widget/CustomWalletLayout/TokenTab.tsx | 25 ----- .../CustomWalletLayout/WalletCard/Address.tsx | 88 ----------------- .../CustomWalletLayout/WalletCard/Balance.tsx | 63 ------------ .../CustomWalletLayout/WalletCard/index.tsx | 87 ----------------- .../CustomWalletLayout/WalletCard/shared.ts | 9 -- .../Widget/CustomWalletLayout/index.tsx | 93 ++++++++++-------- .../Widget/CustomWalletLayout/shared.ts | 35 ------- apps/wallet/src/state/widget/shared.ts | 25 +++-- apps/wallet/src/utils/hooks/wallet.ts | 2 +- packages/core/utils/widget.ts | 40 +++++++- .../gui/components/SliderTabs/TabItem.tsx | 37 +++++++- 26 files changed, 540 insertions(+), 636 deletions(-) create mode 100644 apps/wallet/src/components/WidgetButtons/ButtonItem.tsx create mode 100644 apps/wallet/src/components/WidgetButtons/index.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/ActivityTab.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/EmptyTab.tsx create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/FeatureButtons.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Item.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/ListEmpty.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Separator.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/index.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/TokenTab.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Address.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Balance.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/index.tsx delete mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/shared.ts diff --git a/apps/wallet/src/components/WidgetButtons/ButtonItem.tsx b/apps/wallet/src/components/WidgetButtons/ButtonItem.tsx new file mode 100644 index 000000000..98a994fb3 --- /dev/null +++ b/apps/wallet/src/components/WidgetButtons/ButtonItem.tsx @@ -0,0 +1,62 @@ +import type { FC } from 'react'; +import type { TextStyle, ViewStyle } from 'react-native'; +import { StyleSheet } from 'react-native'; +import { Hoverable, Text, View } from '@walless/gui'; +import type { IconProps } from '@walless/icons'; + +export interface WidgetButtonProps { + style?: ViewStyle; + title?: string; + titleStyle?: TextStyle; + Icon: FC; + iconColor?: string; + iconSize?: number; + onPress?: () => void; +} + +export const ButtonItem: FC = ({ + Icon, + iconColor, + iconSize, + onPress, + style, + title, + titleStyle, +}) => { + const innerStyle: ViewStyle = { + width: 38, + height: 38, + borderRadius: 12, + gap: 8, + backgroundColor: onPress ? '#0694D3' : '#43525F', + alignItems: 'center', + justifyContent: 'center', + }; + + return ( + + + {} + + {title && {title}} + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + }, + innerContainer: { + borderRadius: 12, + }, + title: { + color: '#4e5e6b', + fontSize: 13, + marginTop: 8, + }, +}); diff --git a/apps/wallet/src/components/WidgetButtons/index.tsx b/apps/wallet/src/components/WidgetButtons/index.tsx new file mode 100644 index 000000000..26e26d4b7 --- /dev/null +++ b/apps/wallet/src/components/WidgetButtons/index.tsx @@ -0,0 +1,40 @@ +import type { FC } from 'react'; +import type { ViewStyle } from 'react-native'; +import { StyleSheet } from 'react-native'; +import { View } from '@walless/gui'; + +import type { WidgetButtonProps } from './ButtonItem'; +import { ButtonItem } from './ButtonItem'; + +interface Props { + style?: ViewStyle; + buttons: WidgetButtonProps[]; +} + +const WidgetButtons: FC = ({ style, buttons }) => { + return ( + + {buttons.map((item, idx) => ( + + ))} + + ); +}; + +export default WidgetButtons; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + gap: 18, + }, +}); diff --git a/apps/wallet/src/features/Explorer/index.tsx b/apps/wallet/src/features/Explorer/index.tsx index 04a0f141c..2842ed43a 100644 --- a/apps/wallet/src/features/Explorer/index.tsx +++ b/apps/wallet/src/features/Explorer/index.tsx @@ -3,6 +3,7 @@ import type { StyleProp, ViewStyle } from 'react-native'; import { ScrollView, StyleSheet } from 'react-native'; import { View } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; +import Advertisement from 'features/Widget/CustomWalletLayout/Advertisement'; import Header from './Header'; import Highlights from './Highlights'; @@ -17,6 +18,24 @@ interface Props { onToggleDrawer?: () => void; } +const ads = [ + { + title: 'Cute Kitten', + image: 'https://placehold.co/640x480/e67e22/ffffff', + link: 'https://example.com/kitten', + }, + { + title: 'Majestic Mountain', + image: 'https://placehold.co/640x480/39cccc/ffffff', + link: 'https://example.com/mountain', + }, + { + title: 'Vibrant Sunset', + image: 'https://placehold.co/640x480/f39c12/ffffff', + link: 'https://example.com/sunset', + }, +]; + export const ExplorerFeature: FC = ({ style }) => { return ( @@ -26,6 +45,7 @@ export const ExplorerFeature: FC = ({ style }) => { + ); diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenTab.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/TokenTab.tsx index aedbd5f96..fba60df19 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenTab.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/TokenTab.tsx @@ -1,18 +1,25 @@ import type { FC } from 'react'; import { StyleSheet } from 'react-native'; -import type { Networks } from '@walless/core'; +import type { Networks, Token } from '@walless/core'; +import type { TokenDocument } from '@walless/store'; import { useTokens } from 'utils/hooks'; import TokenList from './TokenList'; interface Props { network: Networks; + tokens?: TokenDocument[]; } -export const TokenTab: FC = ({ network }) => { - const { tokens } = useTokens(network); +export const TokenTab: FC = ({ network, tokens }) => { + const { tokens: networkTokens } = useTokens(network); - return ; + return ( + + ); }; export default TokenTab; diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx index bd68650f0..8f8a0adc2 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx @@ -68,7 +68,7 @@ export const BuiltInNetwork: FC = ({ id }) => { const activatedStyle: TabItemStyle = { containerStyle: { - backgroundColor: '#0694D3', + style: { backgroundColor: '#0694D3' }, }, textStyle: { color: 'white', @@ -78,7 +78,7 @@ export const BuiltInNetwork: FC = ({ id }) => { const deactivatedStyle: TabItemStyle = { containerStyle: { - backgroundColor: 'transparent', + style: { backgroundColor: 'transparent' }, }, textStyle: { color: '#566674', diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/ActivityTab.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/ActivityTab.tsx deleted file mode 100644 index 3603d28aa..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/ActivityTab.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import type { FC } from 'react'; -import { StyleSheet } from 'react-native'; -import type { Networks } from '@walless/core'; -import HistoryFeature from 'features/History'; - -interface Props { - network: Networks; -} - -const ActivityTab: FC = ({ network }) => { - return ; -}; - -export default ActivityTab; - -const styles = StyleSheet.create({ - container: { - flex: 1, - marginVertical: 16, - borderRadius: 10, - overflow: 'hidden', - }, -}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx new file mode 100644 index 000000000..6b2cf5cf9 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx @@ -0,0 +1,76 @@ +import type { FC } from 'react'; +import { Image, Platform, StyleSheet } from 'react-native'; +import type { SharedValue } from 'react-native-reanimated'; +import Animated, { + interpolate, + useAnimatedStyle, +} from 'react-native-reanimated'; +import type { CustomWalletAdvertisement } from '@walless/core'; +import { Anchor, Text } from '@walless/gui'; +import { ArrowTopRight } from '@walless/icons'; + +type ItemProps = CustomWalletAdvertisement & { + currentIndex: number; + index: number; + offsetX: SharedValue; + animatedValue: SharedValue; +}; + +const IMAGE_SIZE = 266; + +const AdvertisementItem: FC = ({ + image, + link, + title, + currentIndex, + index, + offsetX, + animatedValue, +}) => { + const imageSrc = { uri: image }; + + const animatedStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateX: + (index - currentIndex) * (IMAGE_SIZE + 20) + offsetX.value, + }, + ], + }; + }, [currentIndex, offsetX, animatedValue]); + + return ( + + + + {title} + + + + ); +}; + +export default AdvertisementItem; + +const styles = StyleSheet.create({ + container: { + borderRadius: 10, + overflow: 'hidden', + position: 'absolute', + }, + image: { + width: 266, + height: 118, + }, + linkContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: 10, + paddingHorizontal: 12, + }, + title: { + color: '#ffffff', + fontWeight: '500', + }, +}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx new file mode 100644 index 000000000..9f8041271 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx @@ -0,0 +1,95 @@ +import type { FC } from 'react'; +import { useRef, useState } from 'react'; +import type { FlatList } from 'react-native-gesture-handler'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import { useSharedValue } from 'react-native-reanimated'; +import type { CustomWalletAdvertisement } from '@walless/core'; +import { View } from '@walless/gui'; + +import AdvertisementItem from './AdvertisementItem'; + +const IMAGE_SIZE = 266; + +interface Props { + ads: CustomWalletAdvertisement[]; +} + +const Advertisement: FC = ({ ads }) => { + const scrollOffset = useRef(0); + const scrollRef = useRef(null); + const [currentIndex, setCurrentIndex] = useState(0); + const animatedValue = useSharedValue(0); + const offsetX = useSharedValue(0); + + const pan = Gesture.Pan() + .onUpdate((event) => { + scrollRef.current?.scrollToOffset({ + offset: scrollOffset.current - event.translationX, + animated: false, + }); + offsetX.value = event.translationX; + }) + .onFinalize((event) => { + offsetX.value = 0; + + if (currentIndex === 0 && event.translationX > 0) return; + if (currentIndex === ads.length - 1 && event.translationX < 0) return; + + if (event.translationX < -150) { + animatedValue.value = currentIndex + 1; + setCurrentIndex(currentIndex + 1); + } else if (event.translationX > 150) { + animatedValue.value = currentIndex - 1; + setCurrentIndex(currentIndex - 1); + } + }); + + return ( + + + {/* { + return ( + + ); + }} + /> */} + {ads.map((item, index) => { + return ( + + ); + })} + + + ); +}; + +export default Advertisement; diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/EmptyTab.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/EmptyTab.tsx deleted file mode 100644 index 8a793246e..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/EmptyTab.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { FC } from 'react'; -import { StyleSheet } from 'react-native'; -import { Text, View } from '@walless/gui'; - -export const EmptyTab: FC = () => { - return ( - - Not available yet - - ); -}; - -export default EmptyTab; - -const styles = StyleSheet.create({ - text: { - textAlign: 'center', - color: '#566674', - fontSize: 13, - marginTop: 120, - }, -}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/FeatureButtons.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/FeatureButtons.tsx new file mode 100644 index 000000000..1e0daef27 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/FeatureButtons.tsx @@ -0,0 +1,77 @@ +import type { FC } from 'react'; +import { useMemo } from 'react'; +import type { Networks } from '@walless/core'; +import { ArrowBottomRight, ArrowTopRight, Plus, Swap } from '@walless/icons'; +import WidgetButtons from 'components/WidgetButtons'; +import type { WidgetButtonProps } from 'components/WidgetButtons/ButtonItem'; +import { showReceiveModal } from 'modals/Receive'; +import { showSendTokenModal } from 'modals/SendToken'; +import { showSwapModal } from 'modals/Swap'; +import { buyToken } from 'utils/buy'; + +interface Props { + network: Networks; + send: string; + receive: string; + buy: string; + swap: string; +} + +const FeatureButtons: FC = ({ buy, receive, send, swap, network }) => { + const handlePressSend = () => { + showSendTokenModal({ network }); + }; + + const handlePressReceive = () => { + showReceiveModal({ network }); + }; + + const handlePressSwap = () => { + showSwapModal({ network }); + }; + + const handlePressBuy = () => { + buyToken(network); + }; + + const widgetButtons: WidgetButtonProps[] = useMemo(() => { + return [ + { + title: 'Send', + Icon: ArrowTopRight, + style: { + backgroundColor: send, + }, + onPress: handlePressSend, + }, + { + title: 'Receive', + Icon: ArrowBottomRight, + style: { + backgroundColor: receive, + }, + onPress: handlePressReceive, + }, + { + title: 'Buy', + Icon: Plus, + style: { + backgroundColor: buy, + }, + onPress: handlePressBuy, + }, + { + title: 'Swap', + Icon: Swap, + style: { + backgroundColor: swap, + }, + onPress: handlePressSwap, + }, + ]; + }, []); + + return ; +}; + +export default FeatureButtons; diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx index 56713b005..7db258f4a 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx @@ -1,58 +1,59 @@ import type { FC } from 'react'; import { ScrollView, StyleSheet } from 'react-native'; -import type { Networks } from '@walless/core'; +import type { Networks, Nft } from '@walless/core'; import { Text, View } from '@walless/gui'; +import type { NftDocument } from '@walless/store'; import CollectionCard from 'components/CollectionCard'; -import type { WrappedCollection } from 'utils/hooks'; import { useLazyGridLayout, useNfts } from 'utils/hooks'; import { navigate } from 'utils/navigation'; interface Props { network: Networks; + requiredNfts?: NftDocument[]; } -export const NftTab: FC = ({ network }) => { - const { collections } = useNfts(network); +export const NftTab: FC = ({ network, requiredNfts }) => { + const { nfts } = useNfts(network); const { onGridContainerLayout, width } = useLazyGridLayout({ referenceWidth: 150, gap: gridGap, }); - const handlePressItem = (ele: WrappedCollection) => { - const collectionId = ele._id.split('/')[2]; - + const handleNavigateToCollectible = (id: string) => { navigate('Dashboard', { screen: 'Explore', params: { screen: 'Collection', - params: { - screen: 'Default', - params: { id: collectionId }, - }, + params: { screen: 'NFT', params: { id } }, }, }); }; + const filteredNfts = nfts.filter( + (item) => requiredNfts?.some((ele) => ele._id === item._id), + ); + return ( onGridContainerLayout(e.nativeEvent.layout)} > - {collections.length === 0 && ( + {filteredNfts.length === 0 && ( You do not have any NFT yet )} {width > 0 && - collections.map((ele, index) => { + nfts && + nfts.map((ele, index) => { + const collectibleId = ele._id.split('/')[2]; return ( handlePressItem(ele)} + onPress={() => handleNavigateToCollectible(collectibleId)} size={width} /> ); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Item.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Item.tsx deleted file mode 100644 index 85a78087d..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Item.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import type { FC } from 'react'; -import type { StyleProp, ViewStyle } from 'react-native'; -import { Image, StyleSheet } from 'react-native'; -import { Hoverable, Text, View } from '@walless/gui'; -import type { TokenDocument } from '@walless/store'; -import assets from 'utils/assets'; -import { formatQuote } from 'utils/format'; - -interface Props { - style?: StyleProp; - token: TokenDocument; - onPress?: () => void; -} - -export const TokenItem: FC = ({ style, token, onPress }) => { - const { symbol, image, quotes, balance } = token; - const unitQuote = quotes?.usd; - const totalQuote = unitQuote && unitQuote * balance; - const iconSource = image ? { uri: image } : assets.misc.unknownToken; - - const itemName = symbol || 'Unknown'; - - return ( - - - - {itemName} - {formatQuote(unitQuote)} - - - {balance} - - {formatQuote(totalQuote, unitQuote ? 0 : '-')} - - - - ); -}; - -export default TokenItem; - -const iconSize = 32; - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: '#131C24', - paddingVertical: 8, - paddingHorizontal: 12, - }, - iconImg: { - width: iconSize, - height: iconSize, - borderRadius: iconSize / 2, - overflow: 'hidden', - backgroundColor: 'rgba(255, 255, 255, 0.1)', - }, - infoContainer: { - flex: 1, - paddingVertical: 4, - paddingHorizontal: 12, - gap: 2, - }, - balanceContainer: { - paddingVertical: 4, - alignItems: 'flex-end', - gap: 2, - }, - primaryText: { - color: 'white', - fontWeight: '500', - }, - secondaryText: { - color: '#566674', - fontSize: 13, - }, -}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/ListEmpty.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/ListEmpty.tsx deleted file mode 100644 index 549092060..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/ListEmpty.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { StyleSheet } from 'react-native'; -import { Text, View } from '@walless/gui'; - -export const ListEmpty = () => { - return ( - - You do not have any tokens yet. - - ); -}; - -export default ListEmpty; - -const styles = StyleSheet.create({ - container: { - alignItems: 'center', - paddingTop: 120, - }, - text: { - fontSize: 13, - color: '#566674', - }, -}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Separator.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Separator.tsx deleted file mode 100644 index 2a14a0aaf..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/Separator.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { FC } from 'react'; -import type { StyleProp, ViewStyle } from 'react-native'; -import { StyleSheet } from 'react-native'; -import { View } from '@walless/gui'; - -type Props = { - style?: StyleProp; -}; - -const Separator: FC = ({ style }) => { - return ( - - - - ); -}; - -export default Separator; - -const styles = StyleSheet.create({ - container: { - backgroundColor: '#131C24', - paddingHorizontal: 12, - }, - inner: { - width: '100%', - height: 1, - backgroundColor: '#566674', - opacity: 0.4, - }, -}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/index.tsx deleted file mode 100644 index 37038bd3e..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenList/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import type { ComponentType, ReactElement } from 'react'; -import type { ListRenderItem, StyleProp, ViewStyle } from 'react-native'; -import { FlatList, StyleSheet } from 'react-native'; -import type { Token } from '@walless/core'; -import type { TokenDocument } from '@walless/store'; - -import TokenItem from './Item'; -import ListEmpty from './ListEmpty'; -import Separator from './Separator'; - -interface Props { - style?: StyleProp; - itemStyle?: StyleProp; - separateStyle?: StyleProp; - contentContainerStyle?: StyleProp; - items: TokenDocument[]; - ListHeaderComponent?: ComponentType> | ReactElement; - onPressItem?: (item: TokenDocument) => void; -} - -export const TokenList = ({ - style, - itemStyle, - separateStyle, - contentContainerStyle, - items, - ListHeaderComponent, - onPressItem, -}: Props) => { - const renderItem: ListRenderItem> = ({ item, index }) => { - const handlePressItem = () => { - onPressItem?.(item); - }; - - return ( - - ); - }; - - return ( - item._id} - ItemSeparatorComponent={() => } - ListEmptyComponent={ListEmpty} - /> - ); -}; - -export default TokenList; - -const styles = StyleSheet.create({ - firstItem: { - borderTopLeftRadius: 12, - borderTopRightRadius: 12, - }, - lastItem: { - borderBottomLeftRadius: 12, - borderBottomRightRadius: 12, - }, -}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenTab.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/TokenTab.tsx deleted file mode 100644 index aedbd5f96..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/TokenTab.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { FC } from 'react'; -import { StyleSheet } from 'react-native'; -import type { Networks } from '@walless/core'; -import { useTokens } from 'utils/hooks'; - -import TokenList from './TokenList'; - -interface Props { - network: Networks; -} - -export const TokenTab: FC = ({ network }) => { - const { tokens } = useTokens(network); - - return ; -}; - -export default TokenTab; - -const styles = StyleSheet.create({ - tokenListContainer: { - marginVertical: 16, - overflow: 'hidden', - }, -}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Address.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Address.tsx deleted file mode 100644 index 7e4176c7b..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Address.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import type { FC } from 'react'; -import type { ViewStyle } from 'react-native'; -import { Image, StyleSheet } from 'react-native'; -import { shortenAddress } from '@walless/core'; -import { Hoverable, Text, View } from '@walless/gui'; -import { Copy } from '@walless/icons'; -import type { PublicKeyDocument } from '@walless/store'; - -import type { CardSkin } from './shared'; - -interface Props { - index: number; - skin: CardSkin; - item: PublicKeyDocument; - onCopyAddress?: (value: string) => void; -} - -export const WalletAddress: FC = ({ - index, - item, - skin, - onCopyAddress, -}) => { - const { iconSrc, iconColor = '#ffffff', iconSize } = skin; - const iconContainerStyle: ViewStyle = { - width: iconWrapperSize, - height: iconWrapperSize, - borderRadius: iconWrapperSize / 2, - backgroundColor: iconColor, - alignItems: 'center', - justifyContent: 'center', - }; - const iconStyle = { - width: iconSize, - height: iconSize, - borderRadius: iconSize / 2, - }; - - const handleCopy = () => { - item._id && onCopyAddress?.(item._id); - }; - - return ( - - - - - - - {`Wallet #${index + 1}: ${shortenAddress(item._id as string)}`} - - - - - - - - ); -}; - -export default WalletAddress; - -const iconWrapperSize = 22; -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignSelf: 'flex-start', - alignItems: 'center', - padding: 5, - backgroundColor: 'rgba(255, 255, 255, 0.2)', - borderRadius: 30, - }, - addressText: { - marginHorizontal: 5, - color: 'white', - }, - iconWrapper: { - width: iconWrapperSize, - height: iconWrapperSize, - alignItems: 'center', - justifyContent: 'center', - }, - iconInner: { - opacity: 0.4, - backgroundColor: '#FFFFFF', - borderRadius: iconWrapperSize / 2, - }, -}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Balance.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Balance.tsx deleted file mode 100644 index 70985c880..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/Balance.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import type { FC } from 'react'; -import { StyleSheet } from 'react-native'; -import { Hoverable, Text, View } from '@walless/gui'; -import { Eye, EyeOff } from '@walless/icons'; -import { getValuationDisplay } from 'utils/helper'; - -interface Props { - onHide: (next: boolean) => void; - hideBalance: boolean; - valuation?: number; -} - -export const WalletBalance: FC = ({ - onHide, - hideBalance, - valuation = 0, -}) => { - const balanceTextStyle = [ - styles.balanceText, - hideBalance && styles.protectedBalance, - ]; - - return ( - - - onHide?.(!hideBalance)}> - {hideBalance ? : } - - - {getValuationDisplay(valuation, hideBalance)} - - - - ); -}; - -export default WalletBalance; - -const styles = StyleSheet.create({ - container: { - marginVertical: 8, - }, - balanceContainer: { - flexDirection: 'row', - alignItems: 'center', - paddingLeft: 5, - paddingBottom: 20, - gap: 10, - }, - balanceText: { - height: 42, - fontSize: 35, - fontWeight: '500', - color: 'white', - }, - protectedBalance: { - paddingTop: 7, - }, - estimationText: { - opacity: 0.6, - marginLeft: 34, - }, -}); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/index.tsx deleted file mode 100644 index a1bbc4777..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type { FC } from 'react'; -import type { ImageStyle, ViewStyle } from 'react-native'; -import { Image, ImageBackground, StyleSheet } from 'react-native'; -import { View } from '@walless/gui'; -import type { PublicKeyDocument } from '@walless/store'; - -import WalletAddress from './Address'; -import WalletBalance from './Balance'; -import type { CardSkin } from './shared'; - -interface Props { - style?: ViewStyle; - width?: number; - index?: number; - item: PublicKeyDocument; - skin: CardSkin; - valuation?: number; - hideBalance: boolean; - onCopyAddress?: (value: string) => void; - onChangePrivateSetting?: (value: boolean) => void; -} - -export const WalletCard: FC = ({ - width = 312, - index = 0, - item, - skin, - valuation = 0, - hideBalance, - onCopyAddress, - onChangePrivateSetting, -}) => { - const preferedHeight = (width * 145) / 318; - const height = Math.min(preferedHeight, 200); - const containerStyle: ImageStyle = { - width, - height, - aspectRatio: width / height, - borderRadius: 12, - paddingHorizontal: 18, - justifyContent: 'center', - overflow: 'hidden', - }; - - const handleHide = (next: boolean) => { - onChangePrivateSetting?.(next); - }; - - return ( - - - - {skin.largeIconSrc && ( - - - - )} - - ); -}; - -export default WalletCard; - -const styles = StyleSheet.create({ - markContainer: { - position: 'absolute', - top: 0, - bottom: 0, - right: -34, - justifyContent: 'center', - }, - markImage: { - width: 130, - height: 130, - }, -}); - -export * from './shared'; diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/shared.ts b/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/shared.ts deleted file mode 100644 index fe8eda641..000000000 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/WalletCard/shared.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { ImageSourcePropType } from 'react-native'; - -export interface CardSkin { - backgroundSrc: ImageSourcePropType; - largeIconSrc?: ImageSourcePropType; - iconSrc: ImageSourcePropType; - iconSize: number; - iconColor: string; -} diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx index ae95be084..b3f9831c7 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -5,39 +5,65 @@ import type { LayoutRectangle, ViewStyle, } from 'react-native'; -import { StyleSheet, View } from 'react-native'; +import { Platform, StyleSheet, View } from 'react-native'; import Animated from 'react-native-reanimated'; -import { Networks } from '@walless/core'; +import type { CustomWalletMetadata } from '@walless/core'; import type { SlideOption } from '@walless/gui'; import { Slider, SliderTabs } from '@walless/gui'; import type { TabAble, TabItemStyle } from '@walless/gui/components/SliderTabs'; -import FeatureButtons from 'components/FeatureButtons'; import { showCopiedModal } from 'modals/Notification'; -import { showReceiveModal } from 'modals/Receive'; -import { showSendTokenModal } from 'modals/SendToken'; -import { showSwapModal } from 'modals/Swap'; -import { buyToken } from 'utils/buy'; -import { useOpacityAnimated, usePublicKeys, useTokens } from 'utils/hooks'; +import { mockWidgets } from 'state/widget'; +import { getTokenValue, useOpacityAnimated, usePublicKeys } from 'utils/hooks'; import { copy } from 'utils/system'; +import ActivityTab from '../BuiltInNetwork/ActivityTab'; import TokenTab from '../BuiltInNetwork/TokenTab'; +import type { CardSkin } from '../BuiltInNetwork/WalletCard'; +import { WalletCard } from '../BuiltInNetwork/WalletCard'; -import ActivityTab from './ActivityTab'; +import FeatureButtons from './FeatureButtons'; import NftTab from './NftTab'; -import { getWalletCardSkin, layoutTabs } from './shared'; -import WalletCard from './WalletCard'; +import { layoutTabs } from './shared'; interface Props { id: string; } +const convertCustomMetadataToCardSkin = ( + customWalletMetadata: CustomWalletMetadata, +): CardSkin => { + let backgroundSrc = require(customWalletMetadata.coverBanner); + let iconSrc = require(customWalletMetadata.iconSrc); + if (Platform.OS == 'web') { + backgroundSrc = { uri: customWalletMetadata.coverBanner }; + iconSrc = { uri: customWalletMetadata.iconSrc }; + } + + return { + backgroundSrc, + iconSrc, + iconSize: 40, + iconColor: '#ffffff', + }; +}; + export const CustomWalletLayout: FC = ({ id }) => { - const network = id as Networks; + const customWalletWidget = mockWidgets.find((item) => item._id === id); const [activeTabIndex, setActiveTabIndex] = useState(0); + const customWalletMetadata = + customWalletWidget?.customMetadata as CustomWalletMetadata; + + const network = customWalletMetadata.network; + const tokens = customWalletMetadata.tokens; + const requiredNfts = customWalletMetadata.nfts; + const keys = usePublicKeys(network); const [headerLayout, setHeaderLayout] = useState(); - const { valuation } = useTokens(network); - const cardSkin = useMemo(() => getWalletCardSkin(network), [network]); + const valuation = tokens?.reduce( + (accumulator, token) => accumulator + getTokenValue(token, 'usd'), + 0, + ); + const cardSkin = convertCustomMetadataToCardSkin(customWalletMetadata); const opacityAnimated = useOpacityAnimated({ from: 0, to: 1 }); const container: ViewStyle = { @@ -48,11 +74,13 @@ export const CustomWalletLayout: FC = ({ id }) => { return [ { id: 'tokens', - component: () => , + component: () => , }, { id: 'collectibles', - component: () => , + component: () => ( + + ), }, { id: 'activities', @@ -62,9 +90,7 @@ export const CustomWalletLayout: FC = ({ id }) => { }, []); const activatedStyle: TabItemStyle = { - containerStyle: { - backgroundColor: '#0694D3', - }, + containerStyle: customWalletMetadata.activeTabStyle, textStyle: { color: 'white', fontWeight: '500', @@ -73,7 +99,7 @@ export const CustomWalletLayout: FC = ({ id }) => { const deactivatedStyle: TabItemStyle = { containerStyle: { - backgroundColor: 'transparent', + style: { backgroundColor: 'transparent' }, }, textStyle: { color: '#566674', @@ -90,27 +116,13 @@ export const CustomWalletLayout: FC = ({ id }) => { setHeaderLayout(nativeEvent.layout); }; - const handlePressSend = () => { - showSendTokenModal({ network: id as Networks }); - }; - - const handlePressReceive = () => { - showReceiveModal({ network: id as Networks }); - }; - - const handlePressSwap = () => { - showSwapModal({ network: id as Networks }); - }; - - const handlePressBuy = () => { - buyToken(id as Networks); - }; - const handleCopyAddress = (value: string) => { copy(value); showCopiedModal(); }; + if (!customWalletWidget) return null; + return ( @@ -131,10 +143,11 @@ export const CustomWalletLayout: FC = ({ id }) => { })} diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/shared.ts b/apps/wallet/src/features/Widget/CustomWalletLayout/shared.ts index 6784568e4..ff57d0d70 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/shared.ts +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/shared.ts @@ -1,39 +1,4 @@ -import { Networks } from '@walless/core'; import type { TabAble } from '@walless/gui/components/SliderTabs/TabItem'; -import assets from 'utils/assets'; - -import type { CardSkin } from './WalletCard'; - -export const getWalletCardSkin = (id: Networks): CardSkin => { - let asset; - let iconSize = 16; - let iconColor = '#242424'; - - if (id === Networks.solana) { - asset = assets.widget.solana; - iconColor = '#000000'; - } else if (id === Networks.tezos) { - asset = assets.widget.tezos; - iconColor = '#2D7DF8'; - } else if (id === Networks.sui) { - iconColor = '#FFFFFF'; - iconSize = 12; - asset = assets.widget.sui; - } else if (id === Networks.aptos) { - iconColor = '#000000'; - asset = assets.widget.aptos; - } else { - throw Error('Unsupported network'); - } - - return { - backgroundSrc: asset.widgetMeta.cardBackground, - iconSrc: asset.widgetMeta.cardIcon, - largeIconSrc: asset.widgetMeta.cardMark, - iconColor, - iconSize, - }; -}; export const layoutTabs: TabAble[] = [ { diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index f30ec92ce..d22ad5410 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -1,4 +1,4 @@ -import { Networks, WidgetType } from '@walless/core'; +import { Networks, WidgetCategory, WidgetType } from '@walless/core'; import type { WidgetDocument } from '@walless/store'; // TODO: this mocked data is for web only @@ -10,6 +10,7 @@ export const mockWidgets: WidgetDocument[] = [ version: '0.1.8', type: 'Widget', widgetType: WidgetType.GAME, + category: WidgetCategory.GAME, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-pixeverse.png', @@ -20,7 +21,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - networkMeta: { + customMetadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/explore/logo-pixeverse.png', iconUri: '/img/explore/logo-pixeverse.png', @@ -35,6 +36,7 @@ export const mockWidgets: WidgetDocument[] = [ version: '0.9.1', type: 'Widget', widgetType: WidgetType.NETWORK, + category: WidgetCategory.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-solana.png', @@ -46,7 +48,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 90, activeCount: 502, }, - networkMeta: { + customMetadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/network/solana-icon-lg.png', iconUri: '/img/network/solana-icon-sm.svg', @@ -61,6 +63,7 @@ export const mockWidgets: WidgetDocument[] = [ version: '0.0.1', type: 'Widget', widgetType: WidgetType.NETWORK, + category: WidgetCategory.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-sui.png', @@ -72,7 +75,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 100, activeCount: 567, }, - networkMeta: { + customMetadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/network/sui-icon-lg.png', iconUri: '/img/network/sui-icon-sm.png', @@ -87,6 +90,7 @@ export const mockWidgets: WidgetDocument[] = [ version: '0.0.1', type: 'Widget', widgetType: WidgetType.NETWORK, + category: WidgetCategory.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/network/tezos-icon-sm.png', @@ -98,7 +102,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 100, activeCount: 567, }, - networkMeta: { + customMetadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/network/tezos-icon-lg.png', iconUri: '/img/network/tezos-icon-sm.png', @@ -113,6 +117,7 @@ export const mockWidgets: WidgetDocument[] = [ version: '0.0.1', type: 'Widget', widgetType: WidgetType.NETWORK, + category: WidgetCategory.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-aptos.png', @@ -124,7 +129,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - networkMeta: { + customMetadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/explore/aptos-icon.svg', iconUri: '/img/explore/aptos-icon.svg', @@ -139,6 +144,7 @@ export const mockWidgets: WidgetDocument[] = [ version: '0.1.8', type: 'Widget', widgetType: WidgetType.GAME, + category: WidgetCategory.GAME, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/t-rex-runner/runner-icon.png', @@ -149,7 +155,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - networkMeta: { + customMetadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/t-rex-runner/runner-icon.png', iconUri: '/img/t-rex-runner/runner-icon.png', @@ -164,6 +170,7 @@ export const mockWidgets: WidgetDocument[] = [ version: '0.0.1', type: 'Widget', widgetType: WidgetType.GAME, + category: WidgetCategory.GAME, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/sui-jump/suijump-icon.png', @@ -174,7 +181,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - networkMeta: { + customMetadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/sui-jump/suijump-icon.png', iconUri: '/img/sui-jump/suijump-icon.png', @@ -199,7 +206,7 @@ export const mockWidgets: WidgetDocument[] = [ // loveCount: 46, // activeCount: 202, // }, - // networkMeta: { + // customMetadata: { // backgroundUri: '/img/network/sky-card-bg.png', // markUri: '/img/network/solana-icon-lg.png', // iconUri: '/img/explore/thumbnail-under-realm.png', diff --git a/apps/wallet/src/utils/hooks/wallet.ts b/apps/wallet/src/utils/hooks/wallet.ts index de5d4b5ab..25db2d403 100644 --- a/apps/wallet/src/utils/hooks/wallet.ts +++ b/apps/wallet/src/utils/hooks/wallet.ts @@ -48,7 +48,7 @@ export const useRelevantKeys = () => { }, [keyMap, widgetMap]); }; -const getTokenValue = (token: TokenDocument, currency: string) => { +export const getTokenValue = (token: TokenDocument, currency: string) => { const { quotes, balance } = token; const quote = quotes?.[currency] || 0; diff --git a/packages/core/utils/widget.ts b/packages/core/utils/widget.ts index d692473f7..78e8c6e38 100644 --- a/packages/core/utils/widget.ts +++ b/packages/core/utils/widget.ts @@ -1,3 +1,7 @@ +import type { Nft, Token } from '@walless/core'; +import type { TabContainerStyle } from '@walless/gui'; +import type { NftDocument, TokenDocument } from '@walless/store'; + import type { Networks } from './common'; export interface WidgetStoreOptions { @@ -11,7 +15,7 @@ export interface WidgetStoreOptions { activeCount: number; } -export interface WidgetNetworkOptions { +export interface WidgetNetworkMetadata { backgroundUri: string; markUri: string; iconUri: string; @@ -27,12 +31,44 @@ export enum WidgetType { COMMUNITY = 'Community', } +export interface CustomWalletAdvertisement { + title: string; + link: string; + image: string; +} + +export interface CustomWalletMetadata { + coverBanner: string; + iconSrc: string; + backgroundColor: string; + actionButtonBackgroundColors: { + send: string; + receive: string; + buy: string; + swap: string; + }; + activeTabStyle: TabContainerStyle; + advertisements: CustomWalletAdvertisement[]; + tokens?: TokenDocument[]; + nfts?: NftDocument[]; + network: Networks; +} + +export type CustomMetadata = CustomWalletMetadata | WidgetNetworkMetadata; + +export enum WidgetCategory { + GAME = 'Game', + CUSTOM_WALLET = 'CustomWallet', + NETWORK = 'Network', +} + export interface Widget { name: string; networks: Networks[]; version: string; timestamp?: string; widgetType: WidgetType; + category: WidgetCategory; storeMeta: WidgetStoreOptions; - networkMeta: WidgetNetworkOptions; + customMetadata?: CustomMetadata; } diff --git a/packages/gui/components/SliderTabs/TabItem.tsx b/packages/gui/components/SliderTabs/TabItem.tsx index f7dba3d10..0d0553556 100644 --- a/packages/gui/components/SliderTabs/TabItem.tsx +++ b/packages/gui/components/SliderTabs/TabItem.tsx @@ -1,10 +1,19 @@ import type { FC } from 'react'; import type { TextStyle, ViewStyle } from 'react-native'; import { StyleSheet } from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; import { Hoverable, Text } from '@walless/gui'; +export interface TabContainerStyle { + style: ViewStyle; + linearGradient?: { + isHorizontal: boolean; + colors: string[]; + }; +} + export interface TabItemStyle { - containerStyle: ViewStyle; + containerStyle: TabContainerStyle; textStyle: TextStyle; } @@ -20,9 +29,29 @@ interface Props { } export const TabItem: FC = ({ item, style, onPress }) => { + if (style?.containerStyle.linearGradient) { + const isHorizontal = style.containerStyle.linearGradient.isHorizontal; + + return ( + onPress?.(item)}> + + {item.title} + + + ); + } + return ( onPress?.(item)} > {item.title} @@ -32,7 +61,7 @@ export const TabItem: FC = ({ item, style, onPress }) => { export const activatedStyle: TabItemStyle = { containerStyle: { - backgroundColor: '#0694D3', + style: { backgroundColor: '#0694D3' }, }, textStyle: { color: 'white', @@ -42,7 +71,7 @@ export const activatedStyle: TabItemStyle = { export const deactivatedStyle: TabItemStyle = { containerStyle: { - backgroundColor: 'transparent', + style: { backgroundColor: 'transparent' }, }, textStyle: { color: '#566674', From 8f7c4d679340672c53a6e20b706779f32b3da5da Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 29 Jul 2024 11:59:59 +0700 Subject: [PATCH 03/31] add assets for samo widget --- apps/wallet/assets/img/widget/samo-ad-1.png | Bin 0 -> 49610 bytes apps/wallet/assets/img/widget/samo-banner.png | Bin 0 -> 50570 bytes apps/wallet/assets/img/widget/samo-cover.png | Bin 0 -> 77036 bytes apps/wallet/assets/img/widget/samo-icon.png | Bin 0 -> 3004 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/wallet/assets/img/widget/samo-ad-1.png create mode 100644 apps/wallet/assets/img/widget/samo-banner.png create mode 100644 apps/wallet/assets/img/widget/samo-cover.png create mode 100644 apps/wallet/assets/img/widget/samo-icon.png diff --git a/apps/wallet/assets/img/widget/samo-ad-1.png b/apps/wallet/assets/img/widget/samo-ad-1.png new file mode 100644 index 0000000000000000000000000000000000000000..19b7b7f5a0e81ad0b295082cf3cec41b2ceb3213 GIT binary patch literal 49610 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�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

iZ_ME3Z4JU2hf{)zzZtR@g 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󠪟>%&SSrxOfpgrbQiVbHS$jH+~q}ZM2 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 literal 0 HcmV?d00001 diff --git a/apps/wallet/assets/img/widget/samo-cover.png b/apps/wallet/assets/img/widget/samo-cover.png new file mode 100644 index 0000000000000000000000000000000000000000..18cbafafd028c60c36571ce3cab0a3161a160020 GIT binary patch literal 77036 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

    n>5&j?)|3sZGgqQK2?^5SyoCKeTu%rn7G}b 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 Txae7RtW?u%dI9d0ydKC}JGvJQq$T&`2WY(&# zu-#}6SQRFp8Z{)OZ5X!-=sTOUt(JhNsiz%gk57906BJHKnB)3oX-asIp)a zyY@_=tr?hGp&eFWAj$l&i(JP6SLwiob2Vew#D0LyCE>S0B4pS;Im~Og!YkTLd*7?m za-N;8=#HnJef)m|SP>-D@fnmfSU=xt2;R-{t=Q~`P z`l>G*izXpSn=)^!$&N8~r=A*}468yH#+LLGD0+HC?PEDj@LRl4R1le7C$oapbJ4D9 zc;QqjOyCjJy89g&-ILa5 zdIiR4eHml~vAZy^=d(s6bgw^OqO*SuBNF}1`NVI`g>2kpCZ91NEeHYHb|SoB4a!`~ zxln@8Ux0H9HCej^W4mQiSL@+T0+X=9Lv=j|iTy*z^hGr3$@q-!eHCfyYLUrD${2~N zCKVaqw4N^^*7)m?(Z%J4px`h)JXiBxT3MjA+f{kWrSk|$xA<~+iv;j=QzU?GuqDO9 z6zF3HVyMYa2QYT$j+JF|-j}GHJRU#$+3`xk^|!Rt5ZYdrsoCRl`pik$vwKWl{_>Y& zp| z$?SYyR4O+N8xjZ2z1F8oM6^V#hY3%e#0FwE4`z4MTV)v7)RvM1NAsX=OLFB5DRkD) zRJ7c|Xnq6|iEGCpL}IW_sA_0J$6kvrDZHcyp*%4Y*3(=B0K2&!Dz-`QBvi~KS_F23 z4;wwJaJvynm`cae)v2JC(M6*&!&zVYZ)QocCv(rNi(gJFrDz7rH;c1oU{+?TFo+h&rD|BM}78LspE3;t@OT`h_*VFS)Cr+UQ{&=p4q8x zJb(?GifJ8~cp71x1!}O1&^K&Ws@YYwwcYnLX%nf1DPP0 zRZ2eYksZAwa_8;$!>d`-w07cGtS@48cjIRiU1!uHg4wAi zT=7k+eti||-;2OLCMGA~5j`W1KmMegId@i#%fW*OBft3@?*>RbCg(3S%fdp=7|kg9a19m_Z2?Xs!X1WjYDmQ5Meb3wA2C^q z%!^Q|A9H+YG%F)mLzm8%WqHPgB%!iAUc5=f_+02pjDLo81Isq7y7Bln2got?Gkl2i zQ@$+D7i4gUD~FFZO9310Wi=)&^yFGIn?`I*oanr+)RWYr+q0^D@jaS(OHkCq~Ozv&7JF=j|yevlr@f zjE^hYq^mB1WbTii12cL97!bj=4FQj#SGhV{mDS6R)*C4it+PfN-0$f5CU^bN#@bYW zb20ezJUc)+_l>6I>?5G`D{h>=A13=Z+HnsPxK^6RtiLEd16z{I)pQg3x}uoKAcUqo zo}my^@o8Lq`|N8fx>J*_Vo|1nVN6a>%JlS%8V2^expeu8VskC6&2rO?H_B^Y{TexX z^fmyfO_G6^^~eA7&*abk{3FV~upHvScmI1ieCS5Gc)3MpW^xkPd7@?`s)mkO75>Rh zWGTm^Svw5y4TLmLy;n+gcda6LOjwVbneD$a1*mdoE z;vFkt=Pkes+QGVf6=3L=sqECmDr-Dw<$R@|L-iP_sXKTV++s}5rAv{8c8#ZExE#E` z7H7T^Fk+c`->sg?A1iGe_J`7)# z;bG7QL+OFx$h7#fGDn~k;nOA_$UQDVVNkU*kslbPfQ1ruyp6OxVmEl(REP{oH1#46 zMKi9MVVi>2)0UNkukhsAPh;nWQ1ddp1Cg?G#Zs%x3Y@am9tM&R1AmVZjh)Tj`L%r?j-4dJcE}s zw^jfE7NxzbB$=$qWpL!$Ng5r*GcI1ftVUsWW=0mkqOfLVV|_z~f7aI5!K|!k-3Y6j zqjYsv0`DE$Ek|xRqB$M=_8yS7j(%zDX_a$l*((}jx_1RE%z62@zxYckru^=Ay;Hse zMrM2h7|7M;SjpKMh5C- z+lLmnZ>?X5nKQ`>!szoK|MYTLo&y+qhKnYxG-EvDPq_9|(jd_!L&sfc4 zeh3U%$}y*D*#ToS5X#(F1LGxx_1csA152dIM1ybra!&XVwD9iNh+KKHrp8IN79=W+ z3yu58-Bxp5Tm`Y3Iy|Ljn}EMmRzq3DZeChSsmDP&lyyXdx3GgBvv7UQgbl;(PdTcg ziGJGt*J$}{2PxyFe6&Hu9)I&f{OY@-=e%#LP5@(2ZH8U>Th zS`b&Y-w0;oNmt5LPV#)~`q=XZj#dCG#?!6#NziKbiBXTl%wQTtGV2*IyBACiWhn;_!~Ol7(fg;tPqn0^ zdoPS~Z%XrQRzSm3rHn?H;?=I1J=ab``-yRfG%>qaY%sRDOJNc?BFU5Srqe-p_@G5sATd_27sQv1HDzny);M~DUFAFgn5F|dy;%Du5HgbguG;#*K>eE46? zi~a+iv=6v4&5&@KfNKVGku6l4B+8-iJafKO#z-?*^QXq6>O0X;#}0Jzd%G>$e{Ci* zJOhH5>%iI$y*!I?>y^z_jAu_qDcqIiyciI38*r3Z=jI#7y1jM7AMdEH9YutfV>dKQ z3)aDzXDQ`SRREOr5~la9S*%WweBNQ?PRH}cL>XZ{bP3~q)G(V-qFVA=qwH2k#7$hY z8H1F~aih+#BbQG5GIe=NhIcp1ZLe@;W&$Amg{o;7?-_4|!dPxX*f|c*KMqWUmL^4O zP6MKr)6)U>D2JH$r1ND}$>cG>Gx!}QZMM^(#GESu+gC1u+Ff&0U!=C_X_G1LGARI~ zy>|f1_(Dxe>`d?1k|adJPmUgrS=4&Rd_N}7&}&jM`9&<+O3DmYSbv(Ko$T>@^$0Zf z<9+DP#~2Xj9C%r1Bn2j_2CKmw?FM6!9jR%?Dr;vpreMstjoIo)yZ_p64b2uc4pKS| z-a+etFFm`PNxNcHN@IS^u{9@>*dpXw_t(yNgNRuD673J z4v)gnA(oxL;A$s68Vq$gqlCW1==_roCy1{-qN2A8)bNzPV@lPTUEDq>a^SA4Jo_2o z5x^AOAO~YxGvIe<2`1M(mk^UN1B@wv5pEm7vptg0Ud_|z%9;;BU`6|=DLds{6XJzY zfrq)kITPXmyh45_#%!AR~p)FdE~5Sp!Og5=!1XS^F_;cAl1>4`$; z2!uD;zm^%aCH9(h$c+n|>%rTzvOHIjOQ)(T;2kDH7YQWBS;EA6NY0N2?IHz+SF5_FozQN-k#}v2 z`%>JY*;*mhT9Zr%t85+hCB>Y+)*tq?+}iA36*Wph+z7~tGLnq} zu@ld@h9&{13M-y#6O#wSYUnVPv>_5E7GL}@!|mZr#QK}xD^2d3{25hX&pOgPR+H}C zo;F`k1KA#b4&%N=4>AoO&PXUq*jRnKJth8Y9gQKEy~NP%oGKZ7Osbl1(>S8_ z7$QEUE3rXSL&m@OIP;_N zaT?;7?YAAqUQ3e2bmH76vwl>LBS>kuUfD{?`KMu2JJw`$PqPfeOP{z9AoQzf%~w

    g3b#qn8` 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 Date: Mon, 29 Jul 2024 12:00:15 +0700 Subject: [PATCH 04/31] revert explorer --- apps/wallet/src/features/Explorer/index.tsx | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/apps/wallet/src/features/Explorer/index.tsx b/apps/wallet/src/features/Explorer/index.tsx index 2842ed43a..04a0f141c 100644 --- a/apps/wallet/src/features/Explorer/index.tsx +++ b/apps/wallet/src/features/Explorer/index.tsx @@ -3,7 +3,6 @@ import type { StyleProp, ViewStyle } from 'react-native'; import { ScrollView, StyleSheet } from 'react-native'; import { View } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; -import Advertisement from 'features/Widget/CustomWalletLayout/Advertisement'; import Header from './Header'; import Highlights from './Highlights'; @@ -18,24 +17,6 @@ interface Props { onToggleDrawer?: () => void; } -const ads = [ - { - title: 'Cute Kitten', - image: 'https://placehold.co/640x480/e67e22/ffffff', - link: 'https://example.com/kitten', - }, - { - title: 'Majestic Mountain', - image: 'https://placehold.co/640x480/39cccc/ffffff', - link: 'https://example.com/mountain', - }, - { - title: 'Vibrant Sunset', - image: 'https://placehold.co/640x480/f39c12/ffffff', - link: 'https://example.com/sunset', - }, -]; - export const ExplorerFeature: FC = ({ style }) => { return ( @@ -45,7 +26,6 @@ export const ExplorerFeature: FC = ({ style }) => { - ); From a0e8f110813d9d3946bf20f458faff5dd2e1796e Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 29 Jul 2024 12:00:49 +0700 Subject: [PATCH 05/31] adjust custom wallet layout --- .../Widget/CustomWalletLayout/index.tsx | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx index b3f9831c7..9558c8e96 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -5,9 +5,9 @@ import type { LayoutRectangle, ViewStyle, } from 'react-native'; -import { Platform, StyleSheet, View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import Animated from 'react-native-reanimated'; -import type { CustomWalletMetadata } from '@walless/core'; +import type { CustomWalletMetadata, WidgetStoreOptions } from '@walless/core'; import type { SlideOption } from '@walless/gui'; import { Slider, SliderTabs } from '@walless/gui'; import type { TabAble, TabItemStyle } from '@walless/gui/components/SliderTabs'; @@ -21,6 +21,7 @@ import TokenTab from '../BuiltInNetwork/TokenTab'; import type { CardSkin } from '../BuiltInNetwork/WalletCard'; import { WalletCard } from '../BuiltInNetwork/WalletCard'; +import Advertisement from './Advertisement'; import FeatureButtons from './FeatureButtons'; import NftTab from './NftTab'; import { layoutTabs } from './shared'; @@ -31,19 +32,18 @@ interface Props { const convertCustomMetadataToCardSkin = ( customWalletMetadata: CustomWalletMetadata, + storeMeta?: WidgetStoreOptions, ): CardSkin => { - let backgroundSrc = require(customWalletMetadata.coverBanner); - let iconSrc = require(customWalletMetadata.iconSrc); - if (Platform.OS == 'web') { - backgroundSrc = { uri: customWalletMetadata.coverBanner }; - iconSrc = { uri: customWalletMetadata.iconSrc }; - } + const backgroundSrc = { uri: customWalletMetadata.coverBanner }; + const iconSrc = { uri: customWalletMetadata.iconSrc }; + const iconSize = storeMeta?.iconSize || 40; + const iconColor = storeMeta?.iconColor || '#ffffff'; return { backgroundSrc, iconSrc, - iconSize: 40, - iconColor: '#ffffff', + iconSize, + iconColor, }; }; @@ -63,7 +63,10 @@ export const CustomWalletLayout: FC = ({ id }) => { (accumulator, token) => accumulator + getTokenValue(token, 'usd'), 0, ); - const cardSkin = convertCustomMetadataToCardSkin(customWalletMetadata); + const cardSkin = convertCustomMetadataToCardSkin( + customWalletMetadata, + customWalletWidget?.storeMeta, + ); const opacityAnimated = useOpacityAnimated({ from: 0, to: 1 }); const container: ViewStyle = { @@ -124,7 +127,13 @@ export const CustomWalletLayout: FC = ({ id }) => { if (!customWalletWidget) return null; return ( - + {headerLayout?.width && keys.map((item, index) => { @@ -164,6 +173,8 @@ export const CustomWalletLayout: FC = ({ id }) => { items={bottomSliderItems} activeItem={bottomSliderItems[activeTabIndex]} /> + + ); }; From 08790d10bed1bd0c21df06e7a421ed072487b335 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 29 Jul 2024 12:05:40 +0700 Subject: [PATCH 06/31] add advertisement for custom wallet --- .../Advertisement/AdvertisementItem.tsx | 8 ++-- .../Advertisement/index.tsx | 42 +++++-------------- 2 files changed, 14 insertions(+), 36 deletions(-) diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx index 6b2cf5cf9..33a435237 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx @@ -1,10 +1,7 @@ import type { FC } from 'react'; -import { Image, Platform, StyleSheet } from 'react-native'; +import { Image, StyleSheet } from 'react-native'; import type { SharedValue } from 'react-native-reanimated'; -import Animated, { - interpolate, - useAnimatedStyle, -} from 'react-native-reanimated'; +import Animated, { useAnimatedStyle } from 'react-native-reanimated'; import type { CustomWalletAdvertisement } from '@walless/core'; import { Anchor, Text } from '@walless/gui'; import { ArrowTopRight } from '@walless/icons'; @@ -68,6 +65,7 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', paddingVertical: 10, paddingHorizontal: 12, + backgroundColor: '#0C334E', }, title: { color: '#ffffff', diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx index 9f8041271..73260171a 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx @@ -1,5 +1,6 @@ import type { FC } from 'react'; import { useRef, useState } from 'react'; +import { StyleSheet } from 'react-native'; import type { FlatList } from 'react-native-gesture-handler'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import { useSharedValue } from 'react-native-reanimated'; @@ -8,8 +9,6 @@ import { View } from '@walless/gui'; import AdvertisementItem from './AdvertisementItem'; -const IMAGE_SIZE = 266; - interface Props { ads: CustomWalletAdvertisement[]; } @@ -46,35 +45,7 @@ const Advertisement: FC = ({ ads }) => { return ( - - {/* { - return ( - - ); - }} - /> */} + {ads.map((item, index) => { return ( = ({ ads }) => { }; export default Advertisement; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + minHeight: 200, + minWidth: 200, + paddingHorizontal: 20, + }, +}); From 584e1ab3581862fb0e8dbc61bc0a9946eca69a57 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 29 Jul 2024 12:09:40 +0700 Subject: [PATCH 07/31] adjust some related functions --- apps/wallet/src/features/Widget/internal.ts | 2 + apps/wallet/src/modals/RemoveLayout.tsx | 2 +- apps/wallet/src/state/widget/shared.ts | 47 ++++++++++++ apps/wallet/src/utils/assets/index.ts | 11 +++ apps/wallet/src/utils/assets/index.web.ts | 11 +++ packages/core/utils/widget.ts | 7 +- .../gui/components/SliderTabs/TabItem.tsx | 73 ++++++++++++++----- 7 files changed, 132 insertions(+), 21 deletions(-) diff --git a/apps/wallet/src/features/Widget/internal.ts b/apps/wallet/src/features/Widget/internal.ts index 88baee1fb..4e4175e15 100644 --- a/apps/wallet/src/features/Widget/internal.ts +++ b/apps/wallet/src/features/Widget/internal.ts @@ -1,6 +1,7 @@ import type { FC } from 'react'; import BuiltInNetwork from './BuiltInNetwork'; +import CustomWalletLayout from './CustomWalletLayout'; import NotFound from './NotFound'; import Pixeverse from './Pixeverse'; import SUIJump from './SUIJump'; @@ -20,6 +21,7 @@ export const widgetMap: Record = { tRexRunner: TRexRunner, pixeverse: Pixeverse, suijump: SUIJump, + samo: CustomWalletLayout, }; export const extractWidgetComponent = (id: string): WidgetComponent => { diff --git a/apps/wallet/src/modals/RemoveLayout.tsx b/apps/wallet/src/modals/RemoveLayout.tsx index 38a0d5076..dba424cca 100644 --- a/apps/wallet/src/modals/RemoveLayout.tsx +++ b/apps/wallet/src/modals/RemoveLayout.tsx @@ -42,7 +42,7 @@ const RemoveLayoutModal: FC<{ width: 36, height: 36, borderRadius: 6, - backgroundColor: item.networkMeta?.iconColor || 'white', + backgroundColor: item.storeMeta?.iconColor || 'white', }; return ( diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index d22ad5410..c01d73f76 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -1,4 +1,5 @@ import { Networks, WidgetCategory, WidgetType } from '@walless/core'; +import { gradientDirection } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; // TODO: this mocked data is for web only @@ -214,4 +215,50 @@ export const mockWidgets: WidgetDocument[] = [ // iconSize: 16, // }, // }, + { + _id: 'samo', + name: 'SAMO', + networks: [Networks.solana], + version: '0.0.1', + type: 'Widget', + widgetType: WidgetType.COMMUNITY, + category: WidgetCategory.CUSTOM_WALLET, + timestamp: new Date().toISOString(), + storeMeta: { + iconUri: '/img/widget/samo-icon.png', + iconSize: 26, + iconColor: '#ffffff', + coverUri: '/img/widget/samo-cover.png', + description: 'dApp version of the T-rex Runner you already known!', + loveCount: 46, + activeCount: 202, + }, + customMetadata: { + coverBanner: '/img/widget/samo-banner.png', + iconSrc: '/img/widget/samo-icon.png', + backgroundColor: '#141121', + actionButtonBackgroundColors: { + send: '#0051BD', + receive: '#3D55BF', + buy: '#7E60D2', + swap: '#C36BE5', + }, + activeTabStyle: { + linearGradient: { + direction: gradientDirection.LeftToRight, + colors: ['#1A4FB5', '#C36BE5'], + }, + }, + advertisements: [ + { + title: 'Get your SAMO debit card', + link: '', + image: '/img/widget/samo-ad-1.png', + }, + ], + tokens: [], + nfts: [], + network: Networks.solana, + }, + }, ]; diff --git a/apps/wallet/src/utils/assets/index.ts b/apps/wallet/src/utils/assets/index.ts index 699a6b916..a447f096f 100644 --- a/apps/wallet/src/utils/assets/index.ts +++ b/apps/wallet/src/utils/assets/index.ts @@ -79,6 +79,17 @@ const assets: Asset = { cardBackground: require(''), }, }, + samo: { + storeMeta: { + iconUri: require('assets/img/explore/samo-icon.png'), + coverUri: require('assets/img/explore/samo-cover.png'), + }, + widgetMeta: { + cardIcon: require('assets/img/widget/samo-icon.png'), + cardMark: require(''), + cardBackground: require(''), + }, + }, }, setting: { solana: { diff --git a/apps/wallet/src/utils/assets/index.web.ts b/apps/wallet/src/utils/assets/index.web.ts index 917aadfa4..192741c81 100644 --- a/apps/wallet/src/utils/assets/index.web.ts +++ b/apps/wallet/src/utils/assets/index.web.ts @@ -79,6 +79,17 @@ const assets: Asset = { cardBackground: { uri: '' }, }, }, + samo: { + storeMeta: { + iconUri: { uri: '/img/explore/samo-icon.png' }, + coverUri: { uri: '/img/explore/samo-cover.png' }, + }, + widgetMeta: { + cardIcon: { uri: '/img/widget/samo-icon.png' }, + cardMark: { uri: '' }, + cardBackground: { uri: '' }, + }, + }, }, setting: { solana: { icon: { uri: '/img/send-token/icon-solana.png' } }, diff --git a/packages/core/utils/widget.ts b/packages/core/utils/widget.ts index 78e8c6e38..cc9b4823a 100644 --- a/packages/core/utils/widget.ts +++ b/packages/core/utils/widget.ts @@ -26,9 +26,10 @@ export interface WidgetNetworkMetadata { export enum WidgetType { NETWORK = 'Network', GAME = 'Game', - DEFI = 'DeFi', - NFT = 'NFT', COMMUNITY = 'Community', + // We have no widgets for the below types of widget + // DEFI = 'DeFi', + // NFT = 'NFT', } export interface CustomWalletAdvertisement { @@ -47,7 +48,7 @@ export interface CustomWalletMetadata { buy: string; swap: string; }; - activeTabStyle: TabContainerStyle; + activeTabStyle?: TabContainerStyle; advertisements: CustomWalletAdvertisement[]; tokens?: TokenDocument[]; nfts?: NftDocument[]; diff --git a/packages/gui/components/SliderTabs/TabItem.tsx b/packages/gui/components/SliderTabs/TabItem.tsx index 0d0553556..d3bac7f5a 100644 --- a/packages/gui/components/SliderTabs/TabItem.tsx +++ b/packages/gui/components/SliderTabs/TabItem.tsx @@ -4,17 +4,57 @@ import { StyleSheet } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import { Hoverable, Text } from '@walless/gui'; +export interface GradientDirection { + start: { x: number; y: number }; + end: { x: number; y: number }; +} + +export const gradientDirection = { + LeftToRight: { + start: { x: 0, y: 0 }, + end: { x: 1, y: 0 }, + }, + RightToLeft: { + start: { x: 1, y: 0 }, + end: { x: 0, y: 0 }, + }, + TopToBottom: { + start: { x: 0, y: 0 }, + end: { x: 0, y: 1 }, + }, + BottomToTop: { + start: { x: 0, y: 1 }, + end: { x: 0, y: 0 }, + }, + TopRightToBottomLeft: { + start: { x: 1, y: 0 }, + end: { x: 0, y: 1 }, + }, + TopLeftToBottomRight: { + start: { x: 0, y: 0 }, + end: { x: 1, y: 1 }, + }, + BottomLeftToTopRight: { + start: { x: 0, y: 1 }, + end: { x: 1, y: 0 }, + }, + BottomRightToTopLeft: { + start: { x: 1, y: 1 }, + end: { x: 0, y: 0 }, + }, +}; + export interface TabContainerStyle { - style: ViewStyle; + style?: ViewStyle; linearGradient?: { - isHorizontal: boolean; + direction: GradientDirection; colors: string[]; }; } export interface TabItemStyle { - containerStyle: TabContainerStyle; - textStyle: TextStyle; + containerStyle?: TabContainerStyle; + textStyle?: TextStyle; } export interface TabAble { @@ -29,19 +69,16 @@ interface Props { } export const TabItem: FC = ({ item, style, onPress }) => { - if (style?.containerStyle.linearGradient) { - const isHorizontal = style.containerStyle.linearGradient.isHorizontal; - + const containerStyle = style?.containerStyle?.style; + const linearGradientStyle = style?.containerStyle?.linearGradient; + if (linearGradientStyle) { return ( - onPress?.(item)}> + onPress?.(item)}> {item.title} @@ -51,7 +88,7 @@ export const TabItem: FC = ({ item, style, onPress }) => { return ( onPress?.(item)} > {item.title} @@ -82,8 +119,10 @@ export const deactivatedStyle: TabItemStyle = { export default TabItem; const styles = StyleSheet.create({ - container: { + hoverable: { flex: 1, + }, + container: { paddingVertical: 10, borderRadius: 8, }, From 01e30456f9299f42687a92f0d2ecce2a5935ce6b Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 29 Jul 2024 12:34:18 +0700 Subject: [PATCH 08/31] add more ads and ads indicator --- .../Advertisement/AdvertisementIndicator.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementIndicator.tsx diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementIndicator.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementIndicator.tsx new file mode 100644 index 000000000..ed67c3f81 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementIndicator.tsx @@ -0,0 +1,35 @@ +import type { FC } from 'react'; +import { StyleSheet } from 'react-native'; +import type { WithTimingConfig } from 'react-native-reanimated'; +import Animated, { + useAnimatedStyle, + withTiming, +} from 'react-native-reanimated'; + +interface Props { + index: number; + currentIndex: number; +} +const AdvertisementIndicator: FC = ({ currentIndex, index }) => { + const animatedStyle = useAnimatedStyle(() => { + const opacity = currentIndex === index ? 1 : 0.3; + const config: WithTimingConfig = { duration: 650 }; + + return { + opacity: withTiming(opacity, config), + }; + }, [currentIndex]); + + return ; +}; + +export default AdvertisementIndicator; + +const styles = StyleSheet.create({ + indicator: { + width: 28, + height: 4, + backgroundColor: '#ffffff', + borderRadius: 4, + }, +}); From 3191fa7c7f8354fae81e709f44e1dd4e8cc76a1e Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 29 Jul 2024 20:48:40 +0700 Subject: [PATCH 09/31] add ads indicator to the ads --- .../Advertisement/index.tsx | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx index 73260171a..707b6e40b 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx @@ -7,6 +7,7 @@ import { useSharedValue } from 'react-native-reanimated'; import type { CustomWalletAdvertisement } from '@walless/core'; import { View } from '@walless/gui'; +import AdvertisementIndicator from './AdvertisementIndicator'; import AdvertisementItem from './AdvertisementItem'; interface Props { @@ -44,22 +45,33 @@ const Advertisement: FC = ({ ads }) => { }); return ( - - - {ads.map((item, index) => { - return ( - - ); - })} + + + + {ads.map((item, index) => { + return ( + + ); + })} + + + + {ads.map((_, index) => ( + + ))} - + ); }; @@ -67,9 +79,17 @@ export default Advertisement; const styles = StyleSheet.create({ container: { + gap: 8, + paddingHorizontal: 20, + }, + itemsContainer: { flexDirection: 'row', - minHeight: 200, + minHeight: 164, minWidth: 200, - paddingHorizontal: 20, + }, + indicatorContainer: { + flexDirection: 'row', + gap: 4, + alignSelf: 'center', }, }); From db08ef915b866161e98a124db865679901369ad6 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 29 Jul 2024 20:51:02 +0700 Subject: [PATCH 10/31] filter out the custom wallet when users have no required tokens --- .../Explorer/Widgets/CategoryButtons.tsx | 36 +++++++++++-- apps/wallet/src/utils/widget.ts | 53 +++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 apps/wallet/src/utils/widget.ts diff --git a/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx b/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx index 2dd18017a..450d5bd3c 100644 --- a/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx @@ -1,9 +1,12 @@ import type { FC } from 'react'; import { Animated, StyleSheet } from 'react-native'; import { useSharedValue, withTiming } from 'react-native-reanimated'; -import { WidgetType } from '@walless/core'; +import type { CustomWalletMetadata } from '@walless/core'; +import { WidgetCategory, WidgetType } from '@walless/core'; import type { WidgetDocument } from '@walless/store'; import { mockWidgets } from 'state/widget'; +import { useNfts, useTokens } from 'utils/hooks'; +import { filterAssetsFromCustomWalletTokens } from 'utils/widget'; import CategoryButton from './CategoryButton'; @@ -15,15 +18,40 @@ const CategoryButtons: FC = ({ setWidgets }) => { const currentIndex = useSharedValue(0); const animatedValue = useSharedValue(0); const categories = Object.values(WidgetType); + const { tokens } = useTokens(); + const { nfts } = useNfts(); const inputRange = categories.map((_, index) => index); const handleCategoryPress = (activeIndex: number, category: WidgetType) => { currentIndex.value = activeIndex; animatedValue.value = withTiming(activeIndex); - const filteredLayoutCards = mockWidgets.filter( - (item) => item.widgetType === category, - ); + const filteredLayoutCards = mockWidgets.filter((item) => { + if (item.category !== WidgetCategory.CUSTOM_WALLET) + return item.widgetType === category; + + const requiredTokens = (item?.customMetadata as CustomWalletMetadata) + .tokens; + const filteredTokens = filterAssetsFromCustomWalletTokens( + requiredTokens || [], + { + ownedTokens: tokens, + }, + ); + + const requiredNfts = (item?.customMetadata as CustomWalletMetadata).nfts; + const filteredNfts = filterAssetsFromCustomWalletTokens( + requiredNfts || [], + { + ownedNfts: nfts, + }, + ); + + return ( + item.widgetType === category && + (filteredTokens.length !== 0 || filteredNfts.length !== 0) + ); + }); setWidgets(filteredLayoutCards); }; diff --git a/apps/wallet/src/utils/widget.ts b/apps/wallet/src/utils/widget.ts new file mode 100644 index 000000000..86cc97f5f --- /dev/null +++ b/apps/wallet/src/utils/widget.ts @@ -0,0 +1,53 @@ +import type { + SolanaCollectible, + SolanaToken, + SuiNft, + SuiToken, +} from '@walless/core'; +import type { CustomWalletAssets, Nft, Token } from '@walless/core'; +import { Networks } from '@walless/core'; +import type { NftDocument, TokenDocument } from '@walless/store'; + +export const filterAssetsFromCustomWalletTokens = ( + requiredAssets: CustomWalletAssets[], + { + ownedTokens, + ownedNfts, + }: { + ownedTokens?: TokenDocument[]; + ownedNfts?: NftDocument[]; + }, +) => { + if (ownedNfts) { + return ownedNfts.filter( + (nft) => + requiredAssets?.some((ele) => { + let id = ''; + if (nft.network === Networks.solana) { + id = (nft as NftDocument).mint; + } else if (nft.network === Networks.sui) { + id = (nft as NftDocument).objectId; + } + + return ele.mintAddress === id; + }), + ); + } + + if (ownedTokens) { + return ownedTokens.filter( + (token) => + requiredAssets?.some((ele) => { + let id = ''; + if (token.network === Networks.solana) { + id = (token as TokenDocument).mint; + } else if (token.network === Networks.sui) { + id = (token as TokenDocument).coinObjectIds[0]; + } + return ele.mintAddress === id; + }), + ); + } + + return []; +}; From e967e3f91a02d8d826a89eb90d099f67b6216d42 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 29 Jul 2024 20:51:40 +0700 Subject: [PATCH 11/31] fix some small details such as types, size, etc. --- .../Widget/CustomWalletLayout/NftTab.tsx | 17 ++++---- .../Widget/CustomWalletLayout/index.tsx | 41 +++++++++++++++---- .../Explorer/WidgetNavigator/NavigatorOrb.tsx | 8 ++-- apps/wallet/src/state/widget/shared.ts | 27 +++++++++++- packages/core/utils/widget.ts | 11 +++-- 5 files changed, 79 insertions(+), 25 deletions(-) diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx index 7db258f4a..ca9d5d2a4 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx @@ -1,15 +1,15 @@ import type { FC } from 'react'; import { ScrollView, StyleSheet } from 'react-native'; -import type { Networks, Nft } from '@walless/core'; +import type { CustomWalletAssets, Networks } from '@walless/core'; import { Text, View } from '@walless/gui'; -import type { NftDocument } from '@walless/store'; import CollectionCard from 'components/CollectionCard'; import { useLazyGridLayout, useNfts } from 'utils/hooks'; import { navigate } from 'utils/navigation'; +import { filterAssetsFromCustomWalletTokens } from 'utils/widget'; interface Props { network: Networks; - requiredNfts?: NftDocument[]; + requiredNfts?: CustomWalletAssets[]; } export const NftTab: FC = ({ network, requiredNfts }) => { @@ -29,9 +29,9 @@ export const NftTab: FC = ({ network, requiredNfts }) => { }); }; - const filteredNfts = nfts.filter( - (item) => requiredNfts?.some((ele) => ele._id === item._id), - ); + const filteredNfts = filterAssetsFromCustomWalletTokens(requiredNfts || [], { + ownedNfts: nfts, + }); return ( = ({ network, requiredNfts }) => { )} {width > 0 && - nfts && - nfts.map((ele, index) => { + filteredNfts && + filteredNfts.map((ele, index) => { const collectibleId = ele._id.split('/')[2]; return ( = ({ id }) => { const customWalletWidget = mockWidgets.find((item) => item._id === id); const [activeTabIndex, setActiveTabIndex] = useState(0); + const [headerLayout, setHeaderLayout] = useState(); const customWalletMetadata = customWalletWidget?.customMetadata as CustomWalletMetadata; const network = customWalletMetadata.network; - const tokens = customWalletMetadata.tokens; + const requiredTokens = customWalletMetadata.tokens; const requiredNfts = customWalletMetadata.nfts; const keys = usePublicKeys(network); - const [headerLayout, setHeaderLayout] = useState(); - const valuation = tokens?.reduce( + const { tokens: ownedTokens } = useTokens(network); + const filteredTokens = filterAssetsFromCustomWalletTokens( + requiredTokens || [], + { + ownedTokens, + }, + ); + + const valuation = (filteredTokens as TokenDocument[])?.reduce( (accumulator, token) => accumulator + getTokenValue(token, 'usd'), 0, ); @@ -77,7 +96,12 @@ export const CustomWalletLayout: FC = ({ id }) => { return [ { id: 'tokens', - component: () => , + component: () => ( + []} + /> + ), }, { id: 'collectibles', @@ -174,7 +198,9 @@ export const CustomWalletLayout: FC = ({ id }) => { activeItem={bottomSliderItems[activeTabIndex]} /> - + {activeTabIndex === 0 && ( + + )} ); }; @@ -195,6 +221,7 @@ const styles = StyleSheet.create({ }, sliderContainer: { flex: 1, + minHeight: 200, overflow: 'hidden', }, }); diff --git a/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx b/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx index 9225f139f..157a82a9e 100644 --- a/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx +++ b/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx @@ -36,7 +36,7 @@ export const NavigatorOrb: FC = ({ }) => { const containerRef = useRef(null); const iconColor = getIconColor(isActive, item.storeMeta); - const iconSize = item.storeMeta?.iconSize || 20; + const iconSize = item.storeMeta?.iconSize || 40; const offset = useSharedValue(0); const radius = useSharedValue(isActive ? 1000 : 15); const hoverBarStyle = useAnimatedStyle(() => { @@ -52,7 +52,7 @@ export const NavigatorOrb: FC = ({ const orbStyle = useAnimatedStyle(() => { return { // temporarily use transparent without migration for pixeverse widget - backgroundColor: item._id === 'pixeverse' ? 'transparent' : iconColor, + backgroundColor: iconColor, borderRadius: withTiming(radius.value, { duration: 320, easing: Easing.bezier(0.51, 0.58, 0.23, 0.99), @@ -61,8 +61,8 @@ export const NavigatorOrb: FC = ({ }, [isActive]); const iconImgStyle = { - width: iconSize, - height: iconSize, + width: iconColor !== 'transparent' ? iconSize : 40, + height: iconColor !== 'transparent' ? iconSize : 40, }; const handleHoverIn = () => { diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index c01d73f76..868288f16 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -227,7 +227,6 @@ export const mockWidgets: WidgetDocument[] = [ storeMeta: { iconUri: '/img/widget/samo-icon.png', iconSize: 26, - iconColor: '#ffffff', coverUri: '/img/widget/samo-cover.png', description: 'dApp version of the T-rex Runner you already known!', loveCount: 46, @@ -255,8 +254,32 @@ export const mockWidgets: WidgetDocument[] = [ link: '', image: '/img/widget/samo-ad-1.png', }, + { + title: 'Get your SAMO debit card', + link: '', + image: '/img/widget/samo-ad-1.png', + }, + { + title: 'Get your SAMO debit card', + link: '', + image: '/img/widget/samo-ad-1.png', + }, + { + title: 'Get your SAMO debit card', + link: '', + image: '/img/widget/samo-ad-1.png', + }, + ], + tokens: [ + { + mintAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', + amount: 0.1, + }, + { + mintAddress: '11111111111111111111111111111111', + amount: 1, + }, ], - tokens: [], nfts: [], network: Networks.solana, }, diff --git a/packages/core/utils/widget.ts b/packages/core/utils/widget.ts index cc9b4823a..844673116 100644 --- a/packages/core/utils/widget.ts +++ b/packages/core/utils/widget.ts @@ -1,6 +1,4 @@ -import type { Nft, Token } from '@walless/core'; import type { TabContainerStyle } from '@walless/gui'; -import type { NftDocument, TokenDocument } from '@walless/store'; import type { Networks } from './common'; @@ -38,6 +36,11 @@ export interface CustomWalletAdvertisement { image: string; } +export interface CustomWalletAssets { + mintAddress: string; + amount?: number; +} + export interface CustomWalletMetadata { coverBanner: string; iconSrc: string; @@ -50,8 +53,8 @@ export interface CustomWalletMetadata { }; activeTabStyle?: TabContainerStyle; advertisements: CustomWalletAdvertisement[]; - tokens?: TokenDocument[]; - nfts?: NftDocument[]; + tokens?: CustomWalletAssets[]; + nfts?: CustomWalletAssets[]; network: Networks; } From c9d2f38a7cf98f8d84ec48c09da309e593e0b8f7 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Tue, 30 Jul 2024 11:17:56 +0700 Subject: [PATCH 12/31] hndle case wrapped sol is inserted --- apps/wallet/src/state/widget/shared.ts | 3 ++- apps/wallet/src/utils/widget.ts | 14 +++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index 868288f16..b3a189b3b 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -1,6 +1,7 @@ import { Networks, WidgetCategory, WidgetType } from '@walless/core'; import { gradientDirection } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; +import { wrappedSolMint } from 'utils/constants'; // TODO: this mocked data is for web only export const mockWidgets: WidgetDocument[] = [ @@ -276,7 +277,7 @@ export const mockWidgets: WidgetDocument[] = [ amount: 0.1, }, { - mintAddress: '11111111111111111111111111111111', + mintAddress: wrappedSolMint, amount: 1, }, ], diff --git a/apps/wallet/src/utils/widget.ts b/apps/wallet/src/utils/widget.ts index 86cc97f5f..a3a9c4811 100644 --- a/apps/wallet/src/utils/widget.ts +++ b/apps/wallet/src/utils/widget.ts @@ -8,6 +8,8 @@ import type { CustomWalletAssets, Nft, Token } from '@walless/core'; import { Networks } from '@walless/core'; import type { NftDocument, TokenDocument } from '@walless/store'; +import { solMint, wrappedSolMint } from './constants'; + export const filterAssetsFromCustomWalletTokens = ( requiredAssets: CustomWalletAssets[], { @@ -40,7 +42,9 @@ export const filterAssetsFromCustomWalletTokens = ( requiredAssets?.some((ele) => { let id = ''; if (token.network === Networks.solana) { - id = (token as TokenDocument).mint; + id = getSolanaMintAddress( + (token as TokenDocument).mint, + ); } else if (token.network === Networks.sui) { id = (token as TokenDocument).coinObjectIds[0]; } @@ -51,3 +55,11 @@ export const filterAssetsFromCustomWalletTokens = ( return []; }; + +const getSolanaMintAddress = (mint: string) => { + if (mint === wrappedSolMint) { + return solMint; + } + + return mint; +}; From 325fd4341ab21cf45d4d91aed59f03c8943d3ccf Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Tue, 30 Jul 2024 12:51:09 +0700 Subject: [PATCH 13/31] refactor: filter out the custom wallet meet the requirement --- .../features/Explorer/Highlights/index.tsx | 13 ++++-- .../Explorer/Widgets/CategoryButtons.tsx | 40 +++-------------- .../src/features/Explorer/Widgets/index.tsx | 14 ++++-- apps/wallet/src/features/Explorer/index.tsx | 44 ++++++++++++++++++- .../Widget/CustomWalletLayout/NftTab.tsx | 4 +- .../Widget/CustomWalletLayout/index.tsx | 4 +- apps/wallet/src/state/widget/shared.ts | 9 ++-- apps/wallet/src/utils/widget.ts | 38 ++++++++++------ 8 files changed, 101 insertions(+), 65 deletions(-) diff --git a/apps/wallet/src/features/Explorer/Highlights/index.tsx b/apps/wallet/src/features/Explorer/Highlights/index.tsx index f83ac4415..d8e08253b 100644 --- a/apps/wallet/src/features/Explorer/Highlights/index.tsx +++ b/apps/wallet/src/features/Explorer/Highlights/index.tsx @@ -1,12 +1,17 @@ +import type { FC } from 'react'; import { useState } from 'react'; import { StyleSheet } from 'react-native'; import { Text, View } from '@walless/gui'; -import { mockWidgets } from 'state/widget'; +import type { WidgetDocument } from '@walless/store'; import CardCarousel from './CardCarousel'; import HighlightIndicator from './HighlightIndicator'; -const Highlights = () => { +interface Props { + data: WidgetDocument[]; +} + +const Highlights: FC = ({ data }) => { const [currentIndex, setCurrentIndex] = useState(0); return ( @@ -18,14 +23,14 @@ const Highlights = () => { diff --git a/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx b/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx index 450d5bd3c..f08708cf6 100644 --- a/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx @@ -1,57 +1,29 @@ import type { FC } from 'react'; import { Animated, StyleSheet } from 'react-native'; import { useSharedValue, withTiming } from 'react-native-reanimated'; -import type { CustomWalletMetadata } from '@walless/core'; -import { WidgetCategory, WidgetType } from '@walless/core'; +import { WidgetType } from '@walless/core'; import type { WidgetDocument } from '@walless/store'; -import { mockWidgets } from 'state/widget'; -import { useNfts, useTokens } from 'utils/hooks'; -import { filterAssetsFromCustomWalletTokens } from 'utils/widget'; import CategoryButton from './CategoryButton'; interface CategoryButtonsProps { + widgets: WidgetDocument[]; setWidgets: (widgets: WidgetDocument[]) => void; } -const CategoryButtons: FC = ({ setWidgets }) => { +const CategoryButtons: FC = ({ widgets, setWidgets }) => { const currentIndex = useSharedValue(0); const animatedValue = useSharedValue(0); const categories = Object.values(WidgetType); - const { tokens } = useTokens(); - const { nfts } = useNfts(); const inputRange = categories.map((_, index) => index); const handleCategoryPress = (activeIndex: number, category: WidgetType) => { currentIndex.value = activeIndex; animatedValue.value = withTiming(activeIndex); - const filteredLayoutCards = mockWidgets.filter((item) => { - if (item.category !== WidgetCategory.CUSTOM_WALLET) - return item.widgetType === category; - - const requiredTokens = (item?.customMetadata as CustomWalletMetadata) - .tokens; - const filteredTokens = filterAssetsFromCustomWalletTokens( - requiredTokens || [], - { - ownedTokens: tokens, - }, - ); - - const requiredNfts = (item?.customMetadata as CustomWalletMetadata).nfts; - const filteredNfts = filterAssetsFromCustomWalletTokens( - requiredNfts || [], - { - ownedNfts: nfts, - }, - ); - - return ( - item.widgetType === category && - (filteredTokens.length !== 0 || filteredNfts.length !== 0) - ); - }); + const filteredLayoutCards = widgets.filter( + (item) => item.widgetType === category, + ); setWidgets(filteredLayoutCards); }; diff --git a/apps/wallet/src/features/Explorer/Widgets/index.tsx b/apps/wallet/src/features/Explorer/Widgets/index.tsx index 5cb9ee2eb..156927a38 100644 --- a/apps/wallet/src/features/Explorer/Widgets/index.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/index.tsx @@ -1,16 +1,20 @@ +import type { FC } from 'react'; import { useState } from 'react'; import { ScrollView, StyleSheet, View } from 'react-native'; import { WidgetType } from '@walless/core'; import { Text } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; -import { mockWidgets } from 'state/widget'; import CategoryButtons from './CategoryButtons'; import WidgetItem from './WidgetItem'; -const Widgets = () => { +interface Props { + data: WidgetDocument[]; +} + +const Widgets: FC = ({ data }) => { const [widgets, setWidgets] = useState( - mockWidgets.filter((item) => item.widgetType === WidgetType.NETWORK), + data.filter((item) => item.widgetType === WidgetType.NETWORK), ); return ( @@ -21,7 +25,9 @@ const Widgets = () => { Evolving your worlds filled with exciting events - + + + = ({ style }) => { + const { tokens } = useTokens(); + const { nfts } = useNfts(); + + const widgets = useMemo( + () => + mockWidgets.filter((item) => { + if (item.category === WidgetCategory.CUSTOM_WALLET) { + const requiredTokens = (item?.customMetadata as CustomWalletMetadata) + .tokens; + const filteredTokens = filterTokensWithAmountFromCustomWalletToken( + requiredTokens || [], + tokens, + ); + + const requiredNfts = (item?.customMetadata as CustomWalletMetadata) + .nfts; + const filteredNfts = filterAssetsFromCustomWalletAssets( + requiredNfts || [], + { + ownedNfts: nfts, + }, + ); + + return filteredTokens.length !== 0 || filteredNfts.length !== 0; + } + + return true; + }), + [tokens], + ); + return (

    - - + + ); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx index ca9d5d2a4..6bf69efaf 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx @@ -5,7 +5,7 @@ import { Text, View } from '@walless/gui'; import CollectionCard from 'components/CollectionCard'; import { useLazyGridLayout, useNfts } from 'utils/hooks'; import { navigate } from 'utils/navigation'; -import { filterAssetsFromCustomWalletTokens } from 'utils/widget'; +import { filterAssetsFromCustomWalletAssets } from 'utils/widget'; interface Props { network: Networks; @@ -29,7 +29,7 @@ export const NftTab: FC = ({ network, requiredNfts }) => { }); }; - const filteredNfts = filterAssetsFromCustomWalletTokens(requiredNfts || [], { + const filteredNfts = filterAssetsFromCustomWalletAssets(requiredNfts || [], { ownedNfts: nfts, }); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx index 323068f35..2b3304077 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -25,7 +25,7 @@ import { useTokens, } from 'utils/hooks'; import { copy } from 'utils/system'; -import { filterAssetsFromCustomWalletTokens } from 'utils/widget'; +import { filterAssetsFromCustomWalletAssets } from 'utils/widget'; import ActivityTab from '../BuiltInNetwork/ActivityTab'; import TokenTab from '../BuiltInNetwork/TokenTab'; @@ -71,7 +71,7 @@ export const CustomWalletLayout: FC = ({ id }) => { const keys = usePublicKeys(network); const { tokens: ownedTokens } = useTokens(network); - const filteredTokens = filterAssetsFromCustomWalletTokens( + const filteredTokens = filterAssetsFromCustomWalletAssets( requiredTokens || [], { ownedTokens, diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index b3a189b3b..c72e24189 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -274,14 +274,17 @@ export const mockWidgets: WidgetDocument[] = [ tokens: [ { mintAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', - amount: 0.1, + amount: 50000, }, { mintAddress: wrappedSolMint, - amount: 1, }, ], - nfts: [], + nfts: [ + { + mintAddress: '9dKaqke7EUJDGBF6614oowCbA3aSZw5dHLRaNpWGoWDz', + }, + ], network: Networks.solana, }, }, diff --git a/apps/wallet/src/utils/widget.ts b/apps/wallet/src/utils/widget.ts index a3a9c4811..a2bda042a 100644 --- a/apps/wallet/src/utils/widget.ts +++ b/apps/wallet/src/utils/widget.ts @@ -1,16 +1,11 @@ -import type { - SolanaCollectible, - SolanaToken, - SuiNft, - SuiToken, -} from '@walless/core'; +import type { SolanaToken, SuiToken } from '@walless/core'; import type { CustomWalletAssets, Nft, Token } from '@walless/core'; import { Networks } from '@walless/core'; import type { NftDocument, TokenDocument } from '@walless/store'; import { solMint, wrappedSolMint } from './constants'; -export const filterAssetsFromCustomWalletTokens = ( +export const filterAssetsFromCustomWalletAssets = ( requiredAssets: CustomWalletAssets[], { ownedTokens, @@ -24,13 +19,7 @@ export const filterAssetsFromCustomWalletTokens = ( return ownedNfts.filter( (nft) => requiredAssets?.some((ele) => { - let id = ''; - if (nft.network === Networks.solana) { - id = (nft as NftDocument).mint; - } else if (nft.network === Networks.sui) { - id = (nft as NftDocument).objectId; - } - + const id = nft.collectionId || ''; return ele.mintAddress === id; }), ); @@ -56,6 +45,27 @@ export const filterAssetsFromCustomWalletTokens = ( return []; }; +export const filterTokensWithAmountFromCustomWalletToken = ( + requiredAssets: CustomWalletAssets[], + ownedTokens: TokenDocument[], +) => { + return ownedTokens.filter( + (token) => + requiredAssets?.some((ele) => { + let id = ''; + if (token.network === Networks.solana) { + id = getSolanaMintAddress((token as TokenDocument).mint); + } else if (token.network === Networks.sui) { + id = (token as TokenDocument).coinObjectIds[0]; + } + + return ( + ele.mintAddress === id && ele.amount && ele.amount <= token.balance + ); + }), + ); +}; + const getSolanaMintAddress = (mint: string) => { if (mint === wrappedSolMint) { return solMint; From 871057967f93ac62aaa0f12d7d5a987e8c3b0964 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Tue, 30 Jul 2024 12:53:52 +0700 Subject: [PATCH 14/31] fix: type check error --- .../features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx index 69c81b4ad..f891a12f4 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx @@ -75,7 +75,7 @@ const AptosTokensTab: FC = ({ network }) => { const activatedStyle: TabItemStyle = { containerStyle: { - backgroundColor: '#0694D3', + style: { backgroundColor: '#0694D3' }, }, textStyle: { color: 'white', @@ -85,7 +85,7 @@ const AptosTokensTab: FC = ({ network }) => { const deactivatedStyle: TabItemStyle = { containerStyle: { - backgroundColor: 'transparent', + style: { backgroundColor: 'transparent' }, }, textStyle: { color: '#566674', From 23c0c9d9a816ed74f8a7fd4137433c3f4c3cda79 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Tue, 30 Jul 2024 15:43:57 +0700 Subject: [PATCH 15/31] fix: get wrong collection id --- apps/wallet/src/state/widget/shared.ts | 3 ++- apps/wallet/src/utils/widget.ts | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index c72e24189..3ec02a289 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -282,7 +282,8 @@ export const mockWidgets: WidgetDocument[] = [ ], nfts: [ { - mintAddress: '9dKaqke7EUJDGBF6614oowCbA3aSZw5dHLRaNpWGoWDz', + mintAddress: + '98fe506a37c46d67b7212ec689decd6fcd7137ea751fb88d9c7fe89c60c5215f', }, ], network: Networks.solana, diff --git a/apps/wallet/src/utils/widget.ts b/apps/wallet/src/utils/widget.ts index a2bda042a..c9e34802b 100644 --- a/apps/wallet/src/utils/widget.ts +++ b/apps/wallet/src/utils/widget.ts @@ -19,7 +19,9 @@ export const filterAssetsFromCustomWalletAssets = ( return ownedNfts.filter( (nft) => requiredAssets?.some((ele) => { - const id = nft.collectionId || ''; + const splittedStrings = nft.collectionId?.split('/') || []; + const id = splittedStrings[2] || ''; + return ele.mintAddress === id; }), ); @@ -67,8 +69,8 @@ export const filterTokensWithAmountFromCustomWalletToken = ( }; const getSolanaMintAddress = (mint: string) => { - if (mint === wrappedSolMint) { - return solMint; + if (mint === solMint) { + return wrappedSolMint; } return mint; From 8289459cb365ff4e8086c501db8e0c9cc1df8283 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 5 Aug 2024 12:38:45 +0700 Subject: [PATCH 16/31] replace nft tab by collectible list --- .../CollectibleList.tsx} | 62 +++++++++++++------ 1 file changed, 43 insertions(+), 19 deletions(-) rename apps/wallet/src/features/Widget/{CustomWalletLayout/NftTab.tsx => BuiltInNetwork/CollectibleList.tsx} (52%) diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/CollectibleList.tsx similarity index 52% rename from apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx rename to apps/wallet/src/features/Widget/BuiltInNetwork/CollectibleList.tsx index 6bf69efaf..6ae63e6e5 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/NftTab.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/CollectibleList.tsx @@ -1,37 +1,49 @@ import type { FC } from 'react'; import { ScrollView, StyleSheet } from 'react-native'; -import type { CustomWalletAssets, Networks } from '@walless/core'; import { Text, View } from '@walless/gui'; +import type { NftDocument } from '@walless/store'; import CollectionCard from 'components/CollectionCard'; -import { useLazyGridLayout, useNfts } from 'utils/hooks'; +import type { WrappedCollection } from 'utils/hooks'; +import { useLazyGridLayout } from 'utils/hooks'; import { navigate } from 'utils/navigation'; -import { filterAssetsFromCustomWalletAssets } from 'utils/widget'; interface Props { - network: Networks; - requiredNfts?: CustomWalletAssets[]; + collections?: WrappedCollection[]; + nfts?: NftDocument[]; } -export const NftTab: FC = ({ network, requiredNfts }) => { - const { nfts } = useNfts(network); +export const CollectibleList: FC = ({ collections = [], nfts = [] }) => { const { onGridContainerLayout, width } = useLazyGridLayout({ referenceWidth: 150, gap: gridGap, }); - const handleNavigateToCollectible = (id: string) => { + const handlePressCollection = (ele: WrappedCollection) => { + const collectionId = ele._id.split('/')[2]; + navigate('Dashboard', { screen: 'Explore', params: { screen: 'Collection', - params: { screen: 'NFT', params: { id } }, + params: { + screen: 'Default', + params: { id: collectionId }, + }, }, }); }; - const filteredNfts = filterAssetsFromCustomWalletAssets(requiredNfts || [], { - ownedNfts: nfts, - }); + const handlePressCollectible = (ele: NftDocument) => { + const collectibleId = ele._id.split('/')[2]; + + navigate('Dashboard', { + screen: 'Explore', + params: { + screen: 'Collection', + params: { screen: 'NFT', params: { id: collectibleId } }, + }, + }); + }; return ( = ({ network, requiredNfts }) => { showsVerticalScrollIndicator={false} onLayout={(e) => onGridContainerLayout(e.nativeEvent.layout)} > - {filteredNfts.length === 0 && ( + {collections.length === 0 && nfts.length === 0 && ( You do not have any NFT yet )} {width > 0 && - filteredNfts && - filteredNfts.map((ele, index) => { - const collectibleId = ele._id.split('/')[2]; + collections.map((ele, index) => { + return ( + handlePressCollection(ele)} + size={width} + /> + ); + })} + + + + {width > 0 && + nfts.map((ele, index) => { return ( handleNavigateToCollectible(collectibleId)} + onPress={() => handlePressCollectible(ele)} size={width} /> ); @@ -63,7 +88,7 @@ export const NftTab: FC = ({ network, requiredNfts }) => { ); }; -export default NftTab; +export default CollectibleList; const gridGap = 18; const styles = StyleSheet.create({ @@ -72,7 +97,6 @@ const styles = StyleSheet.create({ marginBottom: 32, borderRadius: 12, overflow: 'hidden', - minHeight: 300, }, contentContainer: { flexDirection: 'row', From b43a2b282a117d0eb07a45a86f69b308c45c7ced Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 5 Aug 2024 12:40:35 +0700 Subject: [PATCH 17/31] remove widget type, group it to widget category --- .../features/Explorer/Highlights/index.tsx | 8 +-- .../Explorer/Widgets/CategoryButtons.tsx | 20 ++++-- apps/wallet/src/state/widget/shared.ts | 67 ++++++++++--------- packages/core/utils/widget.ts | 20 ++---- 4 files changed, 59 insertions(+), 56 deletions(-) diff --git a/apps/wallet/src/features/Explorer/Highlights/index.tsx b/apps/wallet/src/features/Explorer/Highlights/index.tsx index d8e08253b..b804e24df 100644 --- a/apps/wallet/src/features/Explorer/Highlights/index.tsx +++ b/apps/wallet/src/features/Explorer/Highlights/index.tsx @@ -8,10 +8,10 @@ import CardCarousel from './CardCarousel'; import HighlightIndicator from './HighlightIndicator'; interface Props { - data: WidgetDocument[]; + widgets: WidgetDocument[]; } -const Highlights: FC = ({ data }) => { +const Highlights: FC = ({ widgets }) => { const [currentIndex, setCurrentIndex] = useState(0); return ( @@ -23,14 +23,14 @@ const Highlights: FC = ({ data }) => { diff --git a/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx b/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx index f08708cf6..d8bcf93f1 100644 --- a/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx @@ -1,7 +1,8 @@ import type { FC } from 'react'; import { Animated, StyleSheet } from 'react-native'; import { useSharedValue, withTiming } from 'react-native-reanimated'; -import { WidgetType } from '@walless/core'; +import type { WidgetCategory } from '@walless/core'; +import { categories, communityList } from '@walless/core'; import type { WidgetDocument } from '@walless/store'; import CategoryButton from './CategoryButton'; @@ -14,16 +15,23 @@ interface CategoryButtonsProps { const CategoryButtons: FC = ({ widgets, setWidgets }) => { const currentIndex = useSharedValue(0); const animatedValue = useSharedValue(0); - const categories = Object.values(WidgetType); const inputRange = categories.map((_, index) => index); - const handleCategoryPress = (activeIndex: number, category: WidgetType) => { + const handleCategoryPress = ( + activeIndex: number, + category: WidgetCategory | string, + ) => { currentIndex.value = activeIndex; animatedValue.value = withTiming(activeIndex); - const filteredLayoutCards = widgets.filter( - (item) => item.widgetType === category, - ); + + const filteredLayoutCards = widgets.filter((widget) => { + if (category === 'Community') { + return communityList.includes(widget.category); + } + + return widget.category === category; + }); setWidgets(filteredLayoutCards); }; diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index 3ec02a289..799b3dd35 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -1,7 +1,7 @@ -import { Networks, WidgetCategory, WidgetType } from '@walless/core'; +import type { CustomWalletAssets } from '@walless/core'; +import { Networks, WidgetCategory } from '@walless/core'; import { gradientDirection } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; -import { wrappedSolMint } from 'utils/constants'; // TODO: this mocked data is for web only export const mockWidgets: WidgetDocument[] = [ @@ -11,7 +11,6 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.solana], version: '0.1.8', type: 'Widget', - widgetType: WidgetType.GAME, category: WidgetCategory.GAME, timestamp: new Date().toISOString(), storeMeta: { @@ -23,7 +22,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - customMetadata: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/explore/logo-pixeverse.png', iconUri: '/img/explore/logo-pixeverse.png', @@ -37,7 +36,6 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.solana], version: '0.9.1', type: 'Widget', - widgetType: WidgetType.NETWORK, category: WidgetCategory.NETWORK, timestamp: new Date().toISOString(), storeMeta: { @@ -50,7 +48,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 90, activeCount: 502, }, - customMetadata: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/network/solana-icon-lg.png', iconUri: '/img/network/solana-icon-sm.svg', @@ -64,7 +62,6 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.sui], version: '0.0.1', type: 'Widget', - widgetType: WidgetType.NETWORK, category: WidgetCategory.NETWORK, timestamp: new Date().toISOString(), storeMeta: { @@ -77,7 +74,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 100, activeCount: 567, }, - customMetadata: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/network/sui-icon-lg.png', iconUri: '/img/network/sui-icon-sm.png', @@ -91,7 +88,6 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.sui], version: '0.0.1', type: 'Widget', - widgetType: WidgetType.NETWORK, category: WidgetCategory.NETWORK, timestamp: new Date().toISOString(), storeMeta: { @@ -104,7 +100,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 100, activeCount: 567, }, - customMetadata: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/network/tezos-icon-lg.png', iconUri: '/img/network/tezos-icon-sm.png', @@ -118,7 +114,6 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.aptos], version: '0.0.1', type: 'Widget', - widgetType: WidgetType.NETWORK, category: WidgetCategory.NETWORK, timestamp: new Date().toISOString(), storeMeta: { @@ -131,7 +126,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - customMetadata: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/explore/aptos-icon.svg', iconUri: '/img/explore/aptos-icon.svg', @@ -145,7 +140,6 @@ export const mockWidgets: WidgetDocument[] = [ networks: [], version: '0.1.8', type: 'Widget', - widgetType: WidgetType.GAME, category: WidgetCategory.GAME, timestamp: new Date().toISOString(), storeMeta: { @@ -157,7 +151,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - customMetadata: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/t-rex-runner/runner-icon.png', iconUri: '/img/t-rex-runner/runner-icon.png', @@ -171,7 +165,6 @@ export const mockWidgets: WidgetDocument[] = [ networks: [], version: '0.0.1', type: 'Widget', - widgetType: WidgetType.GAME, category: WidgetCategory.GAME, timestamp: new Date().toISOString(), storeMeta: { @@ -183,7 +176,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - customMetadata: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/sui-jump/suijump-icon.png', iconUri: '/img/sui-jump/suijump-icon.png', @@ -222,7 +215,6 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.solana], version: '0.0.1', type: 'Widget', - widgetType: WidgetType.COMMUNITY, category: WidgetCategory.CUSTOM_WALLET, timestamp: new Date().toISOString(), storeMeta: { @@ -233,7 +225,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - customMetadata: { + metadata: { coverBanner: '/img/widget/samo-banner.png', iconSrc: '/img/widget/samo-icon.png', backgroundColor: '#141121', @@ -271,21 +263,30 @@ export const mockWidgets: WidgetDocument[] = [ image: '/img/widget/samo-ad-1.png', }, ], - tokens: [ - { - mintAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', - amount: 50000, - }, - { - mintAddress: wrappedSolMint, - }, - ], - nfts: [ - { - mintAddress: - '98fe506a37c46d67b7212ec689decd6fcd7137ea751fb88d9c7fe89c60c5215f', - }, - ], + tokens: new Map([ + [ + '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', + { + mintAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', + amount: 50000, + }, + ], + [ + 'So11111111111111111111111111111111111111112', + { + mintAddress: 'So11111111111111111111111111111111111111112', + }, + ], + ]), + nfts: new Map([ + [ + '98fe506a37c46d67b7212ec689decd6fcd7137ea751fb88d9c7fe89c60c5215f', + { + mintAddress: + '98fe506a37c46d67b7212ec689decd6fcd7137ea751fb88d9c7fe89c60c5215f', + }, + ], + ]), network: Networks.solana, }, }, diff --git a/packages/core/utils/widget.ts b/packages/core/utils/widget.ts index 844673116..db0701a22 100644 --- a/packages/core/utils/widget.ts +++ b/packages/core/utils/widget.ts @@ -21,15 +21,6 @@ export interface WidgetNetworkMetadata { iconColor: string; } -export enum WidgetType { - NETWORK = 'Network', - GAME = 'Game', - COMMUNITY = 'Community', - // We have no widgets for the below types of widget - // DEFI = 'DeFi', - // NFT = 'NFT', -} - export interface CustomWalletAdvertisement { title: string; link: string; @@ -53,11 +44,13 @@ export interface CustomWalletMetadata { }; activeTabStyle?: TabContainerStyle; advertisements: CustomWalletAdvertisement[]; - tokens?: CustomWalletAssets[]; - nfts?: CustomWalletAssets[]; + tokens?: Map; + nfts?: Map; network: Networks; } +export const categories = ['Network', 'Game', 'Community']; + export type CustomMetadata = CustomWalletMetadata | WidgetNetworkMetadata; export enum WidgetCategory { @@ -66,13 +59,14 @@ export enum WidgetCategory { NETWORK = 'Network', } +export const communityList = [WidgetCategory.CUSTOM_WALLET]; + export interface Widget { name: string; networks: Networks[]; version: string; timestamp?: string; - widgetType: WidgetType; category: WidgetCategory; storeMeta: WidgetStoreOptions; - customMetadata?: CustomMetadata; + metadata?: CustomMetadata; } From 0ac6cb142777bcdaa3982d9c448f8bcf3ff2711d Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 5 Aug 2024 12:43:14 +0700 Subject: [PATCH 18/31] replace token tab by token list --- .../Widget/BuiltInNetwork/TokenTab.tsx | 32 ------------- .../features/Widget/BuiltInNetwork/index.tsx | 12 +++-- .../Widget/CustomWalletLayout/index.tsx | 45 ++++++++++--------- 3 files changed, 32 insertions(+), 57 deletions(-) delete mode 100644 apps/wallet/src/features/Widget/BuiltInNetwork/TokenTab.tsx diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenTab.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/TokenTab.tsx deleted file mode 100644 index fba60df19..000000000 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenTab.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { FC } from 'react'; -import { StyleSheet } from 'react-native'; -import type { Networks, Token } from '@walless/core'; -import type { TokenDocument } from '@walless/store'; -import { useTokens } from 'utils/hooks'; - -import TokenList from './TokenList'; - -interface Props { - network: Networks; - tokens?: TokenDocument[]; -} - -export const TokenTab: FC = ({ network, tokens }) => { - const { tokens: networkTokens } = useTokens(network); - - return ( - - ); -}; - -export default TokenTab; - -const styles = StyleSheet.create({ - tokenListContainer: { - marginVertical: 16, - overflow: 'hidden', - }, -}); diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx index 8f8a0adc2..44e18ec6e 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx @@ -24,7 +24,7 @@ import ActivityTab from './ActivityTab'; import AptosTokensTab from './AptosTokensTab'; import NftTab from './NftTab'; import { getWalletCardSkin, layoutTabs } from './shared'; -import TokenTab from './TokenTab'; +import TokenList from './TokenList'; import WalletCard from './WalletCard'; interface Props { @@ -36,7 +36,7 @@ export const BuiltInNetwork: FC = ({ id }) => { const [activeTabIndex, setActiveTabIndex] = useState(0); const keys = usePublicKeys(network); const [headerLayout, setHeaderLayout] = useState(); - const { valuation } = useTokens(network); + const { tokens, valuation } = useTokens(network); const cardSkin = useMemo(() => getWalletCardSkin(network), [network]); const opacityAnimated = useOpacityAnimated({ from: 0, to: 1 }); @@ -48,7 +48,9 @@ export const BuiltInNetwork: FC = ({ id }) => { return [ { id: 'tokens', - component: () => , + component: () => ( + + ), }, { id: 'collectibles', @@ -178,4 +180,8 @@ const styles = StyleSheet.create({ flex: 1, overflow: 'hidden', }, + tokenListContainer: { + marginVertical: 16, + overflow: 'hidden', + }, }); diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx index 2b3304077..56dbb6476 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -15,26 +15,25 @@ import type { import type { SlideOption } from '@walless/gui'; import { Slider, SliderTabs } from '@walless/gui'; import type { TabAble, TabItemStyle } from '@walless/gui/components/SliderTabs'; -import type { TokenDocument } from '@walless/store'; +import type { + NftDocument, + TokenDocument, + WidgetDocument, +} from '@walless/store'; import { showCopiedModal } from 'modals/Notification'; import { mockWidgets } from 'state/widget'; -import { - getTokenValue, - useOpacityAnimated, - usePublicKeys, - useTokens, -} from 'utils/hooks'; +import { getTokenValue, useOpacityAnimated, usePublicKeys } from 'utils/hooks'; import { copy } from 'utils/system'; -import { filterAssetsFromCustomWalletAssets } from 'utils/widget'; +import { filterByOwnedNfts, filterByOwnedTokens } from 'utils/widget'; import ActivityTab from '../BuiltInNetwork/ActivityTab'; -import TokenTab from '../BuiltInNetwork/TokenTab'; +import CollectibleList from '../BuiltInNetwork/CollectibleList'; +import TokenList from '../BuiltInNetwork/TokenList'; import type { CardSkin } from '../BuiltInNetwork/WalletCard'; import { WalletCard } from '../BuiltInNetwork/WalletCard'; import Advertisement from './Advertisement'; import FeatureButtons from './FeatureButtons'; -import NftTab from './NftTab'; import { layoutTabs } from './shared'; interface Props { @@ -63,19 +62,17 @@ export const CustomWalletLayout: FC = ({ id }) => { const [activeTabIndex, setActiveTabIndex] = useState(0); const [headerLayout, setHeaderLayout] = useState(); const customWalletMetadata = - customWalletWidget?.customMetadata as CustomWalletMetadata; + customWalletWidget?.metadata as CustomWalletMetadata; const network = customWalletMetadata.network; - const requiredTokens = customWalletMetadata.tokens; - const requiredNfts = customWalletMetadata.nfts; const keys = usePublicKeys(network); - const { tokens: ownedTokens } = useTokens(network); - const filteredTokens = filterAssetsFromCustomWalletAssets( - requiredTokens || [], - { - ownedTokens, - }, + const { filteredResult: filteredTokens } = filterByOwnedTokens( + customWalletWidget as WidgetDocument, + ); + + const { filteredResult: filteredNfts } = filterByOwnedNfts( + customWalletWidget as WidgetDocument, ); const valuation = (filteredTokens as TokenDocument[])?.reduce( @@ -97,16 +94,16 @@ export const CustomWalletLayout: FC = ({ id }) => { { id: 'tokens', component: () => ( - []} + style={styles.tokenListContainer} /> ), }, { id: 'collectibles', component: () => ( - + ), }, { @@ -224,4 +221,8 @@ const styles = StyleSheet.create({ minHeight: 200, overflow: 'hidden', }, + tokenListContainer: { + marginVertical: 16, + overflow: 'hidden', + }, }); From ccb8b2abf2f04f3d7f16e3f1c9ac7f524668f2d9 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 5 Aug 2024 12:43:51 +0700 Subject: [PATCH 19/31] abstract filter for widget --- apps/wallet/src/features/Explorer/index.tsx | 36 +--- .../features/Swap/Select/SelectFromToken.tsx | 2 +- .../Widget/BuiltInNetwork/TokenList/index.tsx | 8 +- apps/wallet/src/utils/widget.ts | 197 ++++++++++++------ 4 files changed, 151 insertions(+), 92 deletions(-) diff --git a/apps/wallet/src/features/Explorer/index.tsx b/apps/wallet/src/features/Explorer/index.tsx index 4f08da9c3..958cf114d 100644 --- a/apps/wallet/src/features/Explorer/index.tsx +++ b/apps/wallet/src/features/Explorer/index.tsx @@ -2,16 +2,11 @@ import type { FC } from 'react'; import { useMemo } from 'react'; import type { StyleProp, ViewStyle } from 'react-native'; import { ScrollView, StyleSheet } from 'react-native'; -import type { CustomWalletMetadata } from '@walless/core'; -import { WidgetCategory } from '@walless/core'; import { View } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; import { mockWidgets } from 'state/widget'; import { useNfts, useTokens } from 'utils/hooks'; -import { - filterAssetsFromCustomWalletAssets, - filterTokensWithAmountFromCustomWalletToken, -} from 'utils/widget'; +import { filterMap } from 'utils/widget'; import Header from './Header'; import Highlights from './Highlights'; @@ -32,30 +27,15 @@ export const ExplorerFeature: FC = ({ style }) => { const widgets = useMemo( () => - mockWidgets.filter((item) => { - if (item.category === WidgetCategory.CUSTOM_WALLET) { - const requiredTokens = (item?.customMetadata as CustomWalletMetadata) - .tokens; - const filteredTokens = filterTokensWithAmountFromCustomWalletToken( - requiredTokens || [], - tokens, - ); - - const requiredNfts = (item?.customMetadata as CustomWalletMetadata) - .nfts; - const filteredNfts = filterAssetsFromCustomWalletAssets( - requiredNfts || [], - { - ownedNfts: nfts, - }, - ); - - return filteredTokens.length !== 0 || filteredNfts.length !== 0; + mockWidgets.filter((widget) => { + if (filterMap.has(widget._id)) { + const filters = filterMap.get(widget._id); + return filters?.some((filter) => filter(widget).hasItems); } return true; }), - [tokens], + [tokens, nfts], ); return ( @@ -64,8 +44,8 @@ export const ExplorerFeature: FC = ({ style }) => { - - + + ); diff --git a/apps/wallet/src/features/Swap/Select/SelectFromToken.tsx b/apps/wallet/src/features/Swap/Select/SelectFromToken.tsx index 563bafdc3..c69ce0be9 100644 --- a/apps/wallet/src/features/Swap/Select/SelectFromToken.tsx +++ b/apps/wallet/src/features/Swap/Select/SelectFromToken.tsx @@ -64,7 +64,7 @@ const SelectFromToken: FC = () => { diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/index.tsx index 37038bd3e..91d0a53f8 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/index.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/index.tsx @@ -13,7 +13,7 @@ interface Props { itemStyle?: StyleProp; separateStyle?: StyleProp; contentContainerStyle?: StyleProp; - items: TokenDocument[]; + tokens: TokenDocument[]; ListHeaderComponent?: ComponentType> | ReactElement; onPressItem?: (item: TokenDocument) => void; } @@ -23,7 +23,7 @@ export const TokenList = ({ itemStyle, separateStyle, contentContainerStyle, - items, + tokens, ListHeaderComponent, onPressItem, }: Props) => { @@ -39,7 +39,7 @@ export const TokenList = ({ style={[ itemStyle, index === 0 && styles.firstItem, - index === items.length - 1 && styles.lastItem, + index === tokens.length - 1 && styles.lastItem, ]} onPress={handlePressItem} /> @@ -52,7 +52,7 @@ export const TokenList = ({ showsVerticalScrollIndicator={false} style={style} contentContainerStyle={contentContainerStyle} - data={items} + data={tokens} renderItem={renderItem} keyExtractor={(item) => item._id} ItemSeparatorComponent={() => } diff --git a/apps/wallet/src/utils/widget.ts b/apps/wallet/src/utils/widget.ts index c9e34802b..3cd9100eb 100644 --- a/apps/wallet/src/utils/widget.ts +++ b/apps/wallet/src/utils/widget.ts @@ -1,71 +1,146 @@ -import type { SolanaToken, SuiToken } from '@walless/core'; -import type { CustomWalletAssets, Nft, Token } from '@walless/core'; +import type { + CustomWalletMetadata, + SolanaToken, + SuiToken, +} from '@walless/core'; import { Networks } from '@walless/core'; -import type { NftDocument, TokenDocument } from '@walless/store'; - -import { solMint, wrappedSolMint } from './constants'; - -export const filterAssetsFromCustomWalletAssets = ( - requiredAssets: CustomWalletAssets[], - { - ownedTokens, - ownedNfts, - }: { - ownedTokens?: TokenDocument[]; - ownedNfts?: NftDocument[]; - }, -) => { - if (ownedNfts) { - return ownedNfts.filter( - (nft) => - requiredAssets?.some((ele) => { - const splittedStrings = nft.collectionId?.split('/') || []; - const id = splittedStrings[2] || ''; - - return ele.mintAddress === id; - }), - ); - } +import type { TokenDocument, WidgetDocument } from '@walless/store'; +import { nftState, tokenState } from 'state/assets'; - if (ownedTokens) { - return ownedTokens.filter( - (token) => - requiredAssets?.some((ele) => { - let id = ''; - if (token.network === Networks.solana) { - id = getSolanaMintAddress( - (token as TokenDocument).mint, - ); - } else if (token.network === Networks.sui) { - id = (token as TokenDocument).coinObjectIds[0]; - } - return ele.mintAddress === id; - }), - ); +export type WidgetFilter = (widget: WidgetDocument) => { + filteredResult: unknown[]; + hasItems: boolean; +}; + +import { solMint, SUI_COIN_TYPE, wrappedSolMint } from './constants'; + +const getTokenAddress = (token: TokenDocument) => { + let id = ''; + if (token.network === Networks.solana) { + id = getSolanaMintAddress((token as TokenDocument).mint); + } else if (token.network === Networks.sui) { + id = (token as TokenDocument).coinObjectIds[0]; } - return []; + return id; }; -export const filterTokensWithAmountFromCustomWalletToken = ( - requiredAssets: CustomWalletAssets[], - ownedTokens: TokenDocument[], -) => { - return ownedTokens.filter( - (token) => - requiredAssets?.some((ele) => { - let id = ''; - if (token.network === Networks.solana) { - id = getSolanaMintAddress((token as TokenDocument).mint); - } else if (token.network === Networks.sui) { - id = (token as TokenDocument).coinObjectIds[0]; +const getOwnedTokens = (network?: Networks, address?: string) => { + const { map } = tokenState; + + const tokens = Array.from(map.values()).filter((token) => { + const isInNetwork = network ? token.network === network : true; + const isOwnedByAddress = address ? token.owner === address : true; + return isInNetwork && isOwnedByAddress; + }); + + switch (network) { + case Networks.solana: { + const filteredTokens = []; + for (const token of tokens as TokenDocument[]) { + const isNetworkValid = network ? token.network === network : true; + const isAvailable = token.amount !== '0'; + const isSol = token.mint === solMint; + + if (isNetworkValid && (isSol || isAvailable)) { + filteredTokens.push(token); } + } + + return filteredTokens; + } + case Networks.sui: { + const filteredTokens = []; + for (const token of tokens as TokenDocument[]) { + const isNetworkValid = network ? token.network === network : true; + const isAvailable = token.balance !== 0; + const isSUI = token.coinType === SUI_COIN_TYPE; + + if (isNetworkValid && (isSUI || isAvailable)) { + filteredTokens.push(token); + } + } + + return filteredTokens; + } + case Networks.tezos: { + return tokens; + } + case Networks.aptos: { + return tokens; + } + default: { + return tokens; + } + } +}; + +const getOwnedNfts = (network?: Networks, address?: string) => { + const { map } = nftState; + + const nfts = Array.from(map.values()).filter((nft) => { + const isInNetwork = network ? nft.network === network : true; + const isOwnedByAddress = address ? nft.owner === address : true; + const isAvailable = nft.amount > 0; - return ( - ele.mintAddress === id && ele.amount && ele.amount <= token.balance - ); - }), + return isInNetwork && isOwnedByAddress && isAvailable; + }); + + return nfts; +}; + +export const filterByOwnedTokens: WidgetFilter = (widget: WidgetDocument) => { + const ownedTokens = getOwnedTokens( + (widget.metadata as CustomWalletMetadata)?.network, + ); + const requiredTokens = (widget.metadata as CustomWalletMetadata)?.tokens; + const filteredTokens = ownedTokens.filter((ownedToken) => { + const id = getTokenAddress(ownedToken); + return requiredTokens?.has(id); + }); + + return { + filteredResult: filteredTokens, + hasItems: filteredTokens.length > 0, + }; +}; + +export const filterByTokenBalances: WidgetFilter = (widget: WidgetDocument) => { + const { filteredResult: tokens } = filterByOwnedTokens(widget); + const requiredTokens = (widget.metadata as CustomWalletMetadata)?.tokens; + + const filteredTokens = tokens.filter((token) => { + const id = getTokenAddress(token as TokenDocument); + const requiredToken = requiredTokens?.get(id); + + return ( + requiredToken?.amount && + (token as TokenDocument).balance >= requiredToken.amount + ); + }); + + return { + filteredResult: filteredTokens, + hasItems: filteredTokens.length > 0, + }; +}; + +export const filterByOwnedNfts: WidgetFilter = (widget: WidgetDocument) => { + const ownedNfts = getOwnedNfts( + (widget.metadata as CustomWalletMetadata)?.network, ); + const requiredNfts = (widget.metadata as CustomWalletMetadata)?.nfts; + + const filteredNfts = ownedNfts.filter((ownedNft) => { + const splittedStrings = ownedNft.collectionId?.split('/') || []; + const id = splittedStrings[2] || ''; + return requiredNfts?.has(id); + }); + + return { + filteredResult: filteredNfts, + hasItems: filteredNfts.length > 0, + }; }; const getSolanaMintAddress = (mint: string) => { @@ -75,3 +150,7 @@ const getSolanaMintAddress = (mint: string) => { return mint; }; + +export const filterMap = new Map([ + ['samo', [filterByTokenBalances, filterByOwnedNfts]], +]); From ebca8215ccb12fefb6472de18e37aa7bd05691cb Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 5 Aug 2024 12:45:25 +0700 Subject: [PATCH 20/31] change data to widget --- .../src/features/Explorer/Widgets/index.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/wallet/src/features/Explorer/Widgets/index.tsx b/apps/wallet/src/features/Explorer/Widgets/index.tsx index 156927a38..904fee492 100644 --- a/apps/wallet/src/features/Explorer/Widgets/index.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/index.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react'; import { useState } from 'react'; import { ScrollView, StyleSheet, View } from 'react-native'; -import { WidgetType } from '@walless/core'; +import { WidgetCategory } from '@walless/core'; import { Text } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; @@ -9,12 +9,12 @@ import CategoryButtons from './CategoryButtons'; import WidgetItem from './WidgetItem'; interface Props { - data: WidgetDocument[]; + widgets: WidgetDocument[]; } -const Widgets: FC = ({ data }) => { - const [widgets, setWidgets] = useState( - data.filter((item) => item.widgetType === WidgetType.NETWORK), +const Widgets: FC = ({ widgets }) => { + const [renderedWidgets, setRenderedWidgets] = useState( + widgets.filter((widget) => widget.category === WidgetCategory.NETWORK), ); return ( @@ -26,19 +26,19 @@ const Widgets: FC = ({ data }) => { - + - {widgets.length === 0 ? ( + {renderedWidgets.length === 0 ? ( There's no widgets in this section ) : ( - widgets.map((widget) => ( + renderedWidgets.map((widget) => ( )) )} From 150020ba0ded72830c2c3f02b9476339077c42c6 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 5 Aug 2024 15:03:03 +0700 Subject: [PATCH 21/31] fix: type check --- .../wallet/src/features/Explorer/Widgets/CategoryButton.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/wallet/src/features/Explorer/Widgets/CategoryButton.tsx b/apps/wallet/src/features/Explorer/Widgets/CategoryButton.tsx index 2478abd4a..4f32b697c 100644 --- a/apps/wallet/src/features/Explorer/Widgets/CategoryButton.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/CategoryButton.tsx @@ -5,14 +5,14 @@ import Animated, { interpolateColor, useAnimatedStyle, } from 'react-native-reanimated'; -import type { WidgetType } from '@walless/core'; +import type { WidgetCategory } from '@walless/core'; const AnimatedHoverable = Animated.createAnimatedComponent(TouchableOpacity); interface CategoryButtonProps { index: number; - title: WidgetType; - onPress: (index: number, category: WidgetType) => void; + title: string; + onPress: (index: number, category: WidgetCategory | string) => void; animatedValue: SharedValue; data: number[]; } From 6fc0f217a22c4d774ea69ab5027e454fd01c06f3 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 5 Aug 2024 15:20:43 +0700 Subject: [PATCH 22/31] keep the old style of slider tabs and add linear gradient --- .../BuiltInNetwork/AptosTokensTab/index.tsx | 8 ++---- .../features/Widget/BuiltInNetwork/index.tsx | 8 ++---- .../Widget/CustomWalletLayout/index.tsx | 12 ++------ apps/wallet/src/state/widget/shared.ts | 4 +++ packages/core/utils/widget.ts | 4 +-- .../gui/components/SliderTabs/TabItem.tsx | 28 ++++++++----------- packages/gui/components/SliderTabs/index.tsx | 17 ++++++----- 7 files changed, 31 insertions(+), 50 deletions(-) diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx index f891a12f4..0319ffed4 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx @@ -74,9 +74,7 @@ const AptosTokensTab: FC = ({ network }) => { }; const activatedStyle: TabItemStyle = { - containerStyle: { - style: { backgroundColor: '#0694D3' }, - }, + style: { backgroundColor: '#0694D3' }, textStyle: { color: 'white', fontWeight: '500', @@ -84,9 +82,7 @@ const AptosTokensTab: FC = ({ network }) => { }; const deactivatedStyle: TabItemStyle = { - containerStyle: { - style: { backgroundColor: 'transparent' }, - }, + style: { backgroundColor: 'transparent' }, textStyle: { color: '#566674', fontWeight: '400', diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx index 44e18ec6e..6fbce4457 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx @@ -69,9 +69,7 @@ export const BuiltInNetwork: FC = ({ id }) => { }, []); const activatedStyle: TabItemStyle = { - containerStyle: { - style: { backgroundColor: '#0694D3' }, - }, + style: { backgroundColor: '#0694D3' }, textStyle: { color: 'white', fontWeight: '500', @@ -79,9 +77,7 @@ export const BuiltInNetwork: FC = ({ id }) => { }; const deactivatedStyle: TabItemStyle = { - containerStyle: { - style: { backgroundColor: 'transparent' }, - }, + style: { backgroundColor: 'transparent' }, textStyle: { color: '#566674', fontWeight: '400', diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx index 56dbb6476..5b99bd678 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -113,18 +113,10 @@ export const CustomWalletLayout: FC = ({ id }) => { ]; }, []); - const activatedStyle: TabItemStyle = { - containerStyle: customWalletMetadata.activeTabStyle, - textStyle: { - color: 'white', - fontWeight: '500', - }, - }; + const activatedStyle = customWalletMetadata.activeTabStyle; const deactivatedStyle: TabItemStyle = { - containerStyle: { - style: { backgroundColor: 'transparent' }, - }, + style: { backgroundColor: 'transparent' }, textStyle: { color: '#566674', fontWeight: '400', diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index 799b3dd35..76bf34cd5 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -240,6 +240,10 @@ export const mockWidgets: WidgetDocument[] = [ direction: gradientDirection.LeftToRight, colors: ['#1A4FB5', '#C36BE5'], }, + textStyle: { + color: 'white', + fontWeight: '500', + }, }, advertisements: [ { diff --git a/packages/core/utils/widget.ts b/packages/core/utils/widget.ts index db0701a22..e4d6d6128 100644 --- a/packages/core/utils/widget.ts +++ b/packages/core/utils/widget.ts @@ -1,4 +1,4 @@ -import type { TabContainerStyle } from '@walless/gui'; +import type { TabItemStyle } from '@walless/gui'; import type { Networks } from './common'; @@ -42,7 +42,7 @@ export interface CustomWalletMetadata { buy: string; swap: string; }; - activeTabStyle?: TabContainerStyle; + activeTabStyle?: TabItemStyle; advertisements: CustomWalletAdvertisement[]; tokens?: Map; nfts?: Map; diff --git a/packages/gui/components/SliderTabs/TabItem.tsx b/packages/gui/components/SliderTabs/TabItem.tsx index d3bac7f5a..b8e85d59f 100644 --- a/packages/gui/components/SliderTabs/TabItem.tsx +++ b/packages/gui/components/SliderTabs/TabItem.tsx @@ -44,16 +44,12 @@ export const gradientDirection = { }, }; -export interface TabContainerStyle { +export interface TabItemStyle { style?: ViewStyle; linearGradient?: { direction: GradientDirection; colors: string[]; }; -} - -export interface TabItemStyle { - containerStyle?: TabContainerStyle; textStyle?: TextStyle; } @@ -64,13 +60,15 @@ export interface TabAble { interface Props { item: TabAble; - style?: TabItemStyle; + tabStyle?: TabItemStyle; onPress?: (item: TabAble) => void; } -export const TabItem: FC = ({ item, style, onPress }) => { - const containerStyle = style?.containerStyle?.style; - const linearGradientStyle = style?.containerStyle?.linearGradient; +export const TabItem: FC = ({ item, tabStyle, onPress }) => { + const containerStyle = tabStyle?.style; + const linearGradientStyle = tabStyle?.linearGradient; + console.log(linearGradientStyle); + if (linearGradientStyle) { return ( onPress?.(item)}> @@ -80,7 +78,7 @@ export const TabItem: FC = ({ item, style, onPress }) => { start={linearGradientStyle.direction.start} end={linearGradientStyle.direction.end} > - {item.title} + {item.title} ); @@ -91,15 +89,13 @@ export const TabItem: FC = ({ item, style, onPress }) => { style={[styles.hoverable, styles.container, containerStyle]} onPress={() => onPress?.(item)} > - {item.title} + {item.title} ); }; export const activatedStyle: TabItemStyle = { - containerStyle: { - style: { backgroundColor: '#0694D3' }, - }, + style: { backgroundColor: '#0694D3' }, textStyle: { color: 'white', fontWeight: '500', @@ -107,9 +103,7 @@ export const activatedStyle: TabItemStyle = { }; export const deactivatedStyle: TabItemStyle = { - containerStyle: { - style: { backgroundColor: 'transparent' }, - }, + style: { backgroundColor: 'transparent' }, textStyle: { color: '#566674', fontWeight: '400', diff --git a/packages/gui/components/SliderTabs/index.tsx b/packages/gui/components/SliderTabs/index.tsx index 072d403fe..1370f7c21 100644 --- a/packages/gui/components/SliderTabs/index.tsx +++ b/packages/gui/components/SliderTabs/index.tsx @@ -9,8 +9,8 @@ import TabItem from './TabItem'; interface SliderTabsProps { style?: ViewStyle; - activatedStyle: TabItemStyle; - deactivatedStyle: TabItemStyle; + activatedStyle?: TabItemStyle; + deactivatedStyle?: TabItemStyle; items: TabAble[]; activeItem: TabAble; onTabPress?: (item: TabAble) => void; @@ -28,20 +28,19 @@ export const SliderTabs: FC = ({ {items.map((item) => { const isActive = item.id === activeItem.id; - const containerStyle = isActive - ? activatedStyle.containerStyle - : deactivatedStyle.containerStyle; + const containerStyle = isActive ? activatedStyle : deactivatedStyle; const textStyle = isActive - ? activatedStyle.textStyle - : deactivatedStyle.textStyle; + ? activatedStyle?.textStyle + : deactivatedStyle?.textStyle; return ( Date: Mon, 5 Aug 2024 15:25:09 +0700 Subject: [PATCH 23/31] refactor: name the constant --- .../Widget/CustomWalletLayout/Advertisement/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx index 707b6e40b..0e79e757f 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx @@ -10,6 +10,8 @@ import { View } from '@walless/gui'; import AdvertisementIndicator from './AdvertisementIndicator'; import AdvertisementItem from './AdvertisementItem'; +const CHANGE_POINT = 150; + interface Props { ads: CustomWalletAdvertisement[]; } @@ -35,10 +37,10 @@ const Advertisement: FC = ({ ads }) => { if (currentIndex === 0 && event.translationX > 0) return; if (currentIndex === ads.length - 1 && event.translationX < 0) return; - if (event.translationX < -150) { + if (event.translationX < -CHANGE_POINT) { animatedValue.value = currentIndex + 1; setCurrentIndex(currentIndex + 1); - } else if (event.translationX > 150) { + } else if (event.translationX > CHANGE_POINT) { animatedValue.value = currentIndex - 1; setCurrentIndex(currentIndex - 1); } From 0dee52120c24460198d29c65ddfcc3e7ae73f393 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 5 Aug 2024 16:02:08 +0700 Subject: [PATCH 24/31] fix: change icon size to optional --- .../features/Widget/CustomWalletLayout/index.tsx | 2 +- .../Explorer/WidgetNavigator/NavigatorOrb.tsx | 6 ++++-- apps/wallet/src/state/widget/shared.ts | 3 +-- apps/wallet/src/utils/widget.ts | 13 +++++++++++-- packages/core/utils/widget.ts | 2 +- 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx index 5b99bd678..914325b78 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -46,7 +46,7 @@ const convertCustomMetadataToCardSkin = ( ): CardSkin => { const backgroundSrc = { uri: customWalletMetadata.coverBanner }; const iconSrc = { uri: customWalletMetadata.iconSrc }; - const iconSize = storeMeta?.iconSize || 40; + const iconSize = storeMeta?.iconSize || 26; const iconColor = storeMeta?.iconColor || '#ffffff'; return { diff --git a/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx b/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx index 157a82a9e..3d029a595 100644 --- a/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx +++ b/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx @@ -61,10 +61,12 @@ export const NavigatorOrb: FC = ({ }, [isActive]); const iconImgStyle = { - width: iconColor !== 'transparent' ? iconSize : 40, - height: iconColor !== 'transparent' ? iconSize : 40, + width: iconSize, + height: iconSize, }; + console.log(item.name, iconSize); + const handleHoverIn = () => { if (isActive) return; offset.value = withTiming(1, { diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index 76bf34cd5..15f9ee6af 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -219,7 +219,6 @@ export const mockWidgets: WidgetDocument[] = [ timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/widget/samo-icon.png', - iconSize: 26, coverUri: '/img/widget/samo-cover.png', description: 'dApp version of the T-rex Runner you already known!', loveCount: 46, @@ -272,7 +271,7 @@ export const mockWidgets: WidgetDocument[] = [ '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', { mintAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', - amount: 50000, + amount: 0, }, ], [ diff --git a/apps/wallet/src/utils/widget.ts b/apps/wallet/src/utils/widget.ts index 3cd9100eb..72a8716ed 100644 --- a/apps/wallet/src/utils/widget.ts +++ b/apps/wallet/src/utils/widget.ts @@ -113,12 +113,21 @@ export const filterByTokenBalances: WidgetFilter = (widget: WidgetDocument) => { const id = getTokenAddress(token as TokenDocument); const requiredToken = requiredTokens?.get(id); + const result: boolean = + requiredToken?.amount !== undefined && + (token as TokenDocument).balance >= requiredToken?.amount; + console.log(requiredToken); + console.log(token); + console.log(result); + return ( - requiredToken?.amount && - (token as TokenDocument).balance >= requiredToken.amount + requiredToken?.amount !== undefined && + (token as TokenDocument).balance >= requiredToken?.amount ); }); + // console.log(filteredTokens); + return { filteredResult: filteredTokens, hasItems: filteredTokens.length > 0, diff --git a/packages/core/utils/widget.ts b/packages/core/utils/widget.ts index e4d6d6128..2d62833f7 100644 --- a/packages/core/utils/widget.ts +++ b/packages/core/utils/widget.ts @@ -4,7 +4,7 @@ import type { Networks } from './common'; export interface WidgetStoreOptions { iconUri: string; - iconSize: number; + iconSize?: number; iconColor?: string; iconActiveColor?: string; coverUri: string; From 5e907935dfafe8cb79b8eff3abe49303014f4fd3 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Mon, 5 Aug 2024 17:07:29 +0700 Subject: [PATCH 25/31] remove redundant log --- .../wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx | 2 -- packages/gui/components/SliderTabs/TabItem.tsx | 1 - 2 files changed, 3 deletions(-) diff --git a/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx b/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx index 3d029a595..9cafcdd4c 100644 --- a/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx +++ b/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx @@ -65,8 +65,6 @@ export const NavigatorOrb: FC = ({ height: iconSize, }; - console.log(item.name, iconSize); - const handleHoverIn = () => { if (isActive) return; offset.value = withTiming(1, { diff --git a/packages/gui/components/SliderTabs/TabItem.tsx b/packages/gui/components/SliderTabs/TabItem.tsx index b8e85d59f..c00141fe3 100644 --- a/packages/gui/components/SliderTabs/TabItem.tsx +++ b/packages/gui/components/SliderTabs/TabItem.tsx @@ -67,7 +67,6 @@ interface Props { export const TabItem: FC = ({ item, tabStyle, onPress }) => { const containerStyle = tabStyle?.style; const linearGradientStyle = tabStyle?.linearGradient; - console.log(linearGradientStyle); if (linearGradientStyle) { return ( From 696c0aa4417954b20369062ad3a3a7222a465eb7 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Tue, 6 Aug 2024 10:16:12 +0700 Subject: [PATCH 26/31] update scroll animation for advertisement --- .../Advertisement/AdvertisementItem.tsx | 32 +-------- .../Advertisement/index.tsx | 72 +++++++++++-------- 2 files changed, 47 insertions(+), 57 deletions(-) diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx index 33a435237..16f9c99c2 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx @@ -1,44 +1,19 @@ import type { FC } from 'react'; import { Image, StyleSheet } from 'react-native'; -import type { SharedValue } from 'react-native-reanimated'; -import Animated, { useAnimatedStyle } from 'react-native-reanimated'; +import Animated from 'react-native-reanimated'; import type { CustomWalletAdvertisement } from '@walless/core'; import { Anchor, Text } from '@walless/gui'; import { ArrowTopRight } from '@walless/icons'; -type ItemProps = CustomWalletAdvertisement & { - currentIndex: number; - index: number; - offsetX: SharedValue; - animatedValue: SharedValue; -}; - -const IMAGE_SIZE = 266; - -const AdvertisementItem: FC = ({ +const AdvertisementItem: FC = ({ image, link, title, - currentIndex, - index, - offsetX, - animatedValue, }) => { const imageSrc = { uri: image }; - const animatedStyle = useAnimatedStyle(() => { - return { - transform: [ - { - translateX: - (index - currentIndex) * (IMAGE_SIZE + 20) + offsetX.value, - }, - ], - }; - }, [currentIndex, offsetX, animatedValue]); - return ( - + {title} @@ -54,7 +29,6 @@ const styles = StyleSheet.create({ container: { borderRadius: 10, overflow: 'hidden', - position: 'absolute', }, image: { width: 266, diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx index 0e79e757f..be21c959f 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx @@ -1,15 +1,16 @@ import type { FC } from 'react'; import { useRef, useState } from 'react'; +import type { FlatList } from 'react-native'; import { StyleSheet } from 'react-native'; -import type { FlatList } from 'react-native-gesture-handler'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import { useSharedValue } from 'react-native-reanimated'; +import Animated from 'react-native-reanimated'; import type { CustomWalletAdvertisement } from '@walless/core'; import { View } from '@walless/gui'; import AdvertisementIndicator from './AdvertisementIndicator'; import AdvertisementItem from './AdvertisementItem'; +const IMAGE_SIZE = 266; const CHANGE_POINT = 150; interface Props { @@ -20,49 +21,60 @@ const Advertisement: FC = ({ ads }) => { const scrollOffset = useRef(0); const scrollRef = useRef(null); const [currentIndex, setCurrentIndex] = useState(0); - const animatedValue = useSharedValue(0); - const offsetX = useSharedValue(0); const pan = Gesture.Pan() .onUpdate((event) => { + const offset = scrollOffset.current - event.translationX; + if (currentIndex === 0 && event.translationX > 0) return; + if (currentIndex === ads.length - 1 && event.translationX < 0) return; + scrollRef.current?.scrollToOffset({ - offset: scrollOffset.current - event.translationX, + offset, animated: false, }); - offsetX.value = event.translationX; }) .onFinalize((event) => { - offsetX.value = 0; + if (currentIndex === 0 && event.translationX > 0) { + scrollOffset.current = 0; + return; + } + if (currentIndex === ads.length - 1 && event.translationX < 0) { + scrollOffset.current = IMAGE_SIZE * currentIndex; + return; + } - if (currentIndex === 0 && event.translationX > 0) return; - if (currentIndex === ads.length - 1 && event.translationX < 0) return; + let nextIndex = 0; if (event.translationX < -CHANGE_POINT) { - animatedValue.value = currentIndex + 1; - setCurrentIndex(currentIndex + 1); + nextIndex = 1; } else if (event.translationX > CHANGE_POINT) { - animatedValue.value = currentIndex - 1; - setCurrentIndex(currentIndex - 1); + nextIndex = -1; } + + setCurrentIndex(currentIndex + nextIndex); + scrollRef.current?.scrollToIndex({ + index: currentIndex + nextIndex, + animated: false, + }); + scrollOffset.current = IMAGE_SIZE * currentIndex; }); return ( - - {ads.map((item, index) => { - return ( - - ); - })} - + ( + + )} + /> {ads.map((_, index) => ( @@ -82,7 +94,11 @@ export default Advertisement; const styles = StyleSheet.create({ container: { gap: 8, - paddingHorizontal: 20, + paddingHorizontal: 0, + }, + flatlist: { + marginHorizontal: 12, + gap: 12, }, itemsContainer: { flexDirection: 'row', From 6f03cf58b1b1c7a8a0a97dd8eb142a39a1972195 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Tue, 6 Aug 2024 11:01:20 +0700 Subject: [PATCH 27/31] adjust the filter return from object to boolean --- apps/wallet/src/features/Explorer/index.tsx | 6 +-- .../Widget/CustomWalletLayout/index.tsx | 6 +-- apps/wallet/src/state/widget/shared.ts | 2 +- apps/wallet/src/utils/widget.ts | 47 ++++++------------- 4 files changed, 21 insertions(+), 40 deletions(-) diff --git a/apps/wallet/src/features/Explorer/index.tsx b/apps/wallet/src/features/Explorer/index.tsx index 958cf114d..acb07d26a 100644 --- a/apps/wallet/src/features/Explorer/index.tsx +++ b/apps/wallet/src/features/Explorer/index.tsx @@ -28,9 +28,9 @@ export const ExplorerFeature: FC = ({ style }) => { const widgets = useMemo( () => mockWidgets.filter((widget) => { - if (filterMap.has(widget._id)) { - const filters = filterMap.get(widget._id); - return filters?.some((filter) => filter(widget).hasItems); + if (filterMap[widget._id]) { + const filters = filterMap[widget._id]; + return filters?.some((filter) => filter(widget)); } return true; diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx index 914325b78..1f3a7454a 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -67,13 +67,11 @@ export const CustomWalletLayout: FC = ({ id }) => { const network = customWalletMetadata.network; const keys = usePublicKeys(network); - const { filteredResult: filteredTokens } = filterByOwnedTokens( + const filteredTokens = filterByOwnedTokens( customWalletWidget as WidgetDocument, ); - const { filteredResult: filteredNfts } = filterByOwnedNfts( - customWalletWidget as WidgetDocument, - ); + const filteredNfts = filterByOwnedNfts(customWalletWidget as WidgetDocument); const valuation = (filteredTokens as TokenDocument[])?.reduce( (accumulator, token) => accumulator + getTokenValue(token, 'usd'), diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index 15f9ee6af..12d2b4378 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -271,7 +271,7 @@ export const mockWidgets: WidgetDocument[] = [ '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', { mintAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', - amount: 0, + amount: 50000, }, ], [ diff --git a/apps/wallet/src/utils/widget.ts b/apps/wallet/src/utils/widget.ts index 72a8716ed..13174cb34 100644 --- a/apps/wallet/src/utils/widget.ts +++ b/apps/wallet/src/utils/widget.ts @@ -7,10 +7,7 @@ import { Networks } from '@walless/core'; import type { TokenDocument, WidgetDocument } from '@walless/store'; import { nftState, tokenState } from 'state/assets'; -export type WidgetFilter = (widget: WidgetDocument) => { - filteredResult: unknown[]; - hasItems: boolean; -}; +export type WidgetFilter = (widget: WidgetDocument) => boolean; import { solMint, SUI_COIN_TYPE, wrappedSolMint } from './constants'; @@ -89,7 +86,7 @@ const getOwnedNfts = (network?: Networks, address?: string) => { return nfts; }; -export const filterByOwnedTokens: WidgetFilter = (widget: WidgetDocument) => { +export const filterByOwnedTokens = (widget: WidgetDocument) => { const ownedTokens = getOwnedTokens( (widget.metadata as CustomWalletMetadata)?.network, ); @@ -99,42 +96,27 @@ export const filterByOwnedTokens: WidgetFilter = (widget: WidgetDocument) => { return requiredTokens?.has(id); }); - return { - filteredResult: filteredTokens, - hasItems: filteredTokens.length > 0, - }; + return filteredTokens; }; -export const filterByTokenBalances: WidgetFilter = (widget: WidgetDocument) => { - const { filteredResult: tokens } = filterByOwnedTokens(widget); +export const explorerFilterByTokenBalances = (widget: WidgetDocument) => { + const tokens = filterByOwnedTokens(widget); const requiredTokens = (widget.metadata as CustomWalletMetadata)?.tokens; const filteredTokens = tokens.filter((token) => { const id = getTokenAddress(token as TokenDocument); const requiredToken = requiredTokens?.get(id); - const result: boolean = - requiredToken?.amount !== undefined && - (token as TokenDocument).balance >= requiredToken?.amount; - console.log(requiredToken); - console.log(token); - console.log(result); - return ( requiredToken?.amount !== undefined && (token as TokenDocument).balance >= requiredToken?.amount ); }); - // console.log(filteredTokens); - - return { - filteredResult: filteredTokens, - hasItems: filteredTokens.length > 0, - }; + return filteredTokens.length > 0; }; -export const filterByOwnedNfts: WidgetFilter = (widget: WidgetDocument) => { +export const filterByOwnedNfts = (widget: WidgetDocument) => { const ownedNfts = getOwnedNfts( (widget.metadata as CustomWalletMetadata)?.network, ); @@ -146,10 +128,11 @@ export const filterByOwnedNfts: WidgetFilter = (widget: WidgetDocument) => { return requiredNfts?.has(id); }); - return { - filteredResult: filteredNfts, - hasItems: filteredNfts.length > 0, - }; + return filteredNfts; +}; + +export const explorerFilterByOwnedNfts = (widget: WidgetDocument) => { + return filterByOwnedNfts(widget).length > 0; }; const getSolanaMintAddress = (mint: string) => { @@ -160,6 +143,6 @@ const getSolanaMintAddress = (mint: string) => { return mint; }; -export const filterMap = new Map([ - ['samo', [filterByTokenBalances, filterByOwnedNfts]], -]); +export const filterMap: Record = { + samo: [explorerFilterByTokenBalances, explorerFilterByOwnedNfts], +}; From 0bdcc98935808f85a8ae6edf73609b7c0dc18868 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Tue, 6 Aug 2024 11:06:19 +0700 Subject: [PATCH 28/31] move token list and collectible list to components --- .../Widget/BuiltInNetwork => components}/CollectibleList.tsx | 0 .../Widget/BuiltInNetwork => components}/TokenList/Item.tsx | 0 .../BuiltInNetwork => components}/TokenList/ListEmpty.tsx | 0 .../BuiltInNetwork => components}/TokenList/Separator.tsx | 0 .../Widget/BuiltInNetwork => components}/TokenList/index.tsx | 0 apps/wallet/src/features/Swap/Select/SelectFromToken.tsx | 2 +- apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx | 2 +- apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx | 4 ++-- 8 files changed, 4 insertions(+), 4 deletions(-) rename apps/wallet/src/{features/Widget/BuiltInNetwork => components}/CollectibleList.tsx (100%) rename apps/wallet/src/{features/Widget/BuiltInNetwork => components}/TokenList/Item.tsx (100%) rename apps/wallet/src/{features/Widget/BuiltInNetwork => components}/TokenList/ListEmpty.tsx (100%) rename apps/wallet/src/{features/Widget/BuiltInNetwork => components}/TokenList/Separator.tsx (100%) rename apps/wallet/src/{features/Widget/BuiltInNetwork => components}/TokenList/index.tsx (100%) diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/CollectibleList.tsx b/apps/wallet/src/components/CollectibleList.tsx similarity index 100% rename from apps/wallet/src/features/Widget/BuiltInNetwork/CollectibleList.tsx rename to apps/wallet/src/components/CollectibleList.tsx diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/Item.tsx b/apps/wallet/src/components/TokenList/Item.tsx similarity index 100% rename from apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/Item.tsx rename to apps/wallet/src/components/TokenList/Item.tsx diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/ListEmpty.tsx b/apps/wallet/src/components/TokenList/ListEmpty.tsx similarity index 100% rename from apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/ListEmpty.tsx rename to apps/wallet/src/components/TokenList/ListEmpty.tsx diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/Separator.tsx b/apps/wallet/src/components/TokenList/Separator.tsx similarity index 100% rename from apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/Separator.tsx rename to apps/wallet/src/components/TokenList/Separator.tsx diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/index.tsx b/apps/wallet/src/components/TokenList/index.tsx similarity index 100% rename from apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/index.tsx rename to apps/wallet/src/components/TokenList/index.tsx diff --git a/apps/wallet/src/features/Swap/Select/SelectFromToken.tsx b/apps/wallet/src/features/Swap/Select/SelectFromToken.tsx index c69ce0be9..e9e7a6c8f 100644 --- a/apps/wallet/src/features/Swap/Select/SelectFromToken.tsx +++ b/apps/wallet/src/features/Swap/Select/SelectFromToken.tsx @@ -6,7 +6,7 @@ import type { SolanaToken } from '@walless/core'; import { runtime } from '@walless/core'; import { SwipeDownGesture } from '@walless/gui'; import type { TokenDocument } from '@walless/store'; -import TokenList from 'features/Widget/BuiltInNetwork/TokenList'; +import TokenList from 'components/TokenList'; import { useSafeAreaInsets, useSnapshot, useTokens } from 'utils/hooks'; import { swapActions, swapContext } from '../context'; diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx index 6fbce4457..cc5908092 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx @@ -24,7 +24,7 @@ import ActivityTab from './ActivityTab'; import AptosTokensTab from './AptosTokensTab'; import NftTab from './NftTab'; import { getWalletCardSkin, layoutTabs } from './shared'; -import TokenList from './TokenList'; +import TokenList from '../../../components/TokenList'; import WalletCard from './WalletCard'; interface Props { diff --git a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx index 1f3a7454a..0602bea34 100644 --- a/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -26,9 +26,9 @@ import { getTokenValue, useOpacityAnimated, usePublicKeys } from 'utils/hooks'; import { copy } from 'utils/system'; import { filterByOwnedNfts, filterByOwnedTokens } from 'utils/widget'; +import CollectibleList from '../../../components/CollectibleList'; +import TokenList from '../../../components/TokenList'; import ActivityTab from '../BuiltInNetwork/ActivityTab'; -import CollectibleList from '../BuiltInNetwork/CollectibleList'; -import TokenList from '../BuiltInNetwork/TokenList'; import type { CardSkin } from '../BuiltInNetwork/WalletCard'; import { WalletCard } from '../BuiltInNetwork/WalletCard'; From d04a81fbe04cf75a53f5c448e2f791c87df7a799 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Tue, 6 Aug 2024 11:55:50 +0700 Subject: [PATCH 29/31] combine category and subcategory of widget --- .../Explorer/Widgets/CategoryButtons.tsx | 16 +++++------ .../src/features/Explorer/Widgets/index.tsx | 8 ++++-- apps/wallet/src/state/widget/shared.ts | 18 ++++++------- packages/core/utils/widget.ts | 27 +++++++++++++------ 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx b/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx index d8bcf93f1..e94b58aed 100644 --- a/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx @@ -1,8 +1,7 @@ import type { FC } from 'react'; import { Animated, StyleSheet } from 'react-native'; import { useSharedValue, withTiming } from 'react-native-reanimated'; -import type { WidgetCategory } from '@walless/core'; -import { categories, communityList } from '@walless/core'; +import { SubcategoryToCategoryMapping, WidgetCategories } from '@walless/core'; import type { WidgetDocument } from '@walless/store'; import CategoryButton from './CategoryButton'; @@ -15,23 +14,20 @@ interface CategoryButtonsProps { const CategoryButtons: FC = ({ widgets, setWidgets }) => { const currentIndex = useSharedValue(0); const animatedValue = useSharedValue(0); + const categories = Object.values(WidgetCategories); const inputRange = categories.map((_, index) => index); const handleCategoryPress = ( activeIndex: number, - category: WidgetCategory | string, + category: WidgetCategories, ) => { currentIndex.value = activeIndex; animatedValue.value = withTiming(activeIndex); - const filteredLayoutCards = widgets.filter((widget) => { - if (category === 'Community') { - return communityList.includes(widget.category); - } - - return widget.category === category; - }); + const filteredLayoutCards = widgets.filter( + (widget) => SubcategoryToCategoryMapping[widget.category] === category, + ); setWidgets(filteredLayoutCards); }; diff --git a/apps/wallet/src/features/Explorer/Widgets/index.tsx b/apps/wallet/src/features/Explorer/Widgets/index.tsx index 904fee492..c1f11a2a6 100644 --- a/apps/wallet/src/features/Explorer/Widgets/index.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/index.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react'; import { useState } from 'react'; import { ScrollView, StyleSheet, View } from 'react-native'; -import { WidgetCategory } from '@walless/core'; +import { SubcategoryToCategoryMapping, WidgetCategories } from '@walless/core'; import { Text } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; @@ -14,7 +14,11 @@ interface Props { const Widgets: FC = ({ widgets }) => { const [renderedWidgets, setRenderedWidgets] = useState( - widgets.filter((widget) => widget.category === WidgetCategory.NETWORK), + widgets.filter( + (widget) => + SubcategoryToCategoryMapping[widget.category] === + WidgetCategories.NETWORK, + ), ); return ( diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index 12d2b4378..7ad265a69 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -1,5 +1,5 @@ import type { CustomWalletAssets } from '@walless/core'; -import { Networks, WidgetCategory } from '@walless/core'; +import { Networks, WidgetSubcategories } from '@walless/core'; import { gradientDirection } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; @@ -11,7 +11,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.solana], version: '0.1.8', type: 'Widget', - category: WidgetCategory.GAME, + category: WidgetSubcategories.GAME, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-pixeverse.png', @@ -36,7 +36,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.solana], version: '0.9.1', type: 'Widget', - category: WidgetCategory.NETWORK, + category: WidgetSubcategories.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-solana.png', @@ -62,7 +62,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.sui], version: '0.0.1', type: 'Widget', - category: WidgetCategory.NETWORK, + category: WidgetSubcategories.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-sui.png', @@ -88,7 +88,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.sui], version: '0.0.1', type: 'Widget', - category: WidgetCategory.NETWORK, + category: WidgetSubcategories.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/network/tezos-icon-sm.png', @@ -114,7 +114,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.aptos], version: '0.0.1', type: 'Widget', - category: WidgetCategory.NETWORK, + category: WidgetSubcategories.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-aptos.png', @@ -140,7 +140,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [], version: '0.1.8', type: 'Widget', - category: WidgetCategory.GAME, + category: WidgetSubcategories.GAME, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/t-rex-runner/runner-icon.png', @@ -165,7 +165,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [], version: '0.0.1', type: 'Widget', - category: WidgetCategory.GAME, + category: WidgetSubcategories.GAME, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/sui-jump/suijump-icon.png', @@ -215,7 +215,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.solana], version: '0.0.1', type: 'Widget', - category: WidgetCategory.CUSTOM_WALLET, + category: WidgetSubcategories.CUSTOM_WALLET, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/widget/samo-icon.png', diff --git a/packages/core/utils/widget.ts b/packages/core/utils/widget.ts index 2d62833f7..8466b0244 100644 --- a/packages/core/utils/widget.ts +++ b/packages/core/utils/widget.ts @@ -49,24 +49,35 @@ export interface CustomWalletMetadata { network: Networks; } -export const categories = ['Network', 'Game', 'Community']; - -export type CustomMetadata = CustomWalletMetadata | WidgetNetworkMetadata; - -export enum WidgetCategory { +export enum WidgetCategories { + NETWORK = 'Network', GAME = 'Game', - CUSTOM_WALLET = 'CustomWallet', + COMMUNITY = 'Community', +} + +export enum WidgetSubcategories { + CUSTOM_WALLET = 'Custom Wallet', NETWORK = 'Network', + GAME = 'Game', } -export const communityList = [WidgetCategory.CUSTOM_WALLET]; +export type CustomMetadata = CustomWalletMetadata | WidgetNetworkMetadata; + +export const SubcategoryToCategoryMapping: Record< + WidgetSubcategories, + WidgetCategories +> = { + [WidgetSubcategories.CUSTOM_WALLET]: WidgetCategories.COMMUNITY, + [WidgetSubcategories.NETWORK]: WidgetCategories.NETWORK, + [WidgetSubcategories.GAME]: WidgetCategories.GAME, +}; export interface Widget { name: string; networks: Networks[]; version: string; timestamp?: string; - category: WidgetCategory; + category: WidgetSubcategories; storeMeta: WidgetStoreOptions; metadata?: CustomMetadata; } From b8d3351727300d02cb175a90afed4f3bf1788f55 Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Tue, 6 Aug 2024 12:02:39 +0700 Subject: [PATCH 30/31] update type after changing the way to classify widgets --- .../wallet/src/features/Explorer/Widgets/CategoryButton.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/wallet/src/features/Explorer/Widgets/CategoryButton.tsx b/apps/wallet/src/features/Explorer/Widgets/CategoryButton.tsx index 4f32b697c..a924279bd 100644 --- a/apps/wallet/src/features/Explorer/Widgets/CategoryButton.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/CategoryButton.tsx @@ -5,14 +5,14 @@ import Animated, { interpolateColor, useAnimatedStyle, } from 'react-native-reanimated'; -import type { WidgetCategory } from '@walless/core'; +import type { WidgetCategories } from '@walless/core'; const AnimatedHoverable = Animated.createAnimatedComponent(TouchableOpacity); interface CategoryButtonProps { index: number; - title: string; - onPress: (index: number, category: WidgetCategory | string) => void; + title: WidgetCategories; + onPress: (index: number, category: WidgetCategories) => void; animatedValue: SharedValue; data: number[]; } From ea75b5202cfa8f556f3b8c2de76208fb97953c6f Mon Sep 17 00:00:00 2001 From: ltminhthu Date: Tue, 6 Aug 2024 12:09:19 +0700 Subject: [PATCH 31/31] fix: eslint --- apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx index cc5908092..934d71b2c 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx @@ -20,11 +20,12 @@ import { buyToken } from 'utils/buy'; import { useOpacityAnimated, usePublicKeys, useTokens } from 'utils/hooks'; import { copy } from 'utils/system'; +import TokenList from '../../../components/TokenList'; + import ActivityTab from './ActivityTab'; import AptosTokensTab from './AptosTokensTab'; import NftTab from './NftTab'; import { getWalletCardSkin, layoutTabs } from './shared'; -import TokenList from '../../../components/TokenList'; import WalletCard from './WalletCard'; interface Props {