diff --git a/.gitignore b/.gitignore index 896181c0f..1b09fcd08 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,5 @@ cli/tokens/**/cache.json cli/tokens/**/config.json cli/tokens/**/sugar.log -playground \ No newline at end of file +playground +.nvmrc diff --git a/apps/landing/components/layouts/Home/Navigation/LaunchingModal.tsx b/apps/landing/components/layouts/Home/Navigation/LaunchingModal.tsx index ba884e2e2..11d15f4c8 100644 --- a/apps/landing/components/layouts/Home/Navigation/LaunchingModal.tsx +++ b/apps/landing/components/layouts/Home/Navigation/LaunchingModal.tsx @@ -46,7 +46,7 @@ const styles = StyleSheet.create({ }, button: { marginHorizontal: 8, - alignSelf: 'flex-start', + alignSelf: 'flex-end', }, line: { backgroundColor: '#ffffff', diff --git a/apps/landing/components/layouts/Home/Navigation/index.tsx b/apps/landing/components/layouts/Home/Navigation/index.tsx index 9e03634f8..50a61c527 100644 --- a/apps/landing/components/layouts/Home/Navigation/index.tsx +++ b/apps/landing/components/layouts/Home/Navigation/index.tsx @@ -23,12 +23,13 @@ export const HomeNavigation: FC = () => { const modalRef = useRef(null); const temporarilyDisabled = false; + const handleShowLaunchingModal = () => { modalActions.show({ id: 'launching', fullWidth: false, bindingRef: modalRef, - bindingDirection: BindDirections.InnerTopLeft, + bindingDirection: BindDirections.InnerTopRight, animateDirection: AnimateDirections.Inner, component: LaunchingModal, }); 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 69% rename from apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/Item.tsx rename to apps/wallet/src/components/TokenList/Item.tsx index 85a78087d..3107e164f 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/Item.tsx +++ b/apps/wallet/src/components/TokenList/Item.tsx @@ -1,6 +1,7 @@ import type { FC } from 'react'; import type { StyleProp, ViewStyle } from 'react-native'; import { Image, StyleSheet } from 'react-native'; +import type { TokenPnL } from '@walless/core'; import { Hoverable, Text, View } from '@walless/gui'; import type { TokenDocument } from '@walless/store'; import assets from 'utils/assets'; @@ -10,22 +11,33 @@ interface Props { style?: StyleProp; token: TokenDocument; onPress?: () => void; + tokenPnL?: TokenPnL; } -export const TokenItem: FC = ({ style, token, onPress }) => { +export const TokenItem: FC = ({ style, token, onPress, tokenPnL }) => { + const pnl = tokenPnL?.priceChangePercentage24H ?? 0; const { symbol, image, quotes, balance } = token; const unitQuote = quotes?.usd; const totalQuote = unitQuote && unitQuote * balance; + const iconSource = image ? { uri: image } : assets.misc.unknownToken; + const fixedPnL = Math.round(pnl * 10000) / 10000; const itemName = symbol || 'Unknown'; + const isLost = fixedPnL < 0; + const isProfit = fixedPnL > 0; return ( {itemName} - {formatQuote(unitQuote)} + + {formatQuote(unitQuote)} + + {isLost ? `-${-fixedPnL}` : isProfit ? `+${fixedPnL}` : null} + + {balance} @@ -75,4 +87,18 @@ const styles = StyleSheet.create({ color: '#566674', fontSize: 13, }, + unitQuoteContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + profitText: { + color: '#60C591', + fontSize: 10, + alignSelf: 'flex-end', + }, + lostText: { + color: '#AE3939', + fontSize: 10, + }, }); 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 92% rename from apps/wallet/src/features/Widget/BuiltInNetwork/TokenList/index.tsx rename to apps/wallet/src/components/TokenList/index.tsx index 37038bd3e..38a98e0b4 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,9 +39,10 @@ export const TokenList = ({ style={[ itemStyle, index === 0 && styles.firstItem, - index === items.length - 1 && styles.lastItem, + index === tokens.length - 1 && styles.lastItem, ]} onPress={handlePressItem} + tokenPnL={item.pnl} /> ); }; @@ -52,7 +53,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/TotalPnL.tsx b/apps/wallet/src/components/TotalPnL.tsx new file mode 100644 index 000000000..1e0d02a23 --- /dev/null +++ b/apps/wallet/src/components/TotalPnL.tsx @@ -0,0 +1,73 @@ +import type { FC } from 'react'; +import { StyleSheet } from 'react-native'; +import { Text, View } from '@walless/gui'; + +interface Props { + value: number; + percentage: number; + isDarkTheme: boolean; +} + +const TotalPnL: FC = ({ value, percentage, isDarkTheme = false }) => { + const isLost = value < 0; + const isProfit = value > 0; + + return ( + + + {isLost ? `≈ -$${-value}` : isProfit ? `≈ +$${value}` : null} + + + + {isLost ? `${percentage}%` : isProfit ? `+${percentage}%` : null} + + + + ); +}; + +export default TotalPnL; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + pnlTextBase: { + color: '#ffffff', + }, + pnlValueBase: { + fontSize: 20, + }, + darkThemePnLText: { + color: '#babdc0', + }, + lightThemePnLText: { + color: '#ffffff', + }, + percentageContainerBase: { + borderRadius: 4, + paddingVertical: 4, + paddingHorizontal: 8, + }, + ProfitPercentageContainer: { + backgroundColor: '#29985F', + }, + LostPercentageContainer: { + backgroundColor: '#DB1901', + }, +}); 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/engine/runners/solana/subscription.ts b/apps/wallet/src/engine/runners/solana/subscription.ts index 5e14a4b33..35544e5f2 100644 --- a/apps/wallet/src/engine/runners/solana/subscription.ts +++ b/apps/wallet/src/engine/runners/solana/subscription.ts @@ -230,6 +230,8 @@ const handleInitAccountOnLogsChange = async ( const quotes = await getTokenQuotes([ { address: token.mint, network: token.network }, ]); + token.pnl = + quotes[makeHashId({ address: token.mint, network: token.network })].pnl; token.quotes = quotes[ makeHashId({ address: token.mint, network: token.network }) diff --git a/apps/wallet/src/engine/runners/solana/tokens.ts b/apps/wallet/src/engine/runners/solana/tokens.ts index e3e3619b6..7d91e8b22 100644 --- a/apps/wallet/src/engine/runners/solana/tokens.ts +++ b/apps/wallet/src/engine/runners/solana/tokens.ts @@ -4,7 +4,7 @@ import { LAMPORTS_PER_SOL } from '@solana/web3.js'; import type { NetworkCluster, SolanaToken } from '@walless/core'; import { Networks } from '@walless/core'; import type { TokenDocument } from '@walless/store'; -import { getTokenQuote } from 'utils/api'; +import { getTokenQuote as getTokenInfo } from 'utils/api'; import { solMint, wrappedSolMint } from 'utils/constants'; import { addTokenToStorage } from 'utils/storage'; @@ -23,11 +23,12 @@ export const queryTokens = async ( cluster, wallet, ).then(async (doc) => { - const quotes = await getTokenQuote({ + const tokenInfo = await getTokenInfo({ address: wrappedSolMint, network: doc.network, }); - doc.quotes = quotes?.quotes; + doc.quotes = tokenInfo?.quotes; + doc.pnl = tokenInfo?.pnl; await addTokenToStorage(doc); return doc; @@ -42,11 +43,12 @@ export const queryTokens = async ( account, ); - const quotes = await getTokenQuote({ + const tokenInfo = await getTokenInfo({ address: doc.mint, network: doc.network, }); - doc.quotes = quotes?.quotes; + doc.quotes = tokenInfo?.quotes; + doc.pnl = tokenInfo?.pnl; await addTokenToStorage(doc); return doc; diff --git a/apps/wallet/src/features/Explorer/Header.tsx b/apps/wallet/src/features/Explorer/Header.tsx index d2f37049c..a7a6cf665 100644 --- a/apps/wallet/src/features/Explorer/Header.tsx +++ b/apps/wallet/src/features/Explorer/Header.tsx @@ -2,12 +2,11 @@ import type { FC } from 'react'; import type { ViewStyle } from 'react-native'; import { StyleSheet } from 'react-native'; import { Hoverable, Text, View } from '@walless/gui'; -import { Eye, EyeOff, Settings } from '@walless/icons'; +import { Eye, EyeOff } from '@walless/icons'; import { appState } from 'state/app'; import { setPrivacy } from 'state/runtime/config'; import { getValuationDisplay } from 'utils/helper'; import { useTokens } from 'utils/hooks'; -import { navigate } from 'utils/navigation'; import { useSnapshot } from 'valtio'; interface Props { @@ -18,20 +17,13 @@ const Header: FC = ({ style }) => { const { config } = useSnapshot(appState); const { valuation } = useTokens(); - const handleNavigateToSettings = () => { - navigate('Dashboard', { - screen: 'Explore', - params: { screen: 'Profile', params: { screen: 'Setting' } }, - }); - }; - return ( Total balance { setPrivacy(!config.hideBalance); }} @@ -55,10 +47,6 @@ const Header: FC = ({ style }) => { - - - - {/* This button will be implemented in the next task, so I keep it here for now in the comment */} {/* diff --git a/apps/wallet/src/features/Explorer/Highlights/CardCarousel.tsx b/apps/wallet/src/features/Explorer/Highlights/CardCarousel.tsx index e0ae55c5f..8a0e38215 100644 --- a/apps/wallet/src/features/Explorer/Highlights/CardCarousel.tsx +++ b/apps/wallet/src/features/Explorer/Highlights/CardCarousel.tsx @@ -60,8 +60,16 @@ const CardCarousel: FC = ({ gestureStateManager.current = stateManager; }); + const hover = Gesture.Hover() + .onStart(() => { + pressed.current = true; + }) + .onFinalize(() => { + pressed.current = false; + }); + useEffect(() => { - const timer = setTimeout(() => { + const timer = setInterval(() => { if (pressed.current) return; if (currentIndex == widgets.length - 1) { autoSwipeDirection.current = -1; @@ -70,10 +78,10 @@ const CardCarousel: FC = ({ } onChangeCurrentIndex(currentIndex + autoSwipeDirection.current); - }, 2000); + }, 4000); - return () => clearTimeout(timer); - }, [currentIndex, pressed]); + return () => clearInterval(timer); + }, [currentIndex]); // manually end gesture when having any mouse up on web useEffect(() => { @@ -92,20 +100,22 @@ const CardCarousel: FC = ({ return ( - - {widgets.map((card, index) => { - return ( - - ); - })} - + + + {widgets.map((card, index) => { + return ( + + ); + })} + + ); }; 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..60747d9d3 100644 --- a/apps/wallet/src/features/Explorer/index.tsx +++ b/apps/wallet/src/features/Explorer/index.tsx @@ -1,8 +1,11 @@ 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 { useNfts, useTokens, useWidgets } from 'utils/hooks'; +import { filterMap } from 'utils/widget'; import Header from './Header'; import Highlights from './Highlights'; @@ -18,14 +21,31 @@ interface Props { } export const ExplorerFeature: FC = ({ style }) => { + const { tokens } = useTokens(); + const { nfts } = useNfts(); + const widgets = useWidgets({ filterAdded: false }); + + const filteredWidgets = useMemo( + () => + widgets.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/Home/TokenValue.tsx b/apps/wallet/src/features/Home/TokenValue.tsx index 74c946ff1..4a9b629f3 100644 --- a/apps/wallet/src/features/Home/TokenValue.tsx +++ b/apps/wallet/src/features/Home/TokenValue.tsx @@ -2,7 +2,8 @@ import type { FC } from 'react'; import { StyleSheet, View } from 'react-native'; import { Hoverable, Text } from '@walless/gui'; import { Eye, EyeOff } from '@walless/icons'; -import { useSettings } from 'utils/hooks'; +import TotalPnL from 'components/TotalPnL'; +import { useSettings, useTokens } from 'utils/hooks'; interface Props { value: number; @@ -10,29 +11,34 @@ interface Props { const TokenValue: FC = ({ value }) => { const { setting, setPrivacy } = useSettings(); - const balanceTextStyle = [ - styles.balanceText, - setting.hideBalance && styles.protectedBalance, - ]; + const { valuation, pnl } = useTokens(); const handleToggleTokenValue = async () => { setPrivacy(!setting.hideBalance); }; + const pnlRates = (pnl / (valuation != 0 ? valuation : 1)) * 100; return ( Token value - - - {setting.hideBalance ? '****' : '$' + value.toFixed(2)} - - - {setting.hideBalance ? ( - - ) : ( - - )} - + + + + {setting.hideBalance ? '****' : '$' + value.toFixed(2)} + + + {setting.hideBalance ? ( + + ) : ( + + )} + + + ); @@ -53,14 +59,13 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', gap: 12, - minHeight: 84, }, balanceText: { color: '#FFFFFF', fontSize: 40, fontWeight: '500', }, - protectedBalance: { - paddingTop: 16, + balanceAndPercentageContainer: { + alignItems: 'center', }, }); 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/WalletCard/Balance.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/WalletCard/Balance.tsx index 70985c880..3cb45ba46 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/WalletCard/Balance.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/WalletCard/Balance.tsx @@ -2,23 +2,27 @@ import type { FC } from 'react'; import { StyleSheet } from 'react-native'; import { Hoverable, Text, View } from '@walless/gui'; import { Eye, EyeOff } from '@walless/icons'; +import TotalPnL from 'components/TotalPnL'; import { getValuationDisplay } from 'utils/helper'; interface Props { onHide: (next: boolean) => void; hideBalance: boolean; valuation?: number; + pnl?: number; } export const WalletBalance: FC = ({ onHide, hideBalance, valuation = 0, + pnl = 0, }) => { const balanceTextStyle = [ styles.balanceText, hideBalance && styles.protectedBalance, ]; + const pnlRates = (pnl / (valuation != 0 ? valuation : 1)) * 100; return ( @@ -30,6 +34,13 @@ export const WalletBalance: FC = ({ {getValuationDisplay(valuation, hideBalance)} + + + ); }; @@ -44,7 +55,6 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', paddingLeft: 5, - paddingBottom: 20, gap: 10, }, balanceText: { @@ -60,4 +70,7 @@ const styles = StyleSheet.create({ opacity: 0.6, marginLeft: 34, }, + pnLContainer: { + paddingLeft: 10, + }, }); diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/WalletCard/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/WalletCard/index.tsx index a1bbc4777..2d80a4a12 100644 --- a/apps/wallet/src/features/Widget/BuiltInNetwork/WalletCard/index.tsx +++ b/apps/wallet/src/features/Widget/BuiltInNetwork/WalletCard/index.tsx @@ -15,6 +15,7 @@ interface Props { item: PublicKeyDocument; skin: CardSkin; valuation?: number; + pnl?: number; hideBalance: boolean; onCopyAddress?: (value: string) => void; onChangePrivateSetting?: (value: boolean) => void; @@ -26,6 +27,7 @@ export const WalletCard: FC = ({ item, skin, valuation = 0, + pnl = 0, hideBalance, onCopyAddress, onChangePrivateSetting, @@ -58,6 +60,7 @@ export const WalletCard: FC = ({ hideBalance={hideBalance} valuation={valuation} onHide={handleHide} + pnl={pnl} /> {skin.largeIconSrc && ( diff --git a/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx b/apps/wallet/src/features/Widget/BuiltInNetwork/index.tsx index bd68650f0..bd160471c 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, pnl } = 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', @@ -127,6 +126,7 @@ export const BuiltInNetwork: FC = ({ id }) => { index={index} item={item} valuation={valuation} + pnl={pnl} skin={cardSkin} hideBalance={false} width={headerLayout.width} @@ -178,4 +178,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..496090213 --- /dev/null +++ b/apps/wallet/src/features/Widget/CustomWalletLayout/index.tsx @@ -0,0 +1,222 @@ +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 { + getTokenValue, + useOpacityAnimated, + usePublicKeys, + useWidgets, +} 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 = useWidgets().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/FirstTimePopup/BlueCircleBackground.tsx b/apps/wallet/src/modals/FirstTimePopup/BlueCircleBackground.tsx new file mode 100644 index 000000000..a85f78188 --- /dev/null +++ b/apps/wallet/src/modals/FirstTimePopup/BlueCircleBackground.tsx @@ -0,0 +1,36 @@ +import { Defs, G, Svg } from 'react-native-svg'; + +export const BlueCircleBackground = () => { + return ( + + + + + + + + + + + + + ); +}; + +export default BlueCircleBackground; diff --git a/apps/wallet/src/modals/FirstTimePopup/PixeverseCard.tsx b/apps/wallet/src/modals/FirstTimePopup/PixeverseCard.tsx new file mode 100644 index 000000000..2b667a36f --- /dev/null +++ b/apps/wallet/src/modals/FirstTimePopup/PixeverseCard.tsx @@ -0,0 +1,92 @@ +import type { FC } from 'react'; +import { Image, StyleSheet, View } from 'react-native'; +import { Text } from '@walless/gui'; +import type { WidgetDocument } from '@walless/store'; + +interface Props { + widget: WidgetDocument; +} + +const PixeverseCard: FC = ({ widget }) => { + return ( + + + + + + + + Pixeverse + + + {widget.storeMeta.description} + + + + Add + + + + ); +}; + +export default PixeverseCard; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + backgroundColor: '#182027', + paddingHorizontal: 12, + paddingVertical: 12, + borderRadius: 12, + gap: 12, + }, + coverImage: { + width: 112, + height: 80, + borderRadius: 8, + }, + iconImage: { + position: 'absolute', + bottom: 4, + right: 4, + width: 28, + height: 28, + borderRadius: 4, + }, + infoContainer: { + flex: 1, + justifyContent: 'space-between', + }, + title: { + color: '#ffffff', + fontWeight: '500', + }, + description: { + fontSize: 10, + color: '#566674', + }, + addButton: { + backgroundColor: '#198CCA', + paddingHorizontal: 16, + paddingVertical: 4, + borderColor: '#17A3E1', + borderWidth: 1, + borderRadius: 8, + width: 'fit-content', + }, + buttonText: { + color: '#ffffff', + fontWeight: '500', + }, +}); diff --git a/apps/wallet/src/modals/FirstTimePopup/index.tsx b/apps/wallet/src/modals/FirstTimePopup/index.tsx new file mode 100644 index 000000000..51fe959a9 --- /dev/null +++ b/apps/wallet/src/modals/FirstTimePopup/index.tsx @@ -0,0 +1,120 @@ +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { modalActions, Text } from '@walless/gui'; +import { ModalId } from 'modals/types'; +import { mockWidgets } from 'state/widget'; +import { navigate } from 'utils/navigation'; +import { addWidgetToStorage } from 'utils/storage'; + +import BlueCircleBackground from './BlueCircleBackground'; +import PixeverseCard from './PixeverseCard'; + +const FirstTimePopup = () => { + const pixeverseWidget = mockWidgets.find((item) => item._id === 'pixeverse'); + + const handleAddPixeverse = () => { + if (!pixeverseWidget) return; + + addWidgetToStorage('pixeverse', pixeverseWidget); + navigate('Dashboard', { + screen: 'Explore', + params: { + screen: 'Widget', + params: { + id: 'pixeverse', + }, + }, + }); + modalActions.destroy(ModalId.FirstTimePopup); + }; + + return ( + + + + + + + {pixeverseWidget && } + + + + Welcome to Walless + + + + Lets get you set up with a brand new way of Web3 wallet. From the + Explorer tab, you can find the best of web3: gaming, multi-chain + wallets, trading tools,… + + + To start earning daily tokens, add the in-wallet game PIXEVERSE and + the wallet widget. + + + + + Add Pixeverse + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + maxWidth: 374, + borderRadius: 16, + overflow: 'hidden', + }, + upperPart: { + backgroundColor: '#031821', + paddingHorizontal: 52, + paddingVertical: 36, + alignItems: 'center', + }, + backgroundImage: { + position: 'absolute', + top: 0, + }, + addButton: { + backgroundColor: '#198CCA', + paddingHorizontal: 16, + paddingVertical: 4, + borderColor: '#17A3E1', + borderWidth: 1, + borderRadius: 8, + width: 'fit-content', + }, + buttonText: { + color: '#ffffff', + fontWeight: '500', + }, + lowerPart: { + gap: 20, + backgroundColor: '#222F37', + paddingHorizontal: 24, + paddingVertical: 24, + alignItems: 'center', + }, + title: { + color: '#ffffff', + fontSize: 24, + fontWeight: '500', + }, + textContainer: { + gap: 12, + }, + text: { + color: '#ffffff', + }, +}); + +export default FirstTimePopup; + +export const showFirstTimePopup = () => { + modalActions.show({ + id: ModalId.FirstTimePopup, + component: FirstTimePopup, + fullWidth: false, + }); +}; 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/modals/types.ts b/apps/wallet/src/modals/types.ts index e45606070..b5f0504eb 100644 --- a/apps/wallet/src/modals/types.ts +++ b/apps/wallet/src/modals/types.ts @@ -12,4 +12,5 @@ export enum ModalId { ReferralLeaderBoard = 'ReferralLeaderBoard', LoyaltyPartnerQuest = 'LoyaltyPartnerQuest', LoyaltyHistory = 'LoyaltyHistory', + FirstTimePopup = 'FirstTimePopup', } diff --git a/apps/wallet/src/stacks/Dashboard/index.tsx b/apps/wallet/src/stacks/Dashboard/index.tsx index 1e3fd0d1d..bf61ff7f1 100644 --- a/apps/wallet/src/stacks/Dashboard/index.tsx +++ b/apps/wallet/src/stacks/Dashboard/index.tsx @@ -1,11 +1,21 @@ +import { useEffect } from 'react'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { runtime } from '@walless/core'; +import type { ShowFirstTimeUserPopupDocument } from '@walless/store'; +import { showFirstTimePopup } from 'modals/FirstTimePopup'; import BrowserScreen from 'screens/Dashboard/Browser'; import HomeStack from 'stacks/Home'; import SettingStack from 'stacks/Setting'; +import { appState } from 'state/app'; +import { mockWidgets } from 'state/widget'; import { noHeaderNavigation } from 'utils/constants'; -import { useNotificationPermissionRequest } from 'utils/hooks'; +import { + useNotificationPermissionRequest, + useSnapshot, + useWidgets, +} from 'utils/hooks'; import type { DashboardParamList } from 'utils/navigation'; +import { storage } from 'utils/storage'; import ExplorerStack from '../Explorer'; @@ -15,6 +25,24 @@ const Tab = createBottomTabNavigator(); export const DashboardStack = () => { useNotificationPermissionRequest(); + const { showFirstTimePopup: showPopup } = useSnapshot(appState); + const widgets = useWidgets(); + + useEffect(() => { + const alreadyHavePixeverse = widgets.some( + (widget) => widget._id === mockWidgets[0]._id, + ); + + if (showPopup && !alreadyHavePixeverse) { + showFirstTimePopup(); + storage.put({ + _id: 'showFirstTimeUserPopup', + type: 'ShowFirstTimeUserPopup', + value: false, + }); + appState.showFirstTimePopup = false; + } + }, []); return ( = ({ state }) => { }); }; + const handleSettingPress = () => { + navigate('Dashboard', { + screen: 'Explore', + params: { screen: 'Setting', params: { screen: 'Default' } }, + }); + }; + const getIsExtensionActive = (item: WidgetDocument) => { const { routes, index } = state; const isProfileScreen = routes[index].name === 'Profile'; @@ -78,6 +85,7 @@ export const Sidebar: FC = ({ state }) => { onExtensionPress={handleExtensionPress} onRemoveLayout={handleRemoveWidget} onAvatarPress={handleAvatarPress} + onSettingPress={handleSettingPress} /> ); }; 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/stacks/Explorer/WidgetNavigator/index.tsx b/apps/wallet/src/stacks/Explorer/WidgetNavigator/index.tsx index 58c9500b7..0714ee907 100644 --- a/apps/wallet/src/stacks/Explorer/WidgetNavigator/index.tsx +++ b/apps/wallet/src/stacks/Explorer/WidgetNavigator/index.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react'; import type { StyleProp, ViewStyle } from 'react-native'; import { StyleSheet, View } from 'react-native'; import type { UserProfile } from '@walless/core'; -import { Compass } from '@walless/icons'; +import { Compass, Settings } from '@walless/icons'; import type { WidgetDocument } from '@walless/store'; import { showRemoveLayoutModal } from 'modals/RemoveLayout'; import { appState } from 'state/app'; @@ -20,6 +20,7 @@ interface Props { onExtensionPress?: (item: WidgetDocument) => void; onRemoveLayout: (item: WidgetDocument) => void; onAvatarPress?: () => void; + onSettingPress?: () => void; } const orbSize = 40; @@ -33,6 +34,7 @@ export const WidgetNavigator: FC = ({ onExtensionPress, onRemoveLayout, onAvatarPress, + onSettingPress, }) => { const insets = useUniversalInsets(); const containerStyle = { @@ -54,6 +56,14 @@ export const WidgetNavigator: FC = ({ iconSize: orbSize, } as never, }; + + const settingItem: Partial = { + _id: 'setting', + storeMeta: { + iconColor: '#23303C', + iconSize: orbSize, + } as never, + }; const isExplorerActive = getIsExtensionActive?.(exploreItem as never); const handleContextMenu = ( @@ -108,6 +118,15 @@ export const WidgetNavigator: FC = ({ {navigationDisplay.isSidebarAvatarActive && profile?.profileImage && ( + + + + { initialParams={{ screen: 'Default' }} options={options} /> + ({ isBottomTabActive: false, isSidebarAvatarActive: false, }, + showFirstTimePopup: true, isMobileDisplay: false, }); diff --git a/apps/wallet/src/state/bootstrap.ts b/apps/wallet/src/state/bootstrap.ts index 67937dc3d..d57434b7b 100644 --- a/apps/wallet/src/state/bootstrap.ts +++ b/apps/wallet/src/state/bootstrap.ts @@ -8,6 +8,7 @@ import type { PouchDocument, PublicKeyDocument, SettingDocument, + ShowFirstTimeUserPopupDocument, TokenDocument, WidgetDocument, } from '@walless/store'; @@ -44,7 +45,7 @@ import { widgetState } from './widget'; export const bootstrap = async (): Promise => { const startTime = new Date(); - appState.remoteConfig = loadRemoteConfig(); + appState.remoteConfig = await loadRemoteConfig(); await configure(storage); await migrateDatabase(storage, 'app', appMigrations).then(async () => { @@ -71,6 +72,11 @@ export const bootstrap = async (): Promise => { export const launchApp = async (): Promise => { const settings = await storage.safeGet('settings'); + const showFirstTimeUserPopup = + await storage.safeGet( + 'showFirstTimeUserPopup', + ); + appState.showFirstTimePopup = showFirstTimeUserPopup?.value ?? true; const isSignedIn = settings?.profile?.id; if (isSignedIn) { diff --git a/apps/wallet/src/state/widget/shared.ts b/apps/wallet/src/state/widget/shared.ts index f30ec92ce..dc887c570 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, WidgetSubcategories } from '@walless/core'; import type { WidgetDocument } from '@walless/store'; // TODO: this mocked data is for web only @@ -9,7 +9,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 +20,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 +34,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 +46,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 +60,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 +72,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 +86,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 +98,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 +112,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 +124,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 +138,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 +149,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 +163,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 +174,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 +199,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 +207,20 @@ 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, + }, + }, ]; diff --git a/apps/wallet/src/utils/api.ts b/apps/wallet/src/utils/api.ts index 23ccd5ddf..1f586eb95 100644 --- a/apps/wallet/src/utils/api.ts +++ b/apps/wallet/src/utils/api.ts @@ -41,3 +41,16 @@ export const getTokenQuote = async (token: IToken) => { console.log('failed to get token quote:', error); } }; + +export const getTokenPnL = async (token: IToken) => { + try { + const response = await qlClient.request< + { tokenByAddress: TokenInfo }, + { address: string } + >(queries.tokenByAddress, { address: makeHashId(token) }); + + return response.tokenByAddress; + } catch (error) { + console.log('failed to get token quote:', error); + } +}; 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/auth/logout.web.ts b/apps/wallet/src/utils/auth/logout.web.ts index 8895033e2..ced8de3af 100644 --- a/apps/wallet/src/utils/auth/logout.web.ts +++ b/apps/wallet/src/utils/auth/logout.web.ts @@ -5,9 +5,11 @@ import { appActions } from 'state/app'; import { auth } from '../firebase/index.web'; import { storage } from '../storage'; +export const whitelist = ['showFirstTimeUserPopup']; + export const logout = async () => { await signOut(auth()); await engine.clear(); - await storage.clearAllDocs(); + await storage.clearAllDocs(whitelist); appActions.cleanupAfterLogOut(); }; diff --git a/apps/wallet/src/utils/constants.ts b/apps/wallet/src/utils/constants.ts index 98177309f..b2e45cd4e 100644 --- a/apps/wallet/src/utils/constants.ts +++ b/apps/wallet/src/utils/constants.ts @@ -1,4 +1,6 @@ import type { Config, RemoteConfig } from '@walless/core'; +import { Networks } from '@walless/core'; +import { gradientDirection } from '@walless/gui'; export const noHeaderNavigation = { headerShown: false, @@ -15,6 +17,68 @@ export const defaultRemoteConfig: RemoteConfig = { experimentalEnabled: true, deepAnalyticsEnabled: true, minimalVersion: '1.0.0', + customWallets: { + samo: { + 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', + }, + ], + network: Networks.solana, + tokens: { + '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU': { + mintAddress: '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU', + amount: 50000, + }, + So11111111111111111111111111111111111111112: { + mintAddress: 'So11111111111111111111111111111111111111112', + }, + }, + nfts: { + '98fe506a37c46d67b7212ec689decd6fcd7137ea751fb88d9c7fe89c60c5215f': { + mintAddress: + '98fe506a37c46d67b7212ec689decd6fcd7137ea751fb88d9c7fe89c60c5215f', + }, + }, + whitelist: ['thongqtran2@gmail.com'], + }, + }, }; /** diff --git a/apps/wallet/src/utils/firebase/index.web.ts b/apps/wallet/src/utils/firebase/index.web.ts index 690f49650..548b2ad67 100644 --- a/apps/wallet/src/utils/firebase/index.web.ts +++ b/apps/wallet/src/utils/firebase/index.web.ts @@ -1,11 +1,10 @@ import { getAnalytics, logEvent } from '@firebase/analytics'; import { - activate, - fetchConfig, + fetchAndActivate, getAll, getRemoteConfig, } from '@firebase/remote-config'; -import type { RemoteConfig } from '@walless/core'; +import type { CustomWalletMetadata, RemoteConfig } from '@walless/core'; import { defaultRemoteConfig } from 'utils/constants'; import { app } from './index.ext'; @@ -18,15 +17,21 @@ export const remoteConfig = getRemoteConfig(app); remoteConfig.settings.minimumFetchIntervalMillis = __DEV__ ? 10000 : 3600000; remoteConfig.defaultConfig = defaultRemoteConfig as never; -export const loadRemoteConfig = (): RemoteConfig => { - activate(remoteConfig); - fetchConfig(remoteConfig); +export const loadRemoteConfig = async (): Promise => { + await fetchAndActivate(remoteConfig); const allConfig = getAll(remoteConfig); + const customWalletsString = allConfig.customWallets?.asString(); + const customWallets = JSON.parse(customWalletsString) as Record< + string, + CustomWalletMetadata + >; + return { experimentalEnabled: allConfig.experimentalEnabled?.asBoolean(), deepAnalyticsEnabled: allConfig.deepAnalyticsEnabled?.asBoolean(), minimalVersion: allConfig.minimalVersion?.asString() || '1.0.0', + customWallets: customWallets, }; }; diff --git a/apps/wallet/src/utils/hooks/wallet.ts b/apps/wallet/src/utils/hooks/wallet.ts index de5d4b5ab..1630ad20c 100644 --- a/apps/wallet/src/utils/hooks/wallet.ts +++ b/apps/wallet/src/utils/hooks/wallet.ts @@ -48,16 +48,24 @@ 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; return quote * balance; }; +const getTokenPnLValue = (token: TokenDocument, totalValue: number) => { + const { pnl } = token; + const totalPnL = pnl?.priceChangePercentage24H || 0; + + return (totalPnL / 100) * totalValue; +}; + type TokenResult = { tokens: TokenDocument[]; valuation: number; + pnl: number; }; export const useTokens = ( @@ -75,6 +83,7 @@ export const useTokens = ( }); let valuation = 0; + let pnl = 0; switch (network) { case Networks.solana: { @@ -85,7 +94,9 @@ export const useTokens = ( const isSol = token.mint === solMint; if (isNetworkValid && (isSol || isAvailable)) { - valuation += getTokenValue(token, currency); + const totalValue = getTokenValue(token, currency); + valuation += totalValue; + pnl += getTokenPnLValue(token, totalValue); filteredTokens.push(token); } } @@ -103,6 +114,7 @@ export const useTokens = ( return { tokens: filteredTokens, valuation, + pnl, }; } case Networks.sui: { @@ -113,7 +125,9 @@ export const useTokens = ( const isSUI = token.coinType === SUI_COIN_TYPE; if (isNetworkValid && (isSUI || isAvailable)) { - valuation += getTokenValue(token, currency); + const totalValue = getTokenValue(token, currency); + valuation += totalValue; + pnl += getTokenPnLValue(token, totalValue); filteredTokens.push(token); } } @@ -131,19 +145,22 @@ export const useTokens = ( return { tokens: filteredTokens, valuation, + pnl, }; } case Networks.tezos: { - return { tokens, valuation }; + return { tokens, valuation, pnl }; } case Networks.aptos: { - return { tokens, valuation }; + return { tokens, valuation, pnl }; } default: { tokens.forEach((token) => { - valuation += getTokenValue(token, currency); + const totalValue = getTokenValue(token, currency); + valuation += totalValue; + pnl += getTokenPnLValue(token, totalValue); }); - return { tokens, valuation }; + return { tokens, valuation, pnl }; } } }, [map, network, address]) as never as TokenResult; diff --git a/apps/wallet/src/utils/hooks/widget.ts b/apps/wallet/src/utils/hooks/widget.ts index e0c77a9f7..1a2f65419 100644 --- a/apps/wallet/src/utils/hooks/widget.ts +++ b/apps/wallet/src/utils/hooks/widget.ts @@ -1,13 +1,38 @@ import { useMemo } from 'react'; +import type { CustomWalletMetadata } from '@walless/core'; +import { WidgetSubcategories } from '@walless/core'; import { sortBy } from 'lodash'; -import { widgetState } from 'state/widget'; +import { appState } from 'state/app'; +import { mockWidgets, widgetState } from 'state/widget'; import { useSnapshot } from './aliased'; -export const useWidgets = () => { +interface Options { + filterAdded?: boolean; +} + +export const useWidgets = (option?: Options) => { const { map } = useSnapshot(widgetState); + const { remoteConfig } = useSnapshot(appState); return useMemo(() => { + if (option) { + const { filterAdded } = option; + if (!filterAdded) { + const widgets = mockWidgets.map((widget) => { + if (widget.category === WidgetSubcategories.CUSTOM_WALLET) { + widget.metadata = remoteConfig.customWallets?.[ + widget._id + ] as CustomWalletMetadata; + } + + return widget; + }); + + return widgets; + } + } + const widgets = Array.from(map.values()); return sortBy(widgets, 'timestamp'); }, [map]); diff --git a/apps/wallet/src/utils/navigation/index.ts b/apps/wallet/src/utils/navigation/index.ts index c74d4cb20..f73ebe4d3 100644 --- a/apps/wallet/src/utils/navigation/index.ts +++ b/apps/wallet/src/utils/navigation/index.ts @@ -52,7 +52,13 @@ export const linking: LinkingOptions = { path: '/explore', screens: { Widget: '/widget/:id', - Setting: '/setting', + Setting: { + path: '/setting', + screens: { + Default: '/', + Referral: '/referral', + }, + }, Collection: { path: '/collection', screens: { diff --git a/apps/wallet/src/utils/navigation/types.ts b/apps/wallet/src/utils/navigation/types.ts index b3191d43f..e1cd9a85e 100644 --- a/apps/wallet/src/utils/navigation/types.ts +++ b/apps/wallet/src/utils/navigation/types.ts @@ -28,6 +28,7 @@ export type ExploreParamList = { Widget: { id: string }; Collection: NavigatorScreenParams; Profile: NavigatorScreenParams; + Setting: NavigatorScreenParams; Loyalty: undefined; }; diff --git a/apps/wallet/src/utils/widget.ts b/apps/wallet/src/utils/widget.ts new file mode 100644 index 000000000..316962623 --- /dev/null +++ b/apps/wallet/src/utils/widget.ts @@ -0,0 +1,159 @@ +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 { appState } from 'state/app'; + +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?.[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?.[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?.[id]; + }); + + return filteredNfts; +}; + +export const explorerFilterByUserWhitelist = (widget: WidgetDocument) => { + const whitelist = (widget.metadata as CustomWalletMetadata).whitelist; + return whitelist.includes(appState.profile.email || ''); +}; + +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, + explorerFilterByUserWhitelist, + ], +}; diff --git a/packages/core/utils/assets.ts b/packages/core/utils/assets.ts index 4f6f14c8e..8d5019b80 100644 --- a/packages/core/utils/assets.ts +++ b/packages/core/utils/assets.ts @@ -30,8 +30,16 @@ export type Token = { owner: string; balance: number; quotes?: Record; + pnl?: TokenPnL; } & TokenMetadata; +export type TokenPnL = { + currentPrice: number; + priceChangePercentage24H: number; + priceChangePercentage7d: number; + priceChangePercentage30d: number; +}; + export type TokenMetadata = { name: string; symbol: string; diff --git a/packages/core/utils/entity.ts b/packages/core/utils/entity.ts index 0098808c6..c2595ba9c 100644 --- a/packages/core/utils/entity.ts +++ b/packages/core/utils/entity.ts @@ -1,4 +1,5 @@ import type { NetworkCluster, Networks } from './common'; +import type { CustomWalletMetadata } from './widget'; export interface AptosTokenMetadata { creatorAddress: string; @@ -35,6 +36,7 @@ export interface RemoteConfig { experimentalEnabled: boolean; deepAnalyticsEnabled: boolean; minimalVersion: string; + customWallets?: Record; } export interface UserProfile { diff --git a/packages/core/utils/extension.ts b/packages/core/utils/extension.ts index a54cd632f..3d0cdcc33 100644 --- a/packages/core/utils/extension.ts +++ b/packages/core/utils/extension.ts @@ -27,3 +27,7 @@ export interface ExtensionConfig { storeMeta: ExtensionStoreMetadata; networkMeta: ExtensionNetworkMetadata; } + +export interface ShowFirstTimeUserPopup { + value: boolean; +} diff --git a/packages/core/utils/widget.ts b/packages/core/utils/widget.ts index 31b35d919..26bef5426 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,64 @@ 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?: Record; + nfts?: Record; + network: Networks; + whitelist: string[]; +} + +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/graphql/query/token.ts b/packages/graphql/query/token.ts index aee2309f7..206d8045c 100644 --- a/packages/graphql/query/token.ts +++ b/packages/graphql/query/token.ts @@ -7,6 +7,10 @@ export const tokenById = gql` address name quotes + pnl { + currentPrice + priceChangePercentage24H + } } } `; @@ -18,6 +22,10 @@ export const tokenByAddress = gql` address name quotes + pnl { + currentPrice + priceChangePercentage24H + } } } `; @@ -29,6 +37,10 @@ export const tokensByAddress = gql` address name quotes + pnl { + currentPrice + priceChangePercentage24H + } } } `; diff --git a/packages/graphql/types.ts b/packages/graphql/types.ts index 94afcce6f..905a5adad 100644 --- a/packages/graphql/types.ts +++ b/packages/graphql/types.ts @@ -65,6 +65,20 @@ export type ActionCount = { type?: Maybe; }; +export type ActionInput = { + category: ActionCategory; + cycleInHours?: InputMaybe; + mechanism: VerifyMechanism; + metadata?: InputMaybe>>; + milestone?: InputMaybe; + points: Scalars['Float']['input']; + streak?: InputMaybe; + type: Scalars['String']['input']; + validFrom?: InputMaybe; + validUntil?: InputMaybe; + verifier?: InputMaybe; +}; + export type ActionMetadata = { __typename?: 'ActionMetadata'; key?: Maybe; @@ -93,20 +107,6 @@ export type Boost = { validUntil?: Maybe; }; -export type CreateActionInput = { - category: ActionCategory; - cycleInHours?: InputMaybe; - metadata?: InputMaybe>>; - milestone?: InputMaybe; - points: Scalars['Float']['input']; - streak?: InputMaybe; - type: Scalars['String']['input']; - validFrom?: InputMaybe; - validUntil?: InputMaybe; - verifier?: InputMaybe; - verifyMechanism: VerifyMechanism; -}; - export type Device = { __typename?: 'Device'; appVersion?: Maybe; @@ -183,9 +183,11 @@ export type RootMutation = { claimWalletInvitation?: Maybe; createLoyaltyAction?: Maybe; createLoyaltyBoost?: Maybe; + deleteLoyaltyAction?: Maybe; deleteWidget?: Maybe; deleteWidgetAccount?: Maybe; doLoyaltyAction?: Maybe; + doLoyaltyActionManually?: Maybe; doRecurringThenStreakThenMilestoneActionsByType?: Maybe>>; joinWaitlist?: Maybe; registerAccount?: Maybe; @@ -193,6 +195,7 @@ export type RootMutation = { registerWidgetAccount?: Maybe; sendEmergencyKit?: Maybe; trackAccountWallets?: Maybe; + updateLoyaltyAction?: Maybe; updateWidgetAccountRole?: Maybe; updateWidgetOwner?: Maybe; updateWidgetStatus?: Maybe; @@ -220,7 +223,7 @@ export type RootMutationClaimWalletInvitationArgs = { export type RootMutationCreateLoyaltyActionArgs = { - input: CreateActionInput; + input: ActionInput; }; @@ -233,6 +236,11 @@ export type RootMutationCreateLoyaltyBoostArgs = { }; +export type RootMutationDeleteLoyaltyActionArgs = { + id: Scalars['String']['input']; +}; + + export type RootMutationDeleteWidgetArgs = { id: Scalars['String']['input']; }; @@ -248,6 +256,12 @@ export type RootMutationDoLoyaltyActionArgs = { }; +export type RootMutationDoLoyaltyActionManuallyArgs = { + actionId: Scalars['String']['input']; + email: Scalars['String']['input']; +}; + + export type RootMutationDoRecurringThenStreakThenMilestoneActionsByTypeArgs = { type: Scalars['String']['input']; }; @@ -286,6 +300,12 @@ export type RootMutationTrackAccountWalletsArgs = { }; +export type RootMutationUpdateLoyaltyActionArgs = { + id: Scalars['String']['input']; + input: ActionInput; +}; + + export type RootMutationUpdateWidgetAccountRoleArgs = { id: Scalars['String']['input']; role: WidgetAccountRole; @@ -430,11 +450,21 @@ export type TokenInfo = { id: Scalars['String']['output']; name: Scalars['String']['output']; platforms: Scalars['JSON']['output']; + pnl: TokenPnL; quotes: Scalars['JSON']['output']; symbol: Scalars['String']['output']; timestamp: Scalars['DateTime']['output']; }; +export type TokenPnL = { + __typename?: 'TokenPnL'; + currentPrice: Scalars['Float']['output']; + priceChangePercentage7d: Scalars['Float']['output']; + priceChangePercentage24H: Scalars['Float']['output']; + priceChangePercentage30d: Scalars['Float']['output']; + timestamp: Scalars['DateTime']['output']; +}; + export type TrackAccountWalletInput = { address: Scalars['String']['input']; network?: InputMaybe; @@ -579,12 +609,12 @@ export type ResolversTypes = { Action: ResolverTypeWrapper; ActionCategory: ActionCategory; ActionCount: ResolverTypeWrapper; + ActionInput: ActionInput; ActionMetadata: ResolverTypeWrapper; ActionMetadataInput: ActionMetadataInput; ActionRecord: ResolverTypeWrapper; Boolean: ResolverTypeWrapper; Boost: ResolverTypeWrapper; - CreateActionInput: CreateActionInput; DateTime: ResolverTypeWrapper; Device: ResolverTypeWrapper; DeviceInfoInput: DeviceInfoInput; @@ -606,6 +636,7 @@ export type ResolversTypes = { SystemInfo: ResolverTypeWrapper; Token: ResolverTypeWrapper; TokenInfo: ResolverTypeWrapper; + TokenPnL: ResolverTypeWrapper; TrackAccountWalletInput: TrackAccountWalletInput; Uint32: ResolverTypeWrapper; UserProgress: ResolverTypeWrapper; @@ -623,12 +654,12 @@ export type ResolversParentTypes = { Account: Account; Action: Action; ActionCount: ActionCount; + ActionInput: ActionInput; ActionMetadata: ActionMetadata; ActionMetadataInput: ActionMetadataInput; ActionRecord: ActionRecord; Boolean: Scalars['Boolean']['output']; Boost: Boost; - CreateActionInput: CreateActionInput; DateTime: Scalars['DateTime']['output']; Device: Device; DeviceInfoInput: DeviceInfoInput; @@ -649,6 +680,7 @@ export type ResolversParentTypes = { SystemInfo: SystemInfo; Token: Token; TokenInfo: TokenInfo; + TokenPnL: TokenPnL; TrackAccountWalletInput: TrackAccountWalletInput; Uint32: Scalars['Uint32']['output']; UserProgress: UserProgress; @@ -791,9 +823,11 @@ export type RootMutationResolvers, ParentType, ContextType, RequireFields>; createLoyaltyAction?: Resolver, ParentType, ContextType, RequireFields>; createLoyaltyBoost?: Resolver, ParentType, ContextType, RequireFields>; + deleteLoyaltyAction?: Resolver, ParentType, ContextType, RequireFields>; deleteWidget?: Resolver, ParentType, ContextType, RequireFields>; deleteWidgetAccount?: Resolver, ParentType, ContextType, RequireFields>; doLoyaltyAction?: Resolver, ParentType, ContextType, RequireFields>; + doLoyaltyActionManually?: Resolver, ParentType, ContextType, RequireFields>; doRecurringThenStreakThenMilestoneActionsByType?: Resolver>>, ParentType, ContextType, RequireFields>; joinWaitlist?: Resolver, ParentType, ContextType, RequireFields>; registerAccount?: Resolver, ParentType, ContextType, RequireFields>; @@ -801,6 +835,7 @@ export type RootMutationResolvers, ParentType, ContextType, RequireFields>; sendEmergencyKit?: Resolver, ParentType, ContextType, RequireFields>; trackAccountWallets?: Resolver, ParentType, ContextType, RequireFields>; + updateLoyaltyAction?: Resolver, ParentType, ContextType, RequireFields>; updateWidgetAccountRole?: Resolver, ParentType, ContextType, RequireFields>; updateWidgetOwner?: Resolver, ParentType, ContextType, RequireFields>; updateWidgetStatus?: Resolver, ParentType, ContextType, RequireFields>; @@ -863,12 +898,22 @@ export type TokenInfoResolvers; name?: Resolver; platforms?: Resolver; + pnl?: Resolver; quotes?: Resolver; symbol?: Resolver; timestamp?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; +export type TokenPnLResolvers = { + currentPrice?: Resolver; + priceChangePercentage7d?: Resolver; + priceChangePercentage24H?: Resolver; + priceChangePercentage30d?: Resolver; + timestamp?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export interface Uint32ScalarConfig extends GraphQLScalarTypeConfig { name: 'Uint32'; } @@ -938,6 +983,7 @@ export type Resolvers = { SystemInfo?: SystemInfoResolvers; Token?: TokenResolvers; TokenInfo?: TokenInfoResolvers; + TokenPnL?: TokenPnLResolvers; Uint32?: GraphQLScalarType; UserProgress?: UserProgressResolvers; WalletInvitation?: WalletInvitationResolvers; diff --git a/packages/gui/components/ModalManager/ModalContainer.tsx b/packages/gui/components/ModalManager/ModalContainer.tsx index d90098835..bdb2a2715 100644 --- a/packages/gui/components/ModalManager/ModalContainer.tsx +++ b/packages/gui/components/ModalManager/ModalContainer.tsx @@ -123,6 +123,7 @@ export const ModalContainer: FC = ({ item }) => { }); }); bindingObserver.observe(actualBindingRef.current as never); + bindingObserver.observe(referenceMap.root.current as never); return () => bindingObserver.disconnect(); }, []); 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 ( { + if (whitelist.includes(row.id)) return; + if (persistedIds.indexOf(row.id) === -1) { const doc: { _deleted: boolean } = await this.get(row.id); doc._deleted = true; diff --git a/packages/store/utils/type.ts b/packages/store/utils/type.ts index b97ddf257..0e17d3394 100644 --- a/packages/store/utils/type.ts +++ b/packages/store/utils/type.ts @@ -11,6 +11,7 @@ import type { Nft, PublicKey, Setting, + ShowFirstTimeUserPopup, SolanaSwapHistory, SolanaTransferHistory, SolanaUnknownHistory, @@ -39,7 +40,8 @@ export type DocumentType = | 'TrustedDomain' | 'Widget' | 'Extension' - | 'History'; + | 'History' + | 'ShowFirstTimeUserPopup'; export interface IndexedDocument { type: DocumentType; @@ -69,6 +71,9 @@ export type PublicKeyDocument = export type ExtensionDocument = PouchDocument; +export type ShowFirstTimeUserPopupDocument = + PouchDocument; + export type WidgetDocument = PouchDocument; export type TokenDocumentV1 = PouchDocument; diff --git a/schema.graphql b/schema.graphql index 277c5384a..5d4f33317 100644 --- a/schema.graphql +++ b/schema.graphql @@ -103,90 +103,90 @@ type ActionCount { } """""" -type ActionMetadata { +input ActionInput { """""" - key: String + category: ActionCategory! """""" - value: String -} + cycleInHours: Float = 0 -"""""" -input ActionMetadataInput { """""" - key: String + mechanism: VerifyMechanism! """""" - value: String -} + metadata: [ActionMetadataInput] -"""""" -type ActionRecord { """""" - actionId: String + milestone: Int = 0 """""" - timestamp: DateTime + points: Float! """""" - userId: String -} + streak: Int = 0 -"""""" -type Boost { """""" - actionId: String + type: String! """""" - id: String + validFrom: DateTime = "0001-01-01 00:00:00 +0000 UTC" """""" - multiplier: Float + validUntil: DateTime = "0001-01-01 00:00:00 +0000 UTC" """""" - points: Float + verifier: String = "" +} +"""""" +type ActionMetadata { """""" - validFrom: DateTime + key: String """""" - validUntil: DateTime + value: String } """""" -input CreateActionInput { +input ActionMetadataInput { """""" - category: ActionCategory! + key: String """""" - cycleInHours: Float = 0 + value: String +} +"""""" +type ActionRecord { """""" - metadata: [ActionMetadataInput] + actionId: String """""" - milestone: Int = 0 + timestamp: DateTime """""" - points: Float! + userId: String +} +"""""" +type Boost { """""" - streak: Int = 0 + actionId: String """""" - type: String! + id: String """""" - validFrom: DateTime = "0001-01-01 00:00:00 +0000 UTC" + multiplier: Float """""" - validUntil: DateTime = "0001-01-01 00:00:00 +0000 UTC" + points: Float """""" - verifier: String = "" + validFrom: DateTime """""" - verifyMechanism: VerifyMechanism! + validUntil: DateTime } """ @@ -367,11 +367,14 @@ type RootMutation { claimWalletInvitation(code: String!, email: String!): Boolean """""" - createLoyaltyAction(input: CreateActionInput!): Action + createLoyaltyAction(input: ActionInput!): Action """""" createLoyaltyBoost(actionId: String!, multiplier: Float!, points: Float!, validFrom: DateTime!, validUntil: DateTime!): Boost + """""" + deleteLoyaltyAction(id: String!): Boolean + """""" deleteWidget(id: String!): Boolean @@ -381,6 +384,9 @@ type RootMutation { """""" doLoyaltyAction(actionId: String!): ActionRecord + """""" + doLoyaltyActionManually(actionId: String!, email: String!): ActionRecord + """""" doRecurringThenStreakThenMilestoneActionsByType(type: String!): [ActionRecord] @@ -402,6 +408,9 @@ type RootMutation { """""" trackAccountWallets(wallets: [TrackAccountWalletInput]!): Int + """""" + updateLoyaltyAction(id: String!, input: ActionInput!): Action + """""" updateWidgetAccountRole(id: String!, role: WidgetAccountRole!): WidgetAccount @@ -540,6 +549,9 @@ type TokenInfo { """""" platforms: JSON! + """""" + pnl: TokenPnL! + """""" quotes: JSON! @@ -550,6 +562,24 @@ type TokenInfo { timestamp: DateTime! } +"""""" +type TokenPnL { + """""" + currentPrice: Float! + + """""" + priceChangePercentage7d: Float! + + """""" + priceChangePercentage24H: Float! + + """""" + priceChangePercentage30d: Float! + + """""" + timestamp: DateTime! +} + """""" input TrackAccountWalletInput { """"""