From f1e64c15aa38cd491216eec2f047dcfe3d737a10 Mon Sep 17 00:00:00 2001 From: wraeth-eth <104132113+wraeth-eth@users.noreply.github.com> Date: Fri, 29 Nov 2024 12:12:02 +1100 Subject: [PATCH] Wraeth/v4 project nft (#4521) --- .../ReduxProjectCartProvider.tsx | 28 ++-- .../V4PayRedeemCard/NftReward.tsx | 93 ----------- .../components/ReceiveNftItem.tsx | 37 +++++ .../components/ReceiveSection.tsx | 8 + .../usePayProjectModal/usePayProjectTx.ts | 82 +++++---- .../V4PayRedeemCard/PayRedeemInput.tsx | 124 +++++++++++++- .../V4PayRedeemCard/V4NftCreditsCallouts.tsx | 45 +++++ .../V4PayRedeemCard/V4PayRedeemCard.tsx | 41 +++-- .../SuccessPayView/SuccessPayView.tsx | 4 + .../components/SuccessNftItem.tsx | 48 ++++++ .../V4NftRewards/V4NftRewardsProvider.tsx | 126 ++++++++++++++ .../v4/contexts/V4NftRewards/useNftRewards.ts | 59 +++++++ .../transactor/useLaunchProjectWithNftsTx.ts | 21 ++- src/packages/v4/hooks/useNftCartItem.ts | 53 ++++++ .../v4/hooks/useNftRewardsEnabledForPay.ts | 25 +++ src/packages/v4/utils/editRuleset.ts | 46 +++-- .../v4/utils/launchProjectTransformers.ts | 7 +- .../V4ActivityPanel/V4ActivityList.tsx | 27 ++- .../V4ActivityPanel/models/ActivityEvents.ts | 1 + .../utils/transformEventsData.ts | 5 +- .../NftReward/AddNftButton.tsx | 25 +++ .../NftReward/NftDetails.tsx | 67 ++++++++ .../V4NftRewardsPanel/NftReward/NftReward.tsx | 157 ++++++++++++++++++ .../NftReward/NftThumbnail.tsx | 32 ++++ .../NftReward/PreviewAddRemoveNftButton.tsx | 58 +++++++ .../NftReward/RemoveNftButton.tsx | 33 ++++ .../RedeemNftsSection/NftCreditsSection.tsx | 25 +++ .../RedeemNftsSection/RedeemNftTile.tsx | 32 ++++ .../RedeemNftsSection/RedeemNftTiles.tsx | 34 ++++ .../RedeemNftsSection/RedeemNftsSection.tsx | 73 ++++++++ .../V4NftRewardsPanel/V4NftRewardsPanel.tsx | 47 ++++++ .../hooks/useJB721DelegateTokenToNftReward.ts | 37 +++++ .../hooks/useNftRewardsPanel.ts | 36 ++++ .../V4ProjectTabs/V4ProjectTabs.tsx | 20 ++- .../v4/[chainName]/p/[projectId]/index.tsx | 5 +- src/utils/nftRewards.ts | 7 +- 36 files changed, 1342 insertions(+), 226 deletions(-) delete mode 100644 src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/NftReward.tsx create mode 100644 src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/components/ReceiveNftItem.tsx create mode 100644 src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4NftCreditsCallouts.tsx create mode 100644 src/packages/v4/components/ProjectDashboard/components/SuccessPayView/components/SuccessNftItem.tsx create mode 100644 src/packages/v4/contexts/V4NftRewards/V4NftRewardsProvider.tsx create mode 100644 src/packages/v4/contexts/V4NftRewards/useNftRewards.ts create mode 100644 src/packages/v4/hooks/useNftCartItem.ts create mode 100644 src/packages/v4/hooks/useNftRewardsEnabledForPay.ts create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/AddNftButton.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftDetails.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftReward.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftThumbnail.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/PreviewAddRemoveNftButton.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/RemoveNftButton.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/NftCreditsSection.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftTile.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftTiles.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftsSection.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/V4NftRewardsPanel.tsx create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/hooks/useJB721DelegateTokenToNftReward.ts create mode 100644 src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/hooks/useNftRewardsPanel.ts diff --git a/src/packages/v4/components/ProjectDashboard/ReduxProjectCartProvider.tsx b/src/packages/v4/components/ProjectDashboard/ReduxProjectCartProvider.tsx index 02816d4d59..ed5ae57af0 100644 --- a/src/packages/v4/components/ProjectDashboard/ReduxProjectCartProvider.tsx +++ b/src/packages/v4/components/ProjectDashboard/ReduxProjectCartProvider.tsx @@ -1,8 +1,10 @@ import { useWallet } from 'hooks/Wallet' +import { useReadJb721TiersHookPayCreditsOf } from 'juice-sdk-react' +import { useV4NftRewards } from 'packages/v4/contexts/V4NftRewards/V4NftRewardsProvider' import { V4CurrencyOption } from 'packages/v4/models/v4CurrencyOption' import React from 'react' import { useProjectDispatch } from './redux/hooks' -// import { projectCartActions } from './redux/projectCartSlice' +import { projectCartActions } from './redux/projectCartSlice' export type ProjectCartCurrencyAmount = { amount: number @@ -20,26 +22,26 @@ export const ReduxProjectCartProvider = ({ }: { children: React.ReactNode }) => { - // const { rewardTiers } = useContext(NftRewardsContext).nftRewards + const { + nftRewards: { rewardTiers }, + } = useV4NftRewards() const { userAddress } = useWallet() - // const userNftCredits = useNftCredits(userAddress) + const { data: nftCredits } = useReadJb721TiersHookPayCreditsOf({ + address: userAddress, + }) const dispatch = useProjectDispatch() // Set the nfts on load - // useEffect(() => { - // dispatch(projectCartActions.setAllNftRewards(rewardTiers ?? [])) - // }, [dispatch, rewardTiers]) + React.useEffect(() => { + dispatch(projectCartActions.setAllNftRewards(rewardTiers ?? [])) + }, [dispatch, rewardTiers]) // Set the user's NFT credits on load - // useEffect(() => { - // dispatch( - // projectCartActions.setUserNftCredits( - // userNftCredits.data?.toBigInt() ?? 0n, - // ), - // ) - // }, [dispatch, userNftCredits.data]) + React.useEffect(() => { + dispatch(projectCartActions.setUserNftCredits(nftCredits ?? 0n)) + }, [dispatch, nftCredits]) return <>{children} } diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/NftReward.tsx b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/NftReward.tsx deleted file mode 100644 index ac0f817e24..0000000000 --- a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/NftReward.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// const NftReward: React.FC<{ -// nft: ProjectCartNftReward -// className?: string -// }> = ({ nft, className }) => { -// const { -// price, -// name, -// quantity, -// fileUrl, -// removeNft, -// increaseQuantity, -// decreaseQuantity, -// } = useNftCartItem(nft) - -// const handleRemove = useCallback(() => { -// emitConfirmationDeletionModal({ -// onConfirm: removeNft, -// title: t`Remove NFT`, -// description: t`Are you sure you want to remove this NFT?`, -// }) -// }, [removeNft]) - -// const handleDecreaseQuantity = useCallback(() => { -// if (quantity - 1 <= 0) { -// handleRemove() -// } else { -// decreaseQuantity() -// } -// }, [decreaseQuantity, handleRemove, quantity]) - -// const priceText = price === null ? '-' : formatCurrencyAmount(price) - -// return ( -//
-//
-// -//
-//
-// -// NFT -//
- -//
{priceText}
-//
-//
- -//
-// -// -//
-//
-// ) -// } - -// const RemoveIcon: React.FC<{ onClick: () => void }> = ({ onClick }) => ( -// -// ) - -// const QuantityControl: React.FC<{ -// quantity: number -// onIncrease: () => void -// onDecrease: () => void -// }> = ({ quantity, onIncrease, onDecrease }) => { -// return ( -// -// -// {quantity} -// -// -// ) -// } diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/components/ReceiveNftItem.tsx b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/components/ReceiveNftItem.tsx new file mode 100644 index 0000000000..d1072d7a1c --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/components/ReceiveNftItem.tsx @@ -0,0 +1,37 @@ +import { Trans } from '@lingui/macro' +import { CartItemBadge } from 'components/CartItemBadge' +import { SmallNftSquare } from 'components/NftRewards/SmallNftSquare' +import { useNftCartItem } from 'packages/v4/hooks/useNftCartItem' +import { twMerge } from 'tailwind-merge' +import { ProjectCartNftReward } from '../../../ReduxProjectCartProvider' + +export const ReceiveNftItem = ({ + className, + nftReward, +}: { + className?: string + nftReward: ProjectCartNftReward +}) => { + const { fileUrl, name, quantity } = useNftCartItem(nftReward) + + return ( +
+
+
+ + {name} + + NFT + +
+
{quantity}
+
+
+ ) +} diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/components/ReceiveSection.tsx b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/components/ReceiveSection.tsx index 4d0a946216..fdb45559e5 100644 --- a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/components/ReceiveSection.tsx +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/components/ReceiveSection.tsx @@ -6,6 +6,7 @@ import { } from '../hooks/usePayProjectModal/usePayProjectModal' import { useProjectPaymentTokens } from '../hooks/useProjectPaymentTokens' import { EditRewardBeneficiary } from './EditRewardBeneficiary' +import { ReceiveNftItem } from './ReceiveNftItem' import { ReceiveTokensItem } from './ReceiveTokensItem' export const ReceiveSection = ({ className }: { className?: string }) => { @@ -36,6 +37,13 @@ export const ReceiveSection = ({ className }: { className?: string }) => { + {nftRewards.map(nftReward => ( + + ))} ) } diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/usePayProjectModal/usePayProjectTx.ts b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/usePayProjectModal/usePayProjectTx.ts index 16005cb97f..1909d8d166 100644 --- a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/usePayProjectModal/usePayProjectTx.ts +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/usePayProjectModal/usePayProjectTx.ts @@ -1,19 +1,19 @@ +import { waitForTransactionReceipt } from '@wagmi/core' +import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' import { FormikHelpers } from 'formik' import { useWallet } from 'hooks/Wallet' import { useCurrencyConverter } from 'hooks/useCurrencyConverter' -import { useProjectSelector } from 'packages/v4/components/ProjectDashboard/redux/hooks' -import { ProjectPayReceipt } from 'packages/v4/views/V4ProjectDashboard/hooks/useProjectPageQueries' -// import { NftRewardsContext } from 'packages/v4/contexts/NftRewards/NftRewardsContext' -// import { useProjectHasErc20 } from 'packages/v4/hooks/useProjectHasErc20' -import { waitForTransactionReceipt } from '@wagmi/core' -import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' -import { NATIVE_TOKEN } from 'juice-sdk-core' +import { DEFAULT_METADATA, NATIVE_TOKEN } from 'juice-sdk-core' import { useJBContractContext, + useJBRulesetContext, + usePreparePayMetadata, useWriteJbMultiTerminalPay, } from 'juice-sdk-react' -// import { useProjectHasErc20 } from 'packages/v2v3/hooks/useProjectHasErc20' +import { useProjectSelector } from 'packages/v4/components/ProjectDashboard/redux/hooks' +import { useV4NftRewards } from 'packages/v4/contexts/V4NftRewards/V4NftRewardsProvider' import { V4_CURRENCY_ETH } from 'packages/v4/utils/currency' +import { ProjectPayReceipt } from 'packages/v4/views/V4ProjectDashboard/hooks/useProjectPageQueries' import { wagmiConfig } from 'packages/v4/wagmiConfig' import { useCallback, useContext, useMemo } from 'react' import { buildPaymentMemo } from 'utils/buildPaymentMemo' @@ -42,11 +42,12 @@ export const usePayProjectTx = ({ const { payAmount, chosenNftRewards } = useProjectSelector( state => state.projectCart, ) - // const { - // nftRewards: { rewardTiers }, - // } = useContext(NftRewardsContext) + const { + nftRewards: { rewardTiers }, + } = useV4NftRewards() const converter = useCurrencyConverter() const { receivedTickets } = useProjectPaymentTokens() + // TODO: implement // const projectHasErc20 = useProjectHasErc20() const buildPayReceipt = useCallback( @@ -76,10 +77,21 @@ export const usePayProjectTx = ({ } }, [payAmount, converter]) - // const prepareDelegateMetadata = usePrepareDelegatePayMetadata(weiAmount, { - // nftRewards: chosenNftRewards, - // receivedTickets, - // }) + const { + rulesetMetadata: { data: rulesetMetadata }, + } = useJBRulesetContext() + const metadata = usePreparePayMetadata( + rulesetMetadata?.dataHook + ? { + jb721Hook: { + dataHookAddress: rulesetMetadata.dataHook, + tierIdsToMint: chosenNftRewards + .map(({ id, quantity }) => Array(quantity).fill(BigInt(id))) + .flat(), + }, + } + : undefined, + ) const { writeContractAsync: writePay } = useWriteJbMultiTerminalPay() const { contracts, projectId } = useJBContractContext() @@ -103,13 +115,13 @@ export const usePayProjectTx = ({ const memo = buildPaymentMemo({ text: messageString, imageUrl: attachedUrl, - // nftUrls: chosenNftRewards - // .map( - // ({ id }) => - // (rewardTiers ?? []).find(({ id: tierId }) => tierId === id) - // ?.fileUrl, - // ) - // .filter((url): url is string => !!url), + nftUrls: chosenNftRewards + .map( + ({ id }) => + (rewardTiers ?? []).find(({ id: tierId }) => tierId === id) + ?.fileUrl, + ) + .filter((url): url is string => !!url), }) const beneficiary = (values.beneficiaryAddress ?? userAddress) as Address const args = [ @@ -119,9 +131,15 @@ export const usePayProjectTx = ({ beneficiary, 0n, memo, - '0x0', + metadata ?? DEFAULT_METADATA, ] as const + // SIMULATE TRANSACTION: + // const encodedData = encodeFunctionData({ + // abi: jbMultiTerminalAbi, // ABI of the contract + // functionName: 'pay', + // args, + // }) try { const hash = await writePay({ address: contracts.primaryNativeTerminal.data, @@ -147,21 +165,19 @@ export const usePayProjectTx = ({ } }, [ - // projectHasErc20, - buildPayReceipt, - // chosenNftRewards, - onTransactionConfirmedCallback, - onTransactionErrorCallback, - onTransactionPendingCallback, - // payProjectTx, - // rewardTiers, weiAmount, + contracts.primaryNativeTerminal.data, userAddress, - // prepareDelegateMetadata, + chosenNftRewards, projectId, + metadata, + rewardTiers, writePay, - contracts.primaryNativeTerminal.data, + onTransactionPendingCallback, addTransaction, + onTransactionConfirmedCallback, + buildPayReceipt, + onTransactionErrorCallback, ], ) } diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayRedeemInput.tsx b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayRedeemInput.tsx index 7afc137db8..681536befa 100644 --- a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayRedeemInput.tsx +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayRedeemInput.tsx @@ -1,10 +1,21 @@ -import { ArrowDownIcon } from '@heroicons/react/24/outline' +import { + ArrowDownIcon, + MinusIcon, + PlusIcon, + TrashIcon, +} from '@heroicons/react/24/outline' import { t } from '@lingui/macro' import { Tooltip } from 'antd' +import { CartItemBadge } from 'components/CartItemBadge' +import { SmallNftSquare } from 'components/NftRewards/SmallNftSquare' +import { TruncatedText } from 'components/TruncatedText' +import { emitConfirmationDeletionModal } from 'hooks/emitConfirmationDeletionModal' import { useCurrencyConverter } from 'hooks/useCurrencyConverter' +import { useNftCartItem } from 'packages/v4/hooks/useNftCartItem' import { V4_CURRENCY_USD } from 'packages/v4/utils/currency' import { formatCurrencyAmount } from 'packages/v4/utils/formatCurrencyAmount' -import { ReactNode, useMemo } from 'react' +import { useProjectPageQueries } from 'packages/v4/views/V4ProjectDashboard/hooks/useProjectPageQueries' +import React, { ReactNode, useMemo } from 'react' import { twMerge } from 'tailwind-merge' import { formatAmount } from 'utils/format/formatAmount' import { ProjectCartNftReward } from '../ReduxProjectCartProvider' @@ -134,14 +145,13 @@ export const PayRedeemInput = ({
)} - {/* {nfts && nfts?.length > 0 && ( + {nfts && nfts?.length > 0 && (
{nfts.map((nft, i) => ( ))}
)} - */}
{downArrow && ( @@ -208,3 +218,109 @@ const DownArrow = ({ className }: { className?: string }) => { ) } + +const NftReward: React.FC<{ + nft: ProjectCartNftReward + className?: string +}> = ({ nft, className }) => { + const { + price, + name, + quantity, + fileUrl, + removeNft, + increaseQuantity, + decreaseQuantity, + } = useNftCartItem(nft) + const { setProjectPageTab } = useProjectPageQueries() + + const handleRemove = React.useCallback(() => { + emitConfirmationDeletionModal({ + onConfirm: removeNft, + title: t`Remove NFT`, + description: t`Are you sure you want to remove this NFT?`, + }) + }, [removeNft]) + + const handleDecreaseQuantity = React.useCallback(() => { + if (quantity - 1 <= 0) { + handleRemove() + } else { + decreaseQuantity() + } + }, [decreaseQuantity, handleRemove, quantity]) + + const priceText = useMemo(() => { + if (price === null) { + return '-' + } + return formatCurrencyAmount(price) + }, [price]) + + return ( +
+
+ +
+
setProjectPageTab('nft_rewards')} + > + + NFT +
+ +
{priceText}
+
+
+ +
+ + +
+
+ ) +} + +const RemoveIcon: React.FC<{ onClick: () => void }> = ({ onClick }) => ( + +) + +const QuantityControl: React.FC<{ + quantity: number + onIncrease: () => void + onDecrease: () => void +}> = ({ quantity, onIncrease, onDecrease }) => { + return ( + + + {quantity} + + + ) +} diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4NftCreditsCallouts.tsx b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4NftCreditsCallouts.tsx new file mode 100644 index 0000000000..9930cdb7bb --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4NftCreditsCallouts.tsx @@ -0,0 +1,45 @@ +import { CubeIcon } from '@heroicons/react/24/outline' +import { Trans } from '@lingui/macro' +import { Button } from 'antd' +import { useWallet } from 'hooks/Wallet' +import { formatEther } from 'juice-sdk-core' +import { useReadJb721TiersHookPayCreditsOf } from 'juice-sdk-react' +import { useProjectPageQueries } from 'packages/v4/views/V4ProjectDashboard/hooks/useProjectPageQueries' + +export function V4NftCreditsCallouts() { + const { setProjectPageTab } = useProjectPageQueries() + const { userAddress } = useWallet() + const { data: nftCredits } = useReadJb721TiersHookPayCreditsOf({ + address: userAddress, + }) + + if (!nftCredits || nftCredits <= 0n) { + return null + } + + return ( +
+
+
+ +
+ + You have{' '} + {formatEther(nftCredits)} ETH{' '} + of unclaimed NFT credits + +
+ +
+ ) +} diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4PayRedeemCard.tsx b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4PayRedeemCard.tsx index fb63e4426d..e77d1fcf6c 100644 --- a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4PayRedeemCard.tsx +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4PayRedeemCard.tsx @@ -3,15 +3,17 @@ import { Trans, t } from '@lingui/macro' import { Tooltip } from 'antd' import { Callout } from 'components/Callout/Callout' import { useJBRulesetContext } from 'juice-sdk-react' +import { useV4NftRewards } from 'packages/v4/contexts/V4NftRewards/V4NftRewardsProvider' import { usePayoutLimit } from 'packages/v4/hooks/usePayoutLimit' import { MAX_PAYOUT_LIMIT } from 'packages/v4/utils/math' -import { ReactNode } from 'react' +import React, { ReactNode } from 'react' import { twMerge } from 'tailwind-merge' import { useProjectDispatch, useProjectSelector } from '../redux/hooks' import { payRedeemActions } from '../redux/payRedeemSlice' import { PayConfiguration } from './PayConfiguration' import { PayProjectModal } from './PayProjectModal/PayProjectModal' import { RedeemConfiguration } from './RedeemConfiguration' +import { V4NftCreditsCallouts } from './V4NftCreditsCallouts' type PayRedeemCardProps = { className?: string @@ -22,7 +24,7 @@ export const V4PayRedeemCard: React.FC = ({ }) => { const { ruleset, rulesetMetadata } = useJBRulesetContext() const state = useProjectSelector(state => state.payRedeem.cardState) - // const { value: hasNfts, loading: hasNftsLoading } = useHasNftRewards() + const nftRewards = useV4NftRewards() const { data: payoutLimit } = usePayoutLimit() const dispatch = useProjectDispatch() @@ -41,15 +43,25 @@ export const V4PayRedeemCard: React.FC = ({ rulesetMetadata.data.redemptionRate.value > 0n, } - const weight = ruleset.data?.weight - const isIssuingTokens = Boolean(weight && weight.value > 0n) - // const showNfts = hasNfts && !hasNftsLoading - const noticeText = isIssuingTokens - ? // showNfts - // ? t`Project isn't currently issuing tokens, but is issuing NFTs` - // : - t`Project isn't currently issuing tokens` - : undefined + const isIssuingTokens = React.useMemo(() => { + const weight = ruleset.data?.weight + return Boolean(weight && weight.value > 0n) + }, [ruleset.data?.weight]) + + const noticeText = React.useMemo(() => { + if (!isIssuingTokens) { + return undefined + } + const showNfts = + !nftRewards.loading && + (nftRewards.nftRewards.rewardTiers ?? []).length > 0 + + if (showNfts) { + return t`Project isn't currently issuing tokens, but is issuing NFTs` + } + + return t`Project isn't currently issuing tokens` + }, [isIssuingTokens, nftRewards.loading, nftRewards.nftRewards.rewardTiers]) const redeemDisabled = !rulesetMetadata.data?.redemptionRate || @@ -111,9 +123,10 @@ export const V4PayRedeemCard: React.FC = ({ )} - {/* */} - {/* - {projectHasErc20Token && unclaimedTokenBalance?.gt(0) && ( + + + {/* TODO */} + {/* {projectHasErc20Token && unclaimedTokenBalance?.gt(0) && ( )} */} diff --git a/src/packages/v4/components/ProjectDashboard/components/SuccessPayView/SuccessPayView.tsx b/src/packages/v4/components/ProjectDashboard/components/SuccessPayView/SuccessPayView.tsx index abbee4066b..4a3ebc13cd 100644 --- a/src/packages/v4/components/ProjectDashboard/components/SuccessPayView/SuccessPayView.tsx +++ b/src/packages/v4/components/ProjectDashboard/components/SuccessPayView/SuccessPayView.tsx @@ -5,6 +5,7 @@ import dynamic from 'next/dynamic' import Link from 'next/link' import { v4ProjectRoute } from 'packages/v4/utils/routes' import { useChainId } from 'wagmi' +import { SuccessNftItem } from './components/SuccessNftItem' import { SuccessPayCard } from './components/SuccessPayCard' import { SuccessTokensItem } from './components/SuccessTokensItem' import { useSuccessPayView } from './hooks/useSuccessPayView' @@ -78,6 +79,9 @@ export const SuccessPayView = () => { Your NFTs & Rewards
+ {projectPayReceipt?.nfts.map(({ id }) => ( + + ))}
diff --git a/src/packages/v4/components/ProjectDashboard/components/SuccessPayView/components/SuccessNftItem.tsx b/src/packages/v4/components/ProjectDashboard/components/SuccessPayView/components/SuccessNftItem.tsx new file mode 100644 index 0000000000..c494bb7108 --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/components/SuccessPayView/components/SuccessNftItem.tsx @@ -0,0 +1,48 @@ +import { Trans } from '@lingui/macro' +import { CartItemBadge } from 'components/CartItemBadge' +import { NftPreview } from 'components/NftRewards/NftPreview' +import { SmallNftSquare } from 'components/NftRewards/SmallNftSquare' +import { useV4NftRewards } from 'packages/v4/contexts/V4NftRewards/V4NftRewardsProvider' +import { useMemo, useState } from 'react' + +export const SuccessNftItem = ({ id }: { id: number }) => { + const { + nftRewards: { rewardTiers }, + } = useV4NftRewards() + const [previewVisible, setPreviewVisible] = useState(false) + + const openPreview = () => setPreviewVisible(true) + + const rewardTier = useMemo(() => { + if (!rewardTiers) return undefined + const nftReward = rewardTiers.find(reward => reward.id === id) + return nftReward + }, [id, rewardTiers]) + + return ( + <> +
+ + + {rewardTier?.name ?? ''} + + + NFT + +
+ {rewardTier && ( + + )} + + ) +} diff --git a/src/packages/v4/contexts/V4NftRewards/V4NftRewardsProvider.tsx b/src/packages/v4/contexts/V4NftRewards/V4NftRewardsProvider.tsx new file mode 100644 index 0000000000..44a6d1778c --- /dev/null +++ b/src/packages/v4/contexts/V4NftRewards/V4NftRewardsProvider.tsx @@ -0,0 +1,126 @@ +import { + jb721TiersHookStoreAbi, + useJBRulesetContext, + useReadJb721TiersHookPricingContext, + useReadJb721TiersHookStoreAddress, + useReadJb721TiersHookStoreFlagsOf, + useReadJb721TiersHookStoreTiersOf, +} from 'juice-sdk-react' +import { JB721GovernanceType } from 'models/nftRewards' +import { V2V3CurrencyOption } from 'packages/v2v3/models/currencyOption' +import React, { createContext } from 'react' +import { DEFAULT_NFT_PRICING } from 'redux/slices/editingV2Project' +import { NftRewardsData } from 'redux/slices/shared/v2ProjectTypes' +import { CIDsOfNftRewardTiersResponse } from 'utils/nftRewards' +import { ContractFunctionReturnType } from 'viem' +import { useNftRewards } from './useNftRewards' + +const DEFAULT_NFT_FLAGS = { + noNewTiersWithReserves: false, + noNewTiersWithVotes: false, + noNewTiersWithOwnerMinting: false, + preventOverspending: false, +} + +// TODO: This should be imported from the SDK +export type JB721TierV4 = ContractFunctionReturnType< + typeof jb721TiersHookStoreAbi, + 'view', + 'tiersOf' +>[0] + +type NftRewardsContextType = { + // nftRewards: is useReadJb721TiersHookStoreTiersOf.data returned + nftRewards: Omit< + NftRewardsData, + 'flags' | 'collectionMetadata' | 'postPayModal' + > + loading: boolean | undefined +} + +export const V4NftRewardsContext = createContext({ + nftRewards: { + CIDs: undefined, + rewardTiers: undefined, + // postPayModal: undefined, + // collectionMetadata: EMPTY_NFT_COLLECTION_METADATA, + // flags: DEFAULT_NFT_FLAGS, + governanceType: JB721GovernanceType.NONE, + pricing: DEFAULT_NFT_PRICING, + }, + loading: false, +}) + +export const V4NftRewardsProvider: React.FC< + React.PropsWithChildren +> = ({ children }) => { + const jbRuleSet = useJBRulesetContext() + const dataHookAddress = jbRuleSet.rulesetMetadata.data?.dataHook + + const storeAddress = useReadJb721TiersHookStoreAddress({ + address: dataHookAddress, + }) + + const tiersOf = useReadJb721TiersHookStoreTiersOf({ + address: storeAddress.data, + args: [ + dataHookAddress ?? `0x${'0'.repeat(40)}`, + [], // _categories + false, // _includeResolvedUri, return in each tier a result from a tokenUriResolver if one is included in the delegate + 0n, // _startingId + 10n, // limit + ], + }) + + const { data: loadedRewardTiers, isLoading: nftRewardTiersLoading } = + useNftRewards(tiersOf.data ?? [], 4, storeAddress.data) + + const loadedCIDs = CIDsOfNftRewardTiersResponse(tiersOf.data ?? []) + + const p = useReadJb721TiersHookPricingContext() + const currency = Number(p.data ? p.data[0] : 0) as V2V3CurrencyOption + + const flags = useReadJb721TiersHookStoreFlagsOf({ + address: '0x7b1F4Ba6312A104E645B06Ab97e4CaA1ef0F773f', + }) + + const loading = React.useMemo( + () => + storeAddress.isLoading || + tiersOf.isLoading || + nftRewardTiersLoading || + p.isLoading || + flags.isLoading, + [ + storeAddress.isLoading, + tiersOf.isLoading, + nftRewardTiersLoading, + p.isLoading, + flags.isLoading, + ], + ) + + const ctx = { + nftRewards: { + CIDs: loadedCIDs, + rewardTiers: loadedRewardTiers, + pricing: { currency }, + governanceType: JB721GovernanceType.NONE, + // collectionMetadata: { + // ...EMPTY_NFT_COLLECTION_METADATA, + // uri: collection + // } + // postPayModal: projectMetadata?.nftPaymentSuccessModal, + // flags: flags.data ?? DEFAULT_NFT_FLAGS, + }, + loading, + } + + return ( + + {children} + + ) +} + +export const useV4NftRewards = () => React.useContext(V4NftRewardsContext) diff --git a/src/packages/v4/contexts/V4NftRewards/useNftRewards.ts b/src/packages/v4/contexts/V4NftRewards/useNftRewards.ts new file mode 100644 index 0000000000..463806752b --- /dev/null +++ b/src/packages/v4/contexts/V4NftRewards/useNftRewards.ts @@ -0,0 +1,59 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query' +import axios from 'axios' +import { formatEther } from 'juice-sdk-core' +import { IPFSNftRewardTier, NftRewardTier } from 'models/nftRewards' +import { withHttps } from 'utils/externalLink' +import { cidFromUrl, decodeEncodedIpfsUri, ipfsGatewayUrl } from 'utils/ipfs' +import { JB721TierV4 } from './V4NftRewardsProvider' + +async function fetchRewardTierMetadata({ tier }: { tier: JB721TierV4 }) { + const tierCid = decodeEncodedIpfsUri(tier.encodedIPFSUri) + const url = ipfsGatewayUrl(tierCid) + + const response = await axios.get(url) + const tierMetadata: IPFSNftRewardTier = response.data + + const maxSupply = tier.initialSupply + + // Some projects have image links hard-coded to the old IPFS gateway. + const pinataRegex = /^(https?:\/\/jbx\.mypinata\.cloud)/ + if (tierMetadata?.image && pinataRegex.test(tierMetadata.image)) { + const imageUrlCid = cidFromUrl(tierMetadata.image) + tierMetadata.image = ipfsGatewayUrl(imageUrlCid) + } + + const rawContributionFloor = tier.price + + return { + id: tier.id, + name: tierMetadata.name, + description: tierMetadata.description, + externalLink: withHttps(tierMetadata.externalLink), + // convert rawContributionFloor bigint to a number + contributionFloor: formatEther(rawContributionFloor), + maxSupply, + remainingSupply: tier.remainingSupply, + fileUrl: tierMetadata.image, + beneficiary: tier.reserveBeneficiary, + reservedRate: tier.reserveFrequency, + votingWeight: tier.votingUnits, + } +} + +export const useNftRewards = ( + tiers: readonly JB721TierV4[], + projectId: number | undefined, + dataSourceAddress: string | undefined, +): UseQueryResult => { + const enabled = Boolean(tiers?.length) + + return useQuery({ + queryKey: ['nftRewards', projectId, dataSourceAddress], + enabled, + queryFn: async () => { + return await Promise.all( + tiers.map(tier => fetchRewardTierMetadata({ tier })), + ) + }, + }) +} diff --git a/src/packages/v4/hooks/JB721Delegate/transactor/useLaunchProjectWithNftsTx.ts b/src/packages/v4/hooks/JB721Delegate/transactor/useLaunchProjectWithNftsTx.ts index f105666072..04608418c7 100644 --- a/src/packages/v4/hooks/JB721Delegate/transactor/useLaunchProjectWithNftsTx.ts +++ b/src/packages/v4/hooks/JB721Delegate/transactor/useLaunchProjectWithNftsTx.ts @@ -1,3 +1,7 @@ +import { waitForTransactionReceipt } from '@wagmi/core' +import { JUICEBOX_MONEY_PROJECT_METADATA_DOMAIN } from 'constants/metadataDomain' +import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' +import { useWallet } from 'hooks/Wallet' import { DEFAULT_MEMO, NATIVE_TOKEN, @@ -9,26 +13,21 @@ import { useReadJb721TiersHookStoreTiersOf, useWriteJb721TiersHookProjectDeployerLaunchProjectFor, } from 'juice-sdk-react' +import { isValidMustStartAtOrAfter } from 'packages/v2v3/utils/fundingCycle' import { JBDeploy721TiersHookConfig, LaunchProjectWithNftsTxArgs, } from 'packages/v4/models/nfts' -import { Address, WaitForTransactionReceiptReturnType, zeroAddress } from 'viem' -import { - LaunchV2V3ProjectArgs, - transformV2V3CreateArgsToV4, -} from '../../../utils/launchProjectTransformers' - -import { waitForTransactionReceipt } from '@wagmi/core' -import { JUICEBOX_MONEY_PROJECT_METADATA_DOMAIN } from 'constants/metadataDomain' -import { TxHistoryContext } from 'contexts/Transaction/TxHistoryContext' -import { useWallet } from 'hooks/Wallet' -import { isValidMustStartAtOrAfter } from 'packages/v2v3/utils/fundingCycle' import { wagmiConfig } from 'packages/v4/wagmiConfig' import { useContext } from 'react' import { DEFAULT_MUST_START_AT_OR_AFTER } from 'redux/slices/shared/v2ProjectDefaultState' import { ipfsUri } from 'utils/ipfs' +import { Address, WaitForTransactionReceiptReturnType, zeroAddress } from 'viem' import { useChainId } from 'wagmi' +import { + LaunchV2V3ProjectArgs, + transformV2V3CreateArgsToV4, +} from '../../../utils/launchProjectTransformers' import { LaunchTxOpts, SUPPORTED_JB_CONTROLLER_ADDRESS, diff --git a/src/packages/v4/hooks/useNftCartItem.ts b/src/packages/v4/hooks/useNftCartItem.ts new file mode 100644 index 0000000000..6f9b23766f --- /dev/null +++ b/src/packages/v4/hooks/useNftCartItem.ts @@ -0,0 +1,53 @@ +import { useProjectDispatch } from 'packages/v2v3/components/V2V3Project/ProjectDashboard/redux/hooks' +import React from 'react' +import { projectCartActions } from '../components/ProjectDashboard/redux/projectCartSlice' +import { ProjectCartNftReward } from '../components/ProjectDashboard/ReduxProjectCartProvider' +import { useV4NftRewards } from '../contexts/V4NftRewards/V4NftRewardsProvider' +import { V4_CURRENCY_ETH } from '../utils/currency' + +export const useNftCartItem = ({ id, quantity }: ProjectCartNftReward) => { + const dispatch = useProjectDispatch() + const { nftRewards } = useV4NftRewards() + const rewardTiers = React.useMemo( + () => nftRewards.rewardTiers ?? [], + [nftRewards.rewardTiers], + ) + + const rewardTier = React.useMemo( + () => rewardTiers.find(tier => tier.id === id), + [rewardTiers, id], + ) + + const price = React.useMemo( + () => ({ + amount: (rewardTier?.contributionFloor ?? 0) * quantity, + currency: V4_CURRENCY_ETH, + }), + [quantity, rewardTier?.contributionFloor], + ) + + const removeNft = React.useCallback( + () => dispatch(projectCartActions.removeNftReward({ id })), + [dispatch, id], + ) + + const increaseQuantity = React.useCallback( + () => dispatch(projectCartActions.increaseNftRewardQuantity({ id })), + [dispatch, id], + ) + + const decreaseQuantity = React.useCallback( + () => dispatch(projectCartActions.decreaseNftRewardQuantity({ id })), + [dispatch, id], + ) + + return { + name: rewardTier?.name, + fileUrl: rewardTier?.fileUrl, + quantity, + price, + removeNft, + increaseQuantity, + decreaseQuantity, + } +} diff --git a/src/packages/v4/hooks/useNftRewardsEnabledForPay.ts b/src/packages/v4/hooks/useNftRewardsEnabledForPay.ts new file mode 100644 index 0000000000..5a18cbfa54 --- /dev/null +++ b/src/packages/v4/hooks/useNftRewardsEnabledForPay.ts @@ -0,0 +1,25 @@ +import { JBRulesetContext, useJBRulesetContext } from 'juice-sdk-react' +import React from 'react' +import { zeroAddress } from 'viem' +import { useV4NftRewards } from '../contexts/V4NftRewards/V4NftRewardsProvider' + +type RulesetMetadata = JBRulesetContext['rulesetMetadata']['data'] + +export function useNftRewardsEnabledForPay() { + const jbRuleset = useJBRulesetContext() + const { nftRewards } = useV4NftRewards() + + const hasNftRewards = React.useMemo( + () => nftRewards.rewardTiers?.length !== 0, + [nftRewards.rewardTiers], + ) + + return hasNftRewards && hasDataSourceForPay(jbRuleset.rulesetMetadata.data) +} + +const hasDataSourceForPay = (rulesetMetadata: RulesetMetadata) => { + return ( + rulesetMetadata?.dataHook !== zeroAddress && + !!rulesetMetadata?.useDataHookForPay + ) +} diff --git a/src/packages/v4/utils/editRuleset.ts b/src/packages/v4/utils/editRuleset.ts index ca46980f54..38f28dbd09 100644 --- a/src/packages/v4/utils/editRuleset.ts +++ b/src/packages/v4/utils/editRuleset.ts @@ -1,9 +1,9 @@ -import { NATIVE_TOKEN } from "juice-sdk-core"; -import round from "lodash/round"; -import { issuanceRateFrom } from "packages/v2v3/utils/math"; -import { parseWad } from "utils/format/formatNumber"; -import { otherUnitToSeconds } from "utils/format/formatTime"; -import { EditCycleFormFields } from "../views/V4ProjectSettings/EditCyclePage/EditCycleFormFields"; +import { NATIVE_TOKEN } from 'juice-sdk-core' +import round from 'lodash/round' +import { issuanceRateFrom } from 'packages/v2v3/utils/math' +import { parseWad } from 'utils/format/formatNumber' +import { otherUnitToSeconds } from 'utils/format/formatTime' +import { EditCycleFormFields } from '../views/V4ProjectSettings/EditCyclePage/EditCycleFormFields' export function transformEditCycleFormFieldsToTxArgs({ formValues, @@ -11,21 +11,21 @@ export function transformEditCycleFormFieldsToTxArgs({ tokenAddress, projectId, }: { - formValues: EditCycleFormFields; - primaryNativeTerminal: `0x${string}`; - tokenAddress: `0x${string}`; - projectId: bigint; + formValues: EditCycleFormFields + primaryNativeTerminal: `0x${string}` + tokenAddress: `0x${string}` + projectId: bigint }) { - const now = round(new Date().getTime() / 1000); - const mustStartAtOrAfter = now; + const now = round(new Date().getTime() / 1000) + const mustStartAtOrAfter = now const duration = otherUnitToSeconds({ duration: formValues.duration, unit: formValues.durationUnit.value, }) - const weight = BigInt(issuanceRateFrom(formValues.issuanceRate.toString())); - const decayPercent = round(formValues.decayPercent * 10000000); - const approvalHook = formValues.approvalHook; + const weight = BigInt(issuanceRateFrom(formValues.issuanceRate.toString())) + const decayPercent = round(formValues.decayPercent * 10000000) + const approvalHook = formValues.approvalHook const rulesetConfigurations = [ { @@ -54,15 +54,15 @@ export function transformEditCycleFormFieldsToTxArgs({ useTotalSurplusForRedemptions: false, // Defaulting to false as it's not in formValues useDataHookForPay: false, // Defaulting to false as it's not in formValues useDataHookForRedeem: false, // Defaulting to false as it's not in formValues - dataHook: "0x0000000000000000000000000000000000000000" as `0x${string}`, // Defaulting to a null address + dataHook: '0x0000000000000000000000000000000000000000' as `0x${string}`, // Defaulting to a null address metadata: 0, // Assuming no additional metadata is provided - allowCrosschainSuckerExtension: false + allowCrosschainSuckerExtension: false, }, splitGroups: [ { groupId: BigInt(NATIVE_TOKEN), - splits: formValues.payoutSplits.map((split) => ({ + splits: formValues.payoutSplits.map(split => ({ preferAddToBalance: Boolean(split.preferAddToBalance), percent: Number(split.percent.value), projectId: BigInt(split.projectId), @@ -73,7 +73,7 @@ export function transformEditCycleFormFieldsToTxArgs({ }, { groupId: BigInt(1), - splits: formValues.reservedTokensSplits.map((split) => ({ + splits: formValues.reservedTokensSplits.map(split => ({ preferAddToBalance: Boolean(split.preferAddToBalance), percent: Number(split.percent.value), projectId: BigInt(split.projectId), @@ -103,11 +103,7 @@ export function transformEditCycleFormFieldsToTxArgs({ }, ], }, - ]; + ] - return [ - projectId, - rulesetConfigurations, - formValues.memo ?? "", - ] as const; + return [projectId, rulesetConfigurations, formValues.memo ?? ''] as const } diff --git a/src/packages/v4/utils/launchProjectTransformers.ts b/src/packages/v4/utils/launchProjectTransformers.ts index 066ea2d678..0effb3028b 100644 --- a/src/packages/v4/utils/launchProjectTransformers.ts +++ b/src/packages/v4/utils/launchProjectTransformers.ts @@ -128,7 +128,7 @@ type LaunchProjectJBSplit = Omit & { percent: number } export type LaunchV4ProjectGroupedSplit = Omit< V4GroupedSplits, 'splits' | 'groupId' -> & { splits: LaunchProjectJBSplit[], groupId: bigint } +> & { splits: LaunchProjectJBSplit[]; groupId: bigint } export function transformV2V3SplitsToV4({ v2v3Splits, @@ -136,10 +136,7 @@ export function transformV2V3SplitsToV4({ v2v3Splits: V2V3GroupedSplits[] }): LaunchV4ProjectGroupedSplit[] { return v2v3Splits.map(group => ({ - groupId: - group.group === SplitGroup.ETHPayout - ? BigInt(NATIVE_TOKEN) - : 1n, // TODO dont hardcode reserved token group as 1n + groupId: group.group === SplitGroup.ETHPayout ? BigInt(NATIVE_TOKEN) : 1n, // TODO dont hardcode reserved token group as 1n splits: group.splits.map(split => ({ preferAddToBalance: Boolean(split.preferClaimed), percent: split.percent, diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/V4ActivityList.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/V4ActivityList.tsx index 27db6ece2d..a6f83fd979 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/V4ActivityList.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/V4ActivityList.tsx @@ -1,38 +1,38 @@ import { t } from '@lingui/macro' import Loading from 'components/Loading' -import { - NativeTokenValue, - useJBContractContext, - useJBTokenContext, -} from 'juice-sdk-react' +import RichNote from 'components/RichNote/RichNote' +import { NativeTokenValue, useJBContractContext } from 'juice-sdk-react' import { OrderDirection, PayEvent_OrderBy, PayEventsDocument, } from 'packages/v4/graphql/client/graphql' import { useSubgraphQuery } from 'packages/v4/graphql/useSubgraphQuery' +import React from 'react' import { ActivityEvent } from './activityEventElems/ActivityElement' import { ActivityOptions } from './ActivityOptions' import { PayEvent } from './models/ActivityEvents' import { transformPayEventsRes } from './utils/transformEventsData' export function V4ActivityList() { - const { token } = useJBTokenContext() const { projectId } = useJBContractContext() // TODO: pageSize (pagination) const { data: payEventsData, isLoading } = useSubgraphQuery({ - document: PayEventsDocument, + document: PayEventsDocument, variables: { orderBy: PayEvent_OrderBy.timestamp, orderDirection: OrderDirection.desc, where: { projectId: Number(projectId), }, - } + }, }) - const payEvents = transformPayEventsRes(payEventsData) ?? [] + const payEvents = React.useMemo( + () => transformPayEventsRes(payEventsData) ?? [], + [payEventsData], + ) return (
@@ -62,15 +62,10 @@ export function V4ActivityList() { header={t`Paid`} subject={ - - - } - extra={ - - bought {event.beneficiaryTokenCount?.format(6)}{' '} - {token.data?.symbol ?? 'tokens'} + } + extra={} />
) diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/models/ActivityEvents.ts b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/models/ActivityEvents.ts index f960855f98..3e3c623afb 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/models/ActivityEvents.ts +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/models/ActivityEvents.ts @@ -7,6 +7,7 @@ export type PayEvent = { amountUSD: Ether | undefined beneficiary: Address beneficiaryTokenCount?: JBProjectToken + note: string timestamp: number txHash: string } diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/utils/transformEventsData.ts b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/utils/transformEventsData.ts index 243a00239d..4083ca7de4 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/utils/transformEventsData.ts +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ActivityPanel/utils/transformEventsData.ts @@ -9,11 +9,14 @@ export function transformPayEventsRes( return { id: event.id, amount: new Ether(BigInt(event.amount)), - amountUSD: event.amountUSD ? new Ether(BigInt(event.amountUSD)) : undefined, + amountUSD: event.amountUSD + ? new Ether(BigInt(event.amountUSD)) + : undefined, beneficiary: event.beneficiary, beneficiaryTokenCount: new JBProjectToken( BigInt(event.beneficiaryTokenCount), ), + note: event.note, timestamp: event.timestamp, txHash: event.txHash, } diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/AddNftButton.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/AddNftButton.tsx new file mode 100644 index 0000000000..8bac6cf1af --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/AddNftButton.tsx @@ -0,0 +1,25 @@ +import { PlusIcon } from '@heroicons/react/24/solid' +import { Trans } from '@lingui/macro' +import { stopPropagation } from 'react-stop-propagation' +import { twMerge } from 'tailwind-merge' + +export const nftHoverButtonClasses = + 'absolute bottom-0 flex h-12 w-full items-center justify-center rounded-b-lg text-base font-medium text-white opacity-0 transition-all duration-200 ease-in-out group-hover:opacity-100' + +// Button that appears when hovering an NFT reward card +export function AddNftButton({ onClick }: { onClick: VoidFunction }) { + return ( +
+ + + Add NFT + +
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftDetails.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftDetails.tsx new file mode 100644 index 0000000000..053d5d1fdd --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftDetails.tsx @@ -0,0 +1,67 @@ +import { Skeleton } from 'antd' +import { TruncatedText } from 'components/TruncatedText' +import ETHAmount from 'components/currency/ETHAmount' +import { NftRewardTier } from 'models/nftRewards' +import { twMerge } from 'tailwind-merge' +import { parseWad } from 'utils/format/formatNumber' + +export function NftDetails({ + rewardTier, + loading, + hideAttributes, + remainingSupplyText, +}: { + rewardTier: NftRewardTier | undefined + loading: boolean | undefined + hideAttributes?: boolean + remainingSupplyText: string +}) { + return ( +
+ + + + {!hideAttributes ? ( +
+ {rewardTier?.contributionFloor ? ( + + + + + + ) : null} + + + {remainingSupplyText} + + +
+ ) : null} +
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftReward.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftReward.tsx new file mode 100644 index 0000000000..e17b50d006 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftReward.tsx @@ -0,0 +1,157 @@ +import { t } from '@lingui/macro' +import { Tooltip } from 'antd' +import { NftPreview } from 'components/NftRewards/NftPreview' +import { NftRewardTier } from 'models/nftRewards' +import { DEFAULT_NFT_MAX_SUPPLY } from 'packages/v2v3/constants/nftRewards' +import { usePayProjectDisabled } from 'packages/v2v3/hooks/usePayProjectDisabled' +import { useProjectSelector } from 'packages/v4/components/ProjectDashboard/redux/hooks' +import { useNftRewardsEnabledForPay } from 'packages/v4/hooks/useNftRewardsEnabledForPay' +import { useMemo, useState } from 'react' +import { twMerge } from 'tailwind-merge' +import { ipfsUriToGatewayUrl } from 'utils/ipfs' +import { AddNftButton } from './AddNftButton' +import { NftDetails } from './NftDetails' +import { NftThumbnail } from './NftThumbnail' +import { PreviewAddRemoveNftButton } from './PreviewAddRemoveNftButton' +import { RemoveNftButton } from './RemoveNftButton' + +type NftRewardProps = { + className?: string + rewardTier?: NftRewardTier + loading?: boolean + onSelect: (quantity?: number) => void + onDeselect: VoidFunction + previewDisabled?: boolean + hideAttributes?: boolean +} + +export function NftReward({ + className, + loading, + rewardTier, + previewDisabled, + onSelect, + onDeselect, + hideAttributes, +}: NftRewardProps) { + const [previewVisible, setPreviewVisible] = useState(false) + const chosenNftRewards = useProjectSelector( + state => state.projectCart.chosenNftRewards, + ) + + const nftsEnabledForPay = useNftRewardsEnabledForPay() + const { + payDisabled, + message, + loading: payDisabledLoading, + } = usePayProjectDisabled() + + const quantitySelected = useMemo( + () => + chosenNftRewards.find(nft => nft.id === rewardTier?.id)?.quantity ?? 0, + [chosenNftRewards, rewardTier?.id], + ) + const isSelected = quantitySelected > 0 + + const fileUrl = useMemo( + () => + rewardTier?.fileUrl ? ipfsUriToGatewayUrl(rewardTier.fileUrl) : undefined, + [rewardTier?.fileUrl], + ) + + const remainingSupply = rewardTier?.remainingSupply + const hasRemainingSupply = remainingSupply && remainingSupply > 0 + const remainingSupplyText = !hasRemainingSupply + ? t`SOLD OUT` + : rewardTier.maxSupply === DEFAULT_NFT_MAX_SUPPLY + ? t`Unlimited` + : t`${rewardTier?.remainingSupply} remaining` + + const disabled = Boolean( + !hasRemainingSupply || !nftsEnabledForPay || payDisabled, + ) + const disabledReason = useMemo(() => { + if (!hasRemainingSupply) return t`Sold out` + if (!nftsEnabledForPay) return t`NFTs are not enabled for pay` + if (payDisabled) return message + }, [nftsEnabledForPay, hasRemainingSupply, payDisabled, message]) + + const openPreview = () => { + setPreviewVisible(true) + } + + return ( + <> + +
+ + + {!disabled && + (isSelected ? ( + onDeselect()} /> + ) : ( + onSelect(1)} /> + ))} +
+
+ + {rewardTier && !previewDisabled && previewVisible ? ( + onSelect(1)} + onDeselect={onDeselect} + isSelected={isSelected} + /> + } + /> + ) : null} + + ) +} + +export const NftRewardSkeleton = () => ( +
+
+
+
+
+
+
+) diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftThumbnail.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftThumbnail.tsx new file mode 100644 index 0000000000..4a26f9d577 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/NftThumbnail.tsx @@ -0,0 +1,32 @@ +import { JuiceVideoThumbnailOrImage } from 'components/JuiceVideo/JuiceVideoThumbnailOrImage' +import { NftRewardTier } from 'models/nftRewards' +import { twMerge } from 'tailwind-merge' + +export function NftThumbnail({ + fileUrl, + isSelected, + rewardTier, +}: { + fileUrl: string | undefined + isSelected: boolean + rewardTier: NftRewardTier | undefined +}) { + return ( +
+ {fileUrl ? ( + + ) : null} +
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/PreviewAddRemoveNftButton.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/PreviewAddRemoveNftButton.tsx new file mode 100644 index 0000000000..dd3d5a37c9 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/PreviewAddRemoveNftButton.tsx @@ -0,0 +1,58 @@ +import { MinusIcon, PlusIcon } from '@heroicons/react/24/solid' +import { Trans, t } from '@lingui/macro' +import { Button } from 'antd' +import { emitConfirmationDeletionModal } from 'hooks/emitConfirmationDeletionModal' +import { useCallback } from 'react' +import { twMerge } from 'tailwind-merge' + +const iconClasses = 'mr-1 h-6 w-6' +const containerClasses = 'flex items-center justify-center' + +export function PreviewAddRemoveNftButton({ + className, + isSelected, + onSelect, + onDeselect, +}: { + className?: string + isSelected?: boolean + onSelect: VoidFunction + onDeselect: VoidFunction +}) { + const buttonContents = isSelected ? ( +
+ + + Remove NFT + +
+ ) : ( +
+ + + Add NFT + +
+ ) + + const handleDeselect = useCallback(() => { + emitConfirmationDeletionModal({ + onConfirm: onDeselect, + title: t`Remove NFT`, + description: t`Are you sure you want to remove this NFT?`, + }) + }, [onDeselect]) + + return ( + + ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/RemoveNftButton.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/RemoveNftButton.tsx new file mode 100644 index 0000000000..b218c69098 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/NftReward/RemoveNftButton.tsx @@ -0,0 +1,33 @@ +import { TrashIcon } from '@heroicons/react/24/solid' +import { Trans, t } from '@lingui/macro' +import { emitConfirmationDeletionModal } from 'hooks/emitConfirmationDeletionModal' +import { useCallback } from 'react' +import { stopPropagation } from 'react-stop-propagation' +import { twMerge } from 'tailwind-merge' +import { nftHoverButtonClasses } from './AddNftButton' + +// Button that appears when hovering an NFT reward card +export function RemoveNftButton({ onClick }: { onClick: VoidFunction }) { + const handleDeselect = useCallback(() => { + emitConfirmationDeletionModal({ + onConfirm: onClick, + title: t`Remove NFT`, + description: t`Are you sure you want to remove this NFT?`, + }) + }, [onClick]) + + return ( +
+ + + Remove NFT + +
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/NftCreditsSection.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/NftCreditsSection.tsx new file mode 100644 index 0000000000..35ace5f369 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/NftCreditsSection.tsx @@ -0,0 +1,25 @@ +import { Trans } from '@lingui/macro' +import TooltipIcon from 'components/TooltipIcon' +import ETHAmount from 'components/currency/ETHAmount' +import { BigNumber } from 'ethers' + +export function NftCreditsSection({ credits }: { credits: BigNumber }) { + return ( + <> +
+ Your credits +
+
+ credits{' '} + + You have NFT credits from previous payments. Select NFTs to mint + and use your credits. + + } + /> +
+ + ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftTile.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftTile.tsx new file mode 100644 index 0000000000..ba3881d138 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftTile.tsx @@ -0,0 +1,32 @@ +import { Tooltip } from 'antd' +import { JuiceVideoThumbnailOrImage } from 'components/JuiceVideo/JuiceVideoThumbnailOrImage' +import { NftRewardTier } from 'models/nftRewards' +import { useMemo } from 'react' +import { pinataToGatewayUrl } from 'utils/ipfs' + +export function RedeemNftTile({ + rewardTier, + tokenId, +}: { + rewardTier: NftRewardTier | undefined + tokenId: string +}) { + const _name = rewardTier?.name ?? `NFT ${tokenId}` + const fileUrl = useMemo(() => { + if (!rewardTier?.fileUrl) return + return pinataToGatewayUrl(rewardTier.fileUrl) + }, [rewardTier?.fileUrl]) + return ( + +
+ +
+
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftTiles.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftTiles.tsx new file mode 100644 index 0000000000..a5cf4c8391 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftTiles.tsx @@ -0,0 +1,34 @@ +import Loading from 'components/Loading' +import { NfTsQuery } from 'generated/graphql' +import { useJB721DelegateTokenToNftReward } from '../hooks/useJB721DelegateTokenToNftReward' +import { RedeemNftTile } from './RedeemNftTile' + +function RedeemNftTileLoader({ nft }: { nft: NfTsQuery['nfts'][number] }) { + const tokenId = nft.tokenId.toHexString() + const _nft = { + ...nft, + tokenId, + } + const { data: rewardTier } = useJB721DelegateTokenToNftReward(_nft) + if (!rewardTier) + return ( +
+ +
+ ) + return +} + +export function RedeemNftTiles({ + nftAccountBalance, +}: { + nftAccountBalance: NfTsQuery | undefined +}) { + return ( +
+ {nftAccountBalance?.nfts.map((nft, i) => ( + + ))} +
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftsSection.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftsSection.tsx new file mode 100644 index 0000000000..516f96aa61 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftsSection.tsx @@ -0,0 +1,73 @@ +import { useWallet } from 'hooks/Wallet' +import { useState } from 'react' + +export function RedeemNftsSection() { + const [redeemNftsModalVisible, setRedeemNftsModalVisible] = useState(false) + + const { userAddress } = useWallet() + + // TODO: This needs to be implemented + return null + // const { fundingCycleMetadata, primaryTerminalCurrentOverflow } = + // useContext(V2V3ProjectContext) + // const { data, loading } = useNftAccountBalance({ + // accountAddress: userAddress, + // dataSourceAddress: fundingCycleMetadata?.dataSource, + // }) + // const { data: credits, loading: loadingCredits } = useNftCredits(userAddress) + + // const hasOverflow = primaryTerminalCurrentOverflow?.gt(0) + // const hasRedemptionRate = fundingCycleMetadata?.redemptionRate.gt(0) + // const canRedeem = + // hasOverflow && + // hasRedemptionRate && + // fundingCycleMetadata?.useDataSourceForRedeem + + // const hasRedeemableNfts = (data?.nfts?.length ?? 0) > 0 + + // const showRedeemSection = !loading && hasRedeemableNfts && !!userAddress + // const showCreditSection = !loadingCredits && credits && credits.gt(0) + + // if (!showRedeemSection && !showCreditSection) return null + + // return ( + //
+ // {showCreditSection ? ( + //
+ // + //
+ // ) : null} + + // {showRedeemSection ? ( + //
+ //
+ // Your NFTs + //
+ + //
+ // + + // + //
+ + // {redeemNftsModalVisible && ( + // setRedeemNftsModalVisible(false)} + // onConfirmed={() => setRedeemNftsModalVisible(false)} + // /> + // )} + //
+ // ) : null} + //
+ // ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/V4NftRewardsPanel.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/V4NftRewardsPanel.tsx new file mode 100644 index 0000000000..70de871008 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/V4NftRewardsPanel.tsx @@ -0,0 +1,47 @@ +import { Trans, t } from '@lingui/macro' +import { EmptyScreen } from 'components/Project/ProjectTabs/EmptyScreen' +import { NftReward, NftRewardSkeleton } from './NftReward/NftReward' +import { RedeemNftsSection } from './RedeemNftsSection/RedeemNftsSection' +import { useNftRewardsPanel } from './hooks/useNftRewardsPanel' + +export const V4NftRewardsPanel = () => { + const { + rewardTiers, + handleTierSelect, + handleTierDeselect, + loading: nftsLoading, + } = useNftRewardsPanel() + + return ( +
+

+ NFTs +

+ + + {!nftsLoading && rewardTiers?.length ? ( +
+ {rewardTiers?.map((tier, i) => ( +
+ handleTierSelect(tier.id, quantity)} + onDeselect={() => handleTierDeselect(tier.id)} + /> +
+ ))} +
+ ) : nftsLoading ? ( +
+ {[...Array(6)].map((_, i) => ( + + ))} +
+ ) : ( + + )} +
+ ) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/hooks/useJB721DelegateTokenToNftReward.ts b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/hooks/useJB721DelegateTokenToNftReward.ts new file mode 100644 index 0000000000..26542c7ece --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/hooks/useJB721DelegateTokenToNftReward.ts @@ -0,0 +1,37 @@ +import { Nft } from 'generated/graphql' +import { NftRewardTier } from 'models/nftRewards' +import { + NFT_METADATA_CONTRIBUTION_FLOOR_ATTRIBUTES_INDEX, + useJB721DelegateTokenMetadata, +} from 'packages/v2v3/components/V2V3Project/ManageNftsSection/RedeemNftsModal/RedeemNftCard' + +export type RedeemingNft = Pick & { + tokenId: string +} + +export function useJB721DelegateTokenToNftReward(nft: RedeemingNft): { + data: NftRewardTier | undefined +} { + const { data: tierData } = useJB721DelegateTokenMetadata(nft.tokenUri) + const contributionFloor = + tierData?.attributes[NFT_METADATA_CONTRIBUTION_FLOOR_ATTRIBUTES_INDEX] + .value ?? 0 + + return { + data: tierData + ? { + name: tierData.name, + contributionFloor, + id: parseInt(nft.tokenId), + remainingSupply: undefined, + maxSupply: undefined, + fileUrl: tierData.image, + externalLink: undefined, + description: undefined, + beneficiary: undefined, + reservedRate: undefined, + votingWeight: undefined, + } + : undefined, + } +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/hooks/useNftRewardsPanel.ts b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/hooks/useNftRewardsPanel.ts new file mode 100644 index 0000000000..5184ab5a59 --- /dev/null +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/hooks/useNftRewardsPanel.ts @@ -0,0 +1,36 @@ +import { useProjectDispatch } from 'packages/v4/components/ProjectDashboard/redux/hooks' +import { payRedeemActions } from 'packages/v4/components/ProjectDashboard/redux/payRedeemSlice' +import { projectCartActions } from 'packages/v4/components/ProjectDashboard/redux/projectCartSlice' +import { V4NftRewardsContext } from 'packages/v4/contexts/V4NftRewards/V4NftRewardsProvider' +import { useCallback, useContext } from 'react' + +export const useNftRewardsPanel = () => { + const dispatch = useProjectDispatch() + const { + nftRewards: { rewardTiers }, + loading, + } = useContext(V4NftRewardsContext) + + const handleTierSelect = useCallback( + (tierId: number, quantity: number) => { + dispatch(payRedeemActions.changeToPay()) + dispatch(projectCartActions.upsertNftReward({ id: tierId, quantity })) + }, + [dispatch], + ) + + const handleTierDeselect = useCallback( + (tierId: number) => { + dispatch(payRedeemActions.changeToPay()) + dispatch(projectCartActions.removeNftReward({ id: tierId })) + }, + [dispatch], + ) + + return { + rewardTiers, + loading, + handleTierSelect, + handleTierDeselect, + } +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ProjectTabs.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ProjectTabs.tsx index cc99f1436c..0f0534b016 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ProjectTabs.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4ProjectTabs.tsx @@ -1,14 +1,14 @@ -import { Fragment, useEffect, useMemo, useRef, useState } from 'react' - import { Tab } from '@headlessui/react' import { t } from '@lingui/macro' import { ProjectTab } from 'components/Project/ProjectTabs/ProjectTab' import { useOnScreen } from 'hooks/useOnScreen' +import { Fragment, useEffect, useMemo, useRef, useState } from 'react' import { twMerge } from 'tailwind-merge' import { useProjectPageQueries } from '../hooks/useProjectPageQueries' import V4AboutPanel from './V4AboutPanel' import { V4ActivityPanel } from './V4ActivityPanel/V4ActivityPanel' import { V4CyclesPayoutsPanel } from './V4CyclesPayoutsPanel/V4CyclesPayoutsPanel' +import { V4NftRewardsPanel } from './V4NftRewardsPanel/V4NftRewardsPanel' import { V4TokensPanel } from './V4TokensPanel/V4TokensPanel' type ProjectTabConfig = { @@ -21,6 +21,8 @@ type ProjectTabConfig = { export const V4ProjectTabs = ({ className }: { className?: string }) => { const { projectPageTab, setProjectPageTab } = useProjectPageQueries() + const showNftRewards = true + const containerRef = useRef(null) const panelRef = useRef(null) const isPanelVisible = useOnScreen(panelRef) @@ -48,12 +50,12 @@ export const V4ProjectTabs = ({ className }: { className?: string }) => { () => [ { id: 'activity', name: t`Activity`, panel: }, { id: 'about', name: t`About`, panel: }, - // { - // id: 'nft_rewards', - // name: t`NFTs`, - // panel: , - // hideTab: !showNftRewards, - // }, + { + id: 'nft_rewards', + name: t`NFTs`, + panel: , + hideTab: !showNftRewards, + }, { id: 'cycle_payouts', name: t`Cycles & Payouts`, @@ -61,7 +63,7 @@ export const V4ProjectTabs = ({ className }: { className?: string }) => { }, { id: 'tokens', name: t`Tokens`, panel: }, ], - [], + [showNftRewards], ) const selectedTabIndex = useMemo(() => { diff --git a/src/pages/v4/[chainName]/p/[projectId]/index.tsx b/src/pages/v4/[chainName]/p/[projectId]/index.tsx index 0bf163c12e..bf74becab7 100644 --- a/src/pages/v4/[chainName]/p/[projectId]/index.tsx +++ b/src/pages/v4/[chainName]/p/[projectId]/index.tsx @@ -5,6 +5,7 @@ import { JBChainId, JBProjectProvider } from 'juice-sdk-react' import { useRouter } from 'next/router' import { ReduxProjectCartProvider } from 'packages/v4/components/ProjectDashboard/ReduxProjectCartProvider' import store from 'packages/v4/components/ProjectDashboard/redux/store' +import { V4NftRewardsProvider } from 'packages/v4/contexts/V4NftRewards/V4NftRewardsProvider' import V4ProjectMetadataProvider from 'packages/v4/contexts/V4ProjectMetadataProvider' import { useCurrentRouteChainId } from 'packages/v4/hooks/useCurrentRouteChainId' import { V4ProjectDashboard } from 'packages/v4/views/V4ProjectDashboard/V4ProjectDashboard' @@ -71,7 +72,9 @@ const Providers: React.FC< > - {children} + + {children} + diff --git a/src/utils/nftRewards.ts b/src/utils/nftRewards.ts index 628d98d2ee..6ba1ee9218 100644 --- a/src/utils/nftRewards.ts +++ b/src/utils/nftRewards.ts @@ -23,6 +23,7 @@ import { import { DEFAULT_NFT_MAX_SUPPLY } from 'packages/v2v3/constants/nftRewards' import { JB721DelegateVersion } from 'packages/v2v3/models/contracts' import { V2V3CurrencyOption } from 'packages/v2v3/models/currencyOption' +import { JB721TierV4 } from 'packages/v4/contexts/V4NftRewards/V4NftRewardsProvider' import { decodeEncodedIpfsUri, encodeIpfsUri, ipfsUri } from 'utils/ipfs' export function sortNftsByContributionFloor( @@ -67,7 +68,11 @@ export function getNftRewardOfFloor({ // returns an array of CIDs from a given array of RewardTier obj's export function CIDsOfNftRewardTiersResponse( - nftRewardTiersResponse: JB721TierV3[] | JB_721_TIER_V3_2[] | undefined, + nftRewardTiersResponse: + | JB721TierV3[] + | JB_721_TIER_V3_2[] + | readonly JB721TierV4[] + | undefined, ): string[] { const cids = nftRewardTiersResponse