From d763f27a294cd4f7a10a1441f29f663121f385aa Mon Sep 17 00:00:00 2001 From: "Eng. Juan Combetto" Date: Mon, 29 Apr 2024 14:00:28 +0900 Subject: [PATCH 01/16] feat: burn capital, landing page and multi burn WIP --- .../connectWallet/MainConnectWalletButton.tsx | 1 + packages/components/nfts/NFTView.tsx | 56 ++- .../screens/BurnCapital/BurnCapitalScreen.tsx | 106 ++++- .../BurnCapital/components/BurnSideCart.tsx | 399 ++++++++++++++++++ .../BurnCapital/components/BurnableNFTs.tsx | 116 +++++ .../components/TopSectionConnectWallet.tsx | 105 +++++ packages/store/slices/burnCartItems.ts | 60 +++ packages/store/store.ts | 8 +- packages/utils/navigation.ts | 4 +- 9 files changed, 836 insertions(+), 19 deletions(-) create mode 100644 packages/screens/BurnCapital/components/BurnSideCart.tsx create mode 100644 packages/screens/BurnCapital/components/BurnableNFTs.tsx create mode 100644 packages/screens/BurnCapital/components/TopSectionConnectWallet.tsx create mode 100644 packages/store/slices/burnCartItems.ts diff --git a/packages/components/connectWallet/MainConnectWalletButton.tsx b/packages/components/connectWallet/MainConnectWalletButton.tsx index d5856ef99d..d07c668241 100644 --- a/packages/components/connectWallet/MainConnectWalletButton.tsx +++ b/packages/components/connectWallet/MainConnectWalletButton.tsx @@ -16,6 +16,7 @@ export const MainConnectWalletButton: React.FC<{ setIsConnectWalletVisible(true)} /> diff --git a/packages/components/nfts/NFTView.tsx b/packages/components/nfts/NFTView.tsx index 91ec93c1e8..bbf1900993 100644 --- a/packages/components/nfts/NFTView.tsx +++ b/packages/components/nfts/NFTView.tsx @@ -50,6 +50,7 @@ import { SecondaryButton } from "../buttons/SecondaryButton"; import { UserAvatarWithFrame } from "../images/AvatarWithFrame"; import { SpacerColumn, SpacerRow } from "../spacer"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; import { useFeedbacks } from "@/context/FeedbacksProvider"; import { TeritoriNftClient } from "@/contracts-clients/teritori-nft/TeritoriNft.client"; import { popularCollectionsQueryKey } from "@/hooks/marketplace/usePopularCollections"; @@ -59,6 +60,12 @@ import { nftBurnerUserCountQueryKey } from "@/hooks/nft-burner/useNFTBurnerUserC import { collectionStatsQueryKey } from "@/hooks/useCollectionStats"; import { nftsQueryKey } from "@/hooks/useNFTs"; import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; +import { + addSelectedToBurn, + setShowBurnCart, + selectSelectedNFTIds as selectSelectedNFTIdsForBurn, + removeSelectedFromBurn, +} from "@/store/slices/burnCartItems"; // NOTE: we put content in a memoized component to only rerender the container when the window width changes @@ -434,6 +441,23 @@ const NFTViewFooter: React.FC<{ nft: NFT; localSelected: boolean }> = memo( ({ nft, localSelected }) => { const selectedWallet = useSelectedWallet(); const isOwner = nft.ownerId === selectedWallet?.userId; + const dispatch = useAppDispatch(); + + // const burnerFeature = getNetworkFeature( + // "teritori", + // NetworkFeature.CosmWasmNFTsBurner, + // ); + // const { data: authorizedCollections } = + // useNFTBurnerAuthorizedCollections("teritori"); + // const queryClient = useQueryClient(); + + const showRecycle = true; + // !!burnerFeature && + // (authorizedCollections || []).includes(nft.nftContractAddress); + + const selectedForBurn = useSelector(selectSelectedNFTIdsForBurn).includes( + nft.id, + ); return ( = memo( ) : ( - - Not listed - + <> + {showRecycle ? ( + { + dispatch(setShowBurnCart(true)); + if (!selectedForBurn) { + dispatch(addSelectedToBurn(nft)); + } else { + dispatch(removeSelectedFromBurn(nft.id)); + } + }} + text={selectedForBurn ? "Added to the burn 🔥" : "Burnable"} + /> + ) : ( + + Not listed + + )} + )} diff --git a/packages/screens/BurnCapital/BurnCapitalScreen.tsx b/packages/screens/BurnCapital/BurnCapitalScreen.tsx index 05deaa8487..1453216d0f 100644 --- a/packages/screens/BurnCapital/BurnCapitalScreen.tsx +++ b/packages/screens/BurnCapital/BurnCapitalScreen.tsx @@ -1,27 +1,115 @@ +import React, { useState } from "react"; +import { View } from "react-native"; + import { BrandText } from "@/components/BrandText"; +import { OwnedNFTs } from "@/components/OwnedNFTs"; import { ScreenContainer } from "@/components/ScreenContainer"; +import { CollectionContent } from "@/components/collections/CollectionContent"; +import { Tabs } from "@/components/tabs/Tabs"; import { useNFTBurnerTotal } from "@/hooks/nft-burner/useNFTBurnerTotal"; import { useNFTBurnerUserCount } from "@/hooks/nft-burner/useNFTBurnerUserCount"; +import { useIsMobile } from "@/hooks/useIsMobile"; import { useSelectedNetworkId } from "@/hooks/useSelectedNetwork"; import useSelectedWallet from "@/hooks/useSelectedWallet"; import { teritoriNetwork } from "@/networks/teritori"; -import { ScreenFC } from "@/utils/navigation"; +import { BurnSideCart } from "@/screens/BurnCapital/components/BurnSideCart"; +import { BurnableNFTs } from "@/screens/BurnCapital/components/BurnableNFTs"; +import { TopSectionConnectWallet } from "@/screens/BurnCapital/components/TopSectionConnectWallet"; +import { SideCart } from "@/screens/Marketplace/SideCart"; +import { ScreenFC, useAppNavigation } from "@/utils/navigation"; +import { neutral00, neutral33 } from "@/utils/style/colors"; +import { fontSemibold20 } from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; + +const tabs = [ + { id: "burn", name: "Burn NFTs" }, + { id: "leaderboard", name: "Leaderboard" }, +]; export const BurnCapitalScreen: ScreenFC<"BurnCapital"> = ({ route }) => { const inputNetwork = route.params?.network || teritoriNetwork.id; const selectedWallet = useSelectedWallet(); const selectedNetworkId = useSelectedNetworkId(); + const navigation = useAppNavigation(); + const isMobile = useIsMobile(); + + const tabsKeys = Object.keys(tabs); + const [selectedTab, setSelectedTab] = useState(tabsKeys[0]); + const { data: count } = useNFTBurnerUserCount(selectedWallet?.userId); const { data: total } = useNFTBurnerTotal(selectedNetworkId); return ( - - Burn Capital - {typeof total === "number" && ( - Total burned: {total} - )} - {typeof count === "number" && ( - Burned by you: {count} - )} + } + fullWidth + forceNetworkId={inputNetwork} + headerChildren={ + BURN Capital 🔥 + } + responsive + onBackPress={() => navigation.navigate("Marketplace")} + > + + + + + + {typeof total === "number" && ( + Total burned: {total} + )} + {typeof count === "number" && ( + Burned by you: {count} + )} + {/*{nftCollectionToBurn.map((nft) => (*/} + {/* */} + {/*))}*/} + + + + + ); }; diff --git a/packages/screens/BurnCapital/components/BurnSideCart.tsx b/packages/screens/BurnCapital/components/BurnSideCart.tsx new file mode 100644 index 0000000000..67a10bb9d5 --- /dev/null +++ b/packages/screens/BurnCapital/components/BurnSideCart.tsx @@ -0,0 +1,399 @@ +import { toUtf8 } from "@cosmjs/encoding"; +import { EncodeObject } from "@cosmjs/proto-signing"; +import { isDeliverTxFailure } from "@cosmjs/stargate"; +import { EntityId } from "@reduxjs/toolkit"; +import { groupBy } from "lodash"; +import React, { useCallback } from "react"; +import { + FlatList, + Linking, + Pressable, + StyleProp, + View, + ViewStyle, +} from "react-native"; +import { TouchableOpacity } from "react-native-gesture-handler"; +import { TrashIcon } from "react-native-heroicons/outline"; +import { useSelector } from "react-redux"; + +import closeSVG from "@/assets/icons/close.svg"; +import { BrandText } from "@/components/BrandText"; +import { CurrencyIcon } from "@/components/CurrencyIcon"; +import { OptimizedImage } from "@/components/OptimizedImage"; +import { SVG } from "@/components/SVG"; +import { PrimaryButton } from "@/components/buttons/PrimaryButton"; +import { Separator } from "@/components/separators/Separator"; +import { useFeedbacks } from "@/context/FeedbacksProvider"; +import { Wallet } from "@/context/WalletsProvider"; +import { useBalances } from "@/hooks/useBalances"; +import useSelectedWallet from "@/hooks/useSelectedWallet"; +import { parseNftId, txExplorerLink } from "@/networks"; +import { getKeplrSigningCosmWasmClient } from "@/networks/signer"; +import { + emptyBurnCart, + removeSelectedFromBurn, + selectAllSelectedNFTData, + selectSelectedNFTDataById, + selectSelectedNFTIds, + selectShowCart, + setShowBurnCart, +} from "@/store/slices/burnCartItems"; +import { RootState, useAppDispatch } from "@/store/store"; +import { + codGrayColor, + errorColor, + neutral44, + neutral77, + neutralA3, +} from "@/utils/style/colors"; +import { + fontMedium10, + fontSemibold12, + fontSemibold14, +} from "@/utils/style/fonts"; +import { layout } from "@/utils/style/layout"; +import { modalMarginPadding } from "@/utils/style/modals"; + +const Header: React.FC<{ + items: any[]; + onPressClear: () => void; + onPressHide: () => void; +}> = ({ items, onPressClear, onPressHide }) => { + return ( + + + Burn + + {items.length} + + + + + Clear + + + + + + ); +}; + +const CartItems: React.FC<{ id: EntityId }> = ({ id }) => { + const nft = useSelector((state: RootState) => + selectSelectedNFTDataById(state, id), + ); + + const dispatch = useAppDispatch(); + return nft ? ( + + + + + {nft?.name} + { + dispatch(removeSelectedFromBurn(id)); + }} + > + + + + + + ) : null; +}; + +const ItemTotal: React.FC<{ + textLeft: string; + networkId: string; + denom: string; + showLogo?: boolean; + textRight: string | number; +}> = ({ textLeft, showLogo = false, textRight, networkId, denom }) => { + return ( + + + {textLeft} + + + + {typeof textRight === "number" ? textRight.toFixed(0) : textRight} + + {showLogo && ( + + )} + + + ); +}; + +const Footer: React.FC<{ items: any[] }> = ({ items }) => { + const { setToast } = useFeedbacks(); + const wallet = useSelectedWallet(); + const dispatch = useAppDispatch(); + + const selectedNFTData = useSelector(selectAllSelectedNFTData); + const { setToastError, setLoadingFullScreen, setToastSuccess } = + useFeedbacks(); + + const { balances } = useBalances(wallet?.networkId, wallet?.address); + const hasEnoughMoney = selectedNFTData.every((nft) => { + const balance = + balances.find((bal) => bal.denom === nft.denom)?.amount || "0"; + return parseInt(balance, 10) > parseInt(nft.price, 10); + }); + + const cosmosMultiBuy = useCallback( + async (wallet: Wallet) => { + const sender = wallet.address; + if (!sender) { + throw Error("invalid buy args"); + } + + const msgs: EncodeObject[] = []; + + selectedNFTData.map((nft) => { + const [network, , tokenId] = parseNftId(nft.id); + + if (nft.networkId !== "teritori" || !network) { + setToast({ + title: `${nft.networkId} multi-burn is not supported`, + duration: 5000, + mode: "normal", + type: "error", + }); + return; + } + + const msg = { + typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract", + value: { + sender, + msg: toUtf8( + JSON.stringify({ + burn: { + nft_contract_addr: nft.nftContractAddress, + nft_token_id: tokenId, + }, + }), + ), + contract: network.vaultContractAddress, + funds: [], + }, + }; + + msgs.push(msg); + }); + if (msgs.length > 0) { + setLoadingFullScreen(true); + const cosmwasmClient = await getKeplrSigningCosmWasmClient("teritori"); + try { + const tx = await cosmwasmClient.signAndBroadcast( + sender, + msgs, + "auto", + ); + if (isDeliverTxFailure(tx)) { + throw Error(tx.transactionHash); + } + selectedNFTData.map((nft) => { + setToastSuccess({ + title: "Burned", + message: "View TX", + duration: 10000, + onPress: () => { + Linking.openURL(txExplorerLink("teritori", tx.transactionHash)); // test it further + }, + }); + dispatch(removeSelectedFromBurn(nft.id)); //remove items from cart + setLoadingFullScreen(false); + }); + } catch (e: any) { + setToastError({ + title: "Error", + message: `${e}`, + duration: 30000, + }); + } + } + }, + [ + dispatch, + selectedNFTData, + setLoadingFullScreen, + setToast, + setToastError, + setToastSuccess, + ], + ); + + const onBuyButtonPress = async () => { + if (!wallet) { + setToast({ + title: `Wallet is not connected`, + duration: 5000, + mode: "normal", + type: "error", + }); + return; + } + await cosmosMultiBuy(wallet); + }; + + const grouped = groupBy(selectedNFTData, (e) => { + return e.denom; + }); + + return ( + + {Object.values(grouped).map((totals, index) => { + return ( + + ); + })} + + + + onBuyButtonPress()} + /> + + + ); +}; + +export const BurnSideCart: React.FC<{ style?: StyleProp }> = ({ + style, +}) => { + const dispatch = useAppDispatch(); + const selected = useSelector(selectSelectedNFTIds); + const handleEmptyCart = () => { + dispatch(emptyBurnCart()); + }; + const handleHideCart = () => { + dispatch(setShowBurnCart(false)); + }; + + return useShowCart() ? ( + +
handleEmptyCart()} + onPressHide={() => handleHideCart()} + /> + { + return ; + }} + /> + +