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 000000000..19b7b7f5a Binary files /dev/null and b/apps/wallet/assets/img/widget/samo-ad-1.png differ diff --git a/apps/wallet/assets/img/widget/samo-banner.png b/apps/wallet/assets/img/widget/samo-banner.png new file mode 100644 index 000000000..dd64061d6 Binary files /dev/null and b/apps/wallet/assets/img/widget/samo-banner.png differ 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 000000000..18cbafafd Binary files /dev/null and b/apps/wallet/assets/img/widget/samo-cover.png differ diff --git a/apps/wallet/assets/img/widget/samo-icon.png b/apps/wallet/assets/img/widget/samo-icon.png new file mode 100644 index 000000000..dc97ed204 Binary files /dev/null and b/apps/wallet/assets/img/widget/samo-icon.png differ diff --git a/apps/wallet/src/components/CollectibleList.tsx b/apps/wallet/src/components/CollectibleList.tsx new file mode 100644 index 000000000..6ae63e6e5 --- /dev/null +++ b/apps/wallet/src/components/CollectibleList.tsx @@ -0,0 +1,116 @@ +import type { FC } from 'react'; +import { ScrollView, StyleSheet } from 'react-native'; +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 } from 'utils/hooks'; +import { navigate } from 'utils/navigation'; + +interface Props { + collections?: WrappedCollection[]; + nfts?: NftDocument[]; +} + +export const CollectibleList: FC = ({ collections = [], nfts = [] }) => { + const { onGridContainerLayout, width } = useLazyGridLayout({ + referenceWidth: 150, + gap: gridGap, + }); + + const handlePressCollection = (ele: WrappedCollection) => { + const collectionId = ele._id.split('/')[2]; + + navigate('Dashboard', { + screen: 'Explore', + params: { + screen: 'Collection', + params: { + screen: 'Default', + params: { id: collectionId }, + }, + }, + }); + }; + + const handlePressCollectible = (ele: NftDocument) => { + const collectibleId = ele._id.split('/')[2]; + + navigate('Dashboard', { + screen: 'Explore', + params: { + screen: 'Collection', + params: { screen: 'NFT', params: { id: collectibleId } }, + }, + }); + }; + + return ( + onGridContainerLayout(e.nativeEvent.layout)} + > + {collections.length === 0 && nfts.length === 0 && ( + + You do not have any NFT yet + + )} + + {width > 0 && + collections.map((ele, index) => { + return ( + handlePressCollection(ele)} + size={width} + /> + ); + })} + + + + {width > 0 && + nfts.map((ele, index) => { + return ( + handlePressCollectible(ele)} + size={width} + /> + ); + })} + + + ); +}; + +export default CollectibleList; + +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/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 94% rename from apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/index.tsx rename to apps/wallet/src/components/TokenList/index.tsx index 37038bd3e..91d0a53f8 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/index.tsx +++ b/apps/wallet/src/components/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/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/Highlights/index.tsx b/apps/wallet/src/features/Explorer/Highlights/index.tsx index f83ac4415..b804e24df 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 { + widgets: WidgetDocument[]; +} + +const Highlights: FC = ({ widgets }) => { const [currentIndex, setCurrentIndex] = useState(0); return ( @@ -18,14 +23,14 @@ const Highlights = () => { diff --git a/apps/wallet/src/features/Explorer/Widgets/CategoryButton.tsx b/apps/wallet/src/features/Explorer/Widgets/CategoryButton.tsx index 2478abd4a..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 { WidgetType } from '@walless/core'; +import type { WidgetCategories } from '@walless/core'; const AnimatedHoverable = Animated.createAnimatedComponent(TouchableOpacity); interface CategoryButtonProps { index: number; - title: WidgetType; - onPress: (index: number, category: WidgetType) => void; + title: WidgetCategories; + onPress: (index: number, category: WidgetCategories) => void; animatedValue: SharedValue; data: number[]; } diff --git a/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx b/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx index 2dd18017a..e94b58aed 100644 --- a/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/CategoryButtons.tsx @@ -1,28 +1,32 @@ import type { FC } from 'react'; import { Animated, StyleSheet } from 'react-native'; import { useSharedValue, withTiming } from 'react-native-reanimated'; -import { WidgetType } from '@walless/core'; +import { SubcategoryToCategoryMapping, WidgetCategories } from '@walless/core'; import type { WidgetDocument } from '@walless/store'; -import { mockWidgets } from 'state/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 categories = Object.values(WidgetCategories); const inputRange = categories.map((_, index) => index); - const handleCategoryPress = (activeIndex: number, category: WidgetType) => { + const handleCategoryPress = ( + activeIndex: number, + category: WidgetCategories, + ) => { currentIndex.value = activeIndex; animatedValue.value = withTiming(activeIndex); - const filteredLayoutCards = mockWidgets.filter( - (item) => item.widgetType === 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 5cb9ee2eb..c1f11a2a6 100644 --- a/apps/wallet/src/features/Explorer/Widgets/index.tsx +++ b/apps/wallet/src/features/Explorer/Widgets/index.tsx @@ -1,16 +1,24 @@ +import type { FC } from 'react'; import { useState } from 'react'; import { ScrollView, StyleSheet, View } from 'react-native'; -import { WidgetType } from '@walless/core'; +import { SubcategoryToCategoryMapping, WidgetCategories } 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 = () => { - const [widgets, setWidgets] = useState( - mockWidgets.filter((item) => item.widgetType === WidgetType.NETWORK), +interface Props { + widgets: WidgetDocument[]; +} + +const Widgets: FC = ({ widgets }) => { + const [renderedWidgets, setRenderedWidgets] = useState( + widgets.filter( + (widget) => + SubcategoryToCategoryMapping[widget.category] === + WidgetCategories.NETWORK, + ), ); return ( @@ -21,18 +29,20 @@ const Widgets = () => { Evolving your worlds filled with exciting events - + + + - {widgets.length === 0 ? ( + {renderedWidgets.length === 0 ? ( There's no widgets in this section ) : ( - widgets.map((widget) => ( + renderedWidgets.map((widget) => ( )) )} diff --git a/apps/wallet/src/features/Explorer/index.tsx b/apps/wallet/src/features/Explorer/index.tsx index 04a0f141c..acb07d26a 100644 --- a/apps/wallet/src/features/Explorer/index.tsx +++ b/apps/wallet/src/features/Explorer/index.tsx @@ -1,8 +1,12 @@ import type { FC } from 'react'; +import { useMemo } from 'react'; 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 { mockWidgets } from 'state/widget'; +import { useNfts, useTokens } from 'utils/hooks'; +import { filterMap } from 'utils/widget'; import Header from './Header'; import Highlights from './Highlights'; @@ -18,14 +22,30 @@ interface Props { } export const ExplorerFeature: FC = ({ style }) => { + const { tokens } = useTokens(); + const { nfts } = useNfts(); + + const widgets = useMemo( + () => + mockWidgets.filter((widget) => { + if (filterMap[widget._id]) { + const filters = filterMap[widget._id]; + return filters?.some((filter) => filter(widget)); + } + + return true; + }), + [tokens, nfts], + ); + return (
- - + + ); diff --git a/apps/wallet/src/features/Swap/Select/SelectFromToken.tsx b/apps/wallet/src/features/Swap/Select/SelectFromToken.tsx index 563bafdc3..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'; @@ -64,7 +64,7 @@ const SelectFromToken: FC = () => { diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/AptosTokensTab/index.tsx index 69c81b4ad..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: { - backgroundColor: '#0694D3', - }, + style: { backgroundColor: '#0694D3' }, textStyle: { color: 'white', fontWeight: '500', @@ -84,9 +82,7 @@ const AptosTokensTab: FC = ({ network }) => { }; const deactivatedStyle: TabItemStyle = { - containerStyle: { - backgroundColor: 'transparent', - }, + style: { backgroundColor: 'transparent' }, textStyle: { color: '#566674', fontWeight: '400', 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 aedbd5f96..000000000 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/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/BuiltInNetwork/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx index bd68650f0..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 TokenTab from './TokenTab'; import WalletCard from './WalletCard'; interface Props { @@ -36,7 +37,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 +49,9 @@ export const BuiltInNetwork: FC = ({ id }) => { return [ { id: 'tokens', - component: () => , + component: () => ( + + ), }, { id: 'collectibles', @@ -67,9 +70,7 @@ export const BuiltInNetwork: FC = ({ id }) => { }, []); const activatedStyle: TabItemStyle = { - containerStyle: { - backgroundColor: '#0694D3', - }, + style: { backgroundColor: '#0694D3' }, textStyle: { color: 'white', fontWeight: '500', @@ -77,9 +78,7 @@ export const BuiltInNetwork: FC = ({ id }) => { }; const deactivatedStyle: TabItemStyle = { - containerStyle: { - backgroundColor: 'transparent', - }, + style: { backgroundColor: 'transparent' }, textStyle: { color: '#566674', fontWeight: '400', @@ -178,4 +177,8 @@ const styles = StyleSheet.create({ flex: 1, overflow: 'hidden', }, + tokenListContainer: { + marginVertical: 16, + overflow: 'hidden', + }, }); 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, + }, +}); 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..16f9c99c2 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/AdvertisementItem.tsx @@ -0,0 +1,48 @@ +import type { FC } from 'react'; +import { Image, StyleSheet } from 'react-native'; +import Animated from 'react-native-reanimated'; +import type { CustomWalletAdvertisement } from '@walless/core'; +import { Anchor, Text } from '@walless/gui'; +import { ArrowTopRight } from '@walless/icons'; + +const AdvertisementItem: FC = ({ + image, + link, + title, +}) => { + const imageSrc = { uri: image }; + + return ( + + + + {title} + + + + ); +}; + +export default AdvertisementItem; + +const styles = StyleSheet.create({ + container: { + borderRadius: 10, + overflow: 'hidden', + }, + image: { + width: 266, + height: 118, + }, + linkContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: 10, + paddingHorizontal: 12, + backgroundColor: '#0C334E', + }, + 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..be21c959f --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/Advertisement/index.tsx @@ -0,0 +1,113 @@ +import type { FC } from 'react'; +import { useRef, useState } from 'react'; +import type { FlatList } from 'react-native'; +import { StyleSheet } from 'react-native'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +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 { + ads: CustomWalletAdvertisement[]; +} + +const Advertisement: FC = ({ ads }) => { + const scrollOffset = useRef(0); + const scrollRef = useRef(null); + const [currentIndex, setCurrentIndex] = useState(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, + animated: false, + }); + }) + .onFinalize((event) => { + if (currentIndex === 0 && event.translationX > 0) { + scrollOffset.current = 0; + return; + } + if (currentIndex === ads.length - 1 && event.translationX < 0) { + scrollOffset.current = IMAGE_SIZE * currentIndex; + return; + } + + let nextIndex = 0; + + if (event.translationX < -CHANGE_POINT) { + nextIndex = 1; + } else if (event.translationX > CHANGE_POINT) { + nextIndex = -1; + } + + setCurrentIndex(currentIndex + nextIndex); + scrollRef.current?.scrollToIndex({ + index: currentIndex + nextIndex, + animated: false, + }); + scrollOffset.current = IMAGE_SIZE * currentIndex; + }); + + return ( + + + ( + + )} + /> + + + {ads.map((_, index) => ( + + ))} + + + ); +}; + +export default Advertisement; + +const styles = StyleSheet.create({ + container: { + gap: 8, + paddingHorizontal: 0, + }, + flatlist: { + marginHorizontal: 12, + gap: 12, + }, + itemsContainer: { + flexDirection: 'row', + minHeight: 164, + minWidth: 200, + }, + indicatorContainer: { + flexDirection: 'row', + gap: 4, + alignSelf: 'center', + }, +}); 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/index.tsx b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx new file mode 100644 index 000000000..0602bea34 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -0,0 +1,218 @@ +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 type { + CustomWalletMetadata, + Token, + 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'; +import type { + NftDocument, + TokenDocument, + WidgetDocument, +} from '@walless/store'; +import { showCopiedModal } from 'modals/Notification'; +import { mockWidgets } from 'state/widget'; +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 type { CardSkin } from '../BuiltInNetwork/WalletCard'; +import { WalletCard } from '../BuiltInNetwork/WalletCard'; + +import Advertisement from './Advertisement'; +import FeatureButtons from './FeatureButtons'; +import { layoutTabs } from './shared'; + +interface Props { + id: string; +} + +const convertCustomMetadataToCardSkin = ( + customWalletMetadata: CustomWalletMetadata, + storeMeta?: WidgetStoreOptions, +): CardSkin => { + const backgroundSrc = { uri: customWalletMetadata.coverBanner }; + const iconSrc = { uri: customWalletMetadata.iconSrc }; + const iconSize = storeMeta?.iconSize || 26; + const iconColor = storeMeta?.iconColor || '#ffffff'; + + return { + backgroundSrc, + iconSrc, + iconSize, + iconColor, + }; +}; + +export const CustomWalletLayout: FC = ({ id }) => { + const customWalletWidget = mockWidgets.find((item) => item._id === id); + const [activeTabIndex, setActiveTabIndex] = useState(0); + const [headerLayout, setHeaderLayout] = useState(); + const customWalletMetadata = + customWalletWidget?.metadata as CustomWalletMetadata; + + const network = customWalletMetadata.network; + + const keys = usePublicKeys(network); + const filteredTokens = filterByOwnedTokens( + customWalletWidget as WidgetDocument, + ); + + const filteredNfts = filterByOwnedNfts(customWalletWidget as WidgetDocument); + + const valuation = (filteredTokens as TokenDocument[])?.reduce( + (accumulator, token) => accumulator + getTokenValue(token, 'usd'), + 0, + ); + const cardSkin = convertCustomMetadataToCardSkin( + customWalletMetadata, + customWalletWidget?.storeMeta, + ); + const opacityAnimated = useOpacityAnimated({ from: 0, to: 1 }); + + const container: ViewStyle = { + ...styles.container, + }; + + const bottomSliderItems: SlideOption[] = useMemo(() => { + return [ + { + id: 'tokens', + component: () => ( + []} + style={styles.tokenListContainer} + /> + ), + }, + { + id: 'collectibles', + component: () => ( + + ), + }, + { + id: 'activities', + component: () => , + }, + ]; + }, []); + + const activatedStyle = customWalletMetadata.activeTabStyle; + + const deactivatedStyle: TabItemStyle = { + style: { 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 handleCopyAddress = (value: string) => { + copy(value); + showCopiedModal(); + }; + + if (!customWalletWidget) return null; + + return ( + + + {headerLayout?.width && + keys.map((item, index) => { + return ( + + ); + })} + + + + + + + + + {activeTabIndex === 0 && ( + + )} + + ); +}; + +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, + minHeight: 200, + overflow: 'hidden', + }, + tokenListContainer: { + marginVertical: 16, + 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..ff57d0d70 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/shared.ts @@ -0,0 +1,16 @@ +import type { TabAble } from '@walless/gui/components/SliderTabs/TabItem'; + +export const layoutTabs: TabAble[] = [ + { + id: 'tokens', + title: 'Tokens', + }, + { + id: 'collectibles', + title: 'Collectibles', + }, + { + id: 'activities', + title: 'Activities', + }, +]; 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/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx b/apps/wallet/src/stacks/Explorer/WidgetNavigator/NavigatorOrb.tsx index 9225f139f..9cafcdd4c 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), diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index f30ec92ce..7ad265a69 100644 --- a/apps/wallet/src/state/widget/shared.ts +++ b/apps/wallet/src/state/widget/shared.ts @@ -1,4 +1,6 @@ -import { Networks, WidgetType } from '@walless/core'; +import type { CustomWalletAssets } from '@walless/core'; +import { Networks, WidgetSubcategories } from '@walless/core'; +import { gradientDirection } from '@walless/gui'; import type { WidgetDocument } from '@walless/store'; // TODO: this mocked data is for web only @@ -9,7 +11,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.solana], version: '0.1.8', type: 'Widget', - widgetType: WidgetType.GAME, + category: WidgetSubcategories.GAME, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-pixeverse.png', @@ -20,7 +22,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - networkMeta: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/explore/logo-pixeverse.png', iconUri: '/img/explore/logo-pixeverse.png', @@ -34,7 +36,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.solana], version: '0.9.1', type: 'Widget', - widgetType: WidgetType.NETWORK, + category: WidgetSubcategories.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: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/network/solana-icon-lg.png', iconUri: '/img/network/solana-icon-sm.svg', @@ -60,7 +62,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.sui], version: '0.0.1', type: 'Widget', - widgetType: WidgetType.NETWORK, + category: WidgetSubcategories.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-sui.png', @@ -72,7 +74,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 100, activeCount: 567, }, - networkMeta: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/network/sui-icon-lg.png', iconUri: '/img/network/sui-icon-sm.png', @@ -86,7 +88,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.sui], version: '0.0.1', type: 'Widget', - widgetType: WidgetType.NETWORK, + category: WidgetSubcategories.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/network/tezos-icon-sm.png', @@ -98,7 +100,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 100, activeCount: 567, }, - networkMeta: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/network/tezos-icon-lg.png', iconUri: '/img/network/tezos-icon-sm.png', @@ -112,7 +114,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [Networks.aptos], version: '0.0.1', type: 'Widget', - widgetType: WidgetType.NETWORK, + category: WidgetSubcategories.NETWORK, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/explore/logo-aptos.png', @@ -124,7 +126,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - networkMeta: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/explore/aptos-icon.svg', iconUri: '/img/explore/aptos-icon.svg', @@ -138,7 +140,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [], version: '0.1.8', type: 'Widget', - widgetType: WidgetType.GAME, + category: WidgetSubcategories.GAME, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/t-rex-runner/runner-icon.png', @@ -149,7 +151,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - networkMeta: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/t-rex-runner/runner-icon.png', iconUri: '/img/t-rex-runner/runner-icon.png', @@ -163,7 +165,7 @@ export const mockWidgets: WidgetDocument[] = [ networks: [], version: '0.0.1', type: 'Widget', - widgetType: WidgetType.GAME, + category: WidgetSubcategories.GAME, timestamp: new Date().toISOString(), storeMeta: { iconUri: '/img/sui-jump/suijump-icon.png', @@ -174,7 +176,7 @@ export const mockWidgets: WidgetDocument[] = [ loveCount: 46, activeCount: 202, }, - networkMeta: { + metadata: { backgroundUri: '/img/network/sky-card-bg.png', markUri: '/img/sui-jump/suijump-icon.png', iconUri: '/img/sui-jump/suijump-icon.png', @@ -199,7 +201,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', @@ -207,4 +209,88 @@ export const mockWidgets: WidgetDocument[] = [ // iconSize: 16, // }, // }, + { + _id: 'samo', + name: 'SAMO', + networks: [Networks.solana], + version: '0.0.1', + type: 'Widget', + category: WidgetSubcategories.CUSTOM_WALLET, + timestamp: new Date().toISOString(), + storeMeta: { + iconUri: '/img/widget/samo-icon.png', + coverUri: '/img/widget/samo-cover.png', + description: 'dApp version of the T-rex Runner you already known!', + loveCount: 46, + activeCount: 202, + }, + metadata: { + 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'], + }, + textStyle: { + color: 'white', + fontWeight: '500', + }, + }, + advertisements: [ + { + 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', + }, + { + title: 'Get your SAMO debit card', + link: '', + image: '/img/widget/samo-ad-1.png', + }, + ], + tokens: new Map([ + [ + '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', + { + mintAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', + amount: 50000, + }, + ], + [ + 'So11111111111111111111111111111111111111112', + { + mintAddress: 'So11111111111111111111111111111111111111112', + }, + ], + ]), + nfts: new Map([ + [ + '98fe506a37c46d67b7212ec689decd6fcd7137ea751fb88d9c7fe89c60c5215f', + { + mintAddress: + '98fe506a37c46d67b7212ec689decd6fcd7137ea751fb88d9c7fe89c60c5215f', + }, + ], + ]), + 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/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/apps/wallet/src/utils/widget.ts b/apps/wallet/src/utils/widget.ts new file mode 100644 index 000000000..13174cb34 --- /dev/null +++ b/apps/wallet/src/utils/widget.ts @@ -0,0 +1,148 @@ +import type { + CustomWalletMetadata, + SolanaToken, + SuiToken, +} from '@walless/core'; +import { Networks } from '@walless/core'; +import type { TokenDocument, WidgetDocument } from '@walless/store'; +import { nftState, tokenState } from 'state/assets'; + +export type WidgetFilter = (widget: WidgetDocument) => 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 id; +}; + +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 isInNetwork && isOwnedByAddress && isAvailable; + }); + + return nfts; +}; + +export const filterByOwnedTokens = (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 filteredTokens; +}; + +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); + + return ( + requiredToken?.amount !== undefined && + (token as TokenDocument).balance >= requiredToken?.amount + ); + }); + + return filteredTokens.length > 0; +}; + +export const filterByOwnedNfts = (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 filteredNfts; +}; + +export const explorerFilterByOwnedNfts = (widget: WidgetDocument) => { + return filterByOwnedNfts(widget).length > 0; +}; + +const getSolanaMintAddress = (mint: string) => { + if (mint === solMint) { + return wrappedSolMint; + } + + return mint; +}; + +export const filterMap: Record = { + samo: [explorerFilterByTokenBalances, explorerFilterByOwnedNfts], +}; diff --git a/packages/core/utils/widget.ts b/packages/core/utils/widget.ts index 31b35d919..8466b0244 100644 --- a/packages/core/utils/widget.ts +++ b/packages/core/utils/widget.ts @@ -1,8 +1,10 @@ +import type { TabItemStyle } from '@walless/gui'; + import type { Networks } from './common'; export interface WidgetStoreOptions { iconUri: string; - iconSize: number; + iconSize?: number; iconColor?: string; iconActiveColor?: string; coverUri: string; @@ -11,7 +13,7 @@ export interface WidgetStoreOptions { activeCount: number; } -export interface WidgetNetworkOptions { +export interface WidgetNetworkMetadata { backgroundUri: string; markUri: string; iconUri: string; @@ -19,19 +21,63 @@ export interface WidgetNetworkOptions { iconColor: string; } -export enum WidgetType { +export interface CustomWalletAdvertisement { + title: string; + link: string; + image: string; +} + +export interface CustomWalletAssets { + mintAddress: string; + amount?: number; +} + +export interface CustomWalletMetadata { + coverBanner: string; + iconSrc: string; + backgroundColor: string; + actionButtonBackgroundColors: { + send: string; + receive: string; + buy: string; + swap: string; + }; + activeTabStyle?: TabItemStyle; + advertisements: CustomWalletAdvertisement[]; + tokens?: Map; + nfts?: Map; + network: Networks; +} + +export enum WidgetCategories { NETWORK = 'Network', GAME = 'Game', - DEFI = 'DeFi', - NFT = 'NFT', + COMMUNITY = 'Community', } +export enum WidgetSubcategories { + CUSTOM_WALLET = 'Custom Wallet', + NETWORK = 'Network', + GAME = 'Game', +} + +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; - widgetType: WidgetType; + category: WidgetSubcategories; storeMeta: WidgetStoreOptions; - networkMeta: WidgetNetworkOptions; + metadata?: CustomMetadata; } diff --git a/packages/gui/components/SliderTabs/TabItem.tsx b/packages/gui/components/SliderTabs/TabItem.tsx index f7dba3d10..c00141fe3 100644 --- a/packages/gui/components/SliderTabs/TabItem.tsx +++ b/packages/gui/components/SliderTabs/TabItem.tsx @@ -1,11 +1,56 @@ 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 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 TabItemStyle { - containerStyle: ViewStyle; - textStyle: TextStyle; + style?: ViewStyle; + linearGradient?: { + direction: GradientDirection; + colors: string[]; + }; + textStyle?: TextStyle; } export interface TabAble { @@ -15,25 +60,41 @@ export interface TabAble { interface Props { item: TabAble; - style?: TabItemStyle; + tabStyle?: TabItemStyle; onPress?: (item: TabAble) => void; } -export const TabItem: FC = ({ item, style, onPress }) => { +export const TabItem: FC = ({ item, tabStyle, onPress }) => { + const containerStyle = tabStyle?.style; + const linearGradientStyle = tabStyle?.linearGradient; + + if (linearGradientStyle) { + return ( + onPress?.(item)}> + + {item.title} + + + ); + } + return ( onPress?.(item)} > - {item.title} + {item.title} ); }; export const activatedStyle: TabItemStyle = { - containerStyle: { - backgroundColor: '#0694D3', - }, + style: { backgroundColor: '#0694D3' }, textStyle: { color: 'white', fontWeight: '500', @@ -41,9 +102,7 @@ export const activatedStyle: TabItemStyle = { }; export const deactivatedStyle: TabItemStyle = { - containerStyle: { - backgroundColor: 'transparent', - }, + style: { backgroundColor: 'transparent' }, textStyle: { color: '#566674', fontWeight: '400', @@ -53,8 +112,10 @@ export const deactivatedStyle: TabItemStyle = { export default TabItem; const styles = StyleSheet.create({ - container: { + hoverable: { flex: 1, + }, + container: { paddingVertical: 10, borderRadius: 8, }, 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 (