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 (
+
+ )
+}
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
+
+
+ ) : (
+
+ )
+
+ 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