From a173ab97d692644465acec1211db13240e46c944 Mon Sep 17 00:00:00 2001 From: Wraeth Date: Fri, 29 Nov 2024 10:48:08 +1000 Subject: [PATCH] Apply v4 NFT credit on checkout --- src/locales/messages.pot | 3 - .../ReduxProjectCartProvider.tsx | 15 +- .../PayProjectModal/PayProjectModal.tsx | 80 ++++++++--- .../PayProjectModal/hooks/usePayAmounts.ts | 132 ++++++++++++++++++ .../usePayProjectModal/usePayProjectTx.ts | 22 ++- .../V4PayRedeemCard/V4NftCreditsCallouts.tsx | 8 +- .../v4/contexts/V4UserNftCreditsProvider.tsx | 37 +++++ .../RedeemNftsSection/NftCreditsSection.tsx | 5 +- .../RedeemNftsSection/RedeemNftsSection.tsx | 85 +++++------ .../v4/[chainName]/p/[projectId]/index.tsx | 11 +- 10 files changed, 309 insertions(+), 89 deletions(-) create mode 100644 src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/usePayAmounts.ts create mode 100644 src/packages/v4/contexts/V4UserNftCreditsProvider.tsx diff --git a/src/locales/messages.pot b/src/locales/messages.pot index 911a89df0c..b2e94cd120 100644 --- a/src/locales/messages.pot +++ b/src/locales/messages.pot @@ -1229,9 +1229,6 @@ msgstr "" msgid "Issue as ERC-20" msgstr "" -msgid "Pay {primaryAmount}" -msgstr "" - msgid "Active" msgstr "" diff --git a/src/packages/v4/components/ProjectDashboard/ReduxProjectCartProvider.tsx b/src/packages/v4/components/ProjectDashboard/ReduxProjectCartProvider.tsx index ed5ae57af0..e84f41f073 100644 --- a/src/packages/v4/components/ProjectDashboard/ReduxProjectCartProvider.tsx +++ b/src/packages/v4/components/ProjectDashboard/ReduxProjectCartProvider.tsx @@ -1,6 +1,5 @@ -import { useWallet } from 'hooks/Wallet' -import { useReadJb721TiersHookPayCreditsOf } from 'juice-sdk-react' import { useV4NftRewards } from 'packages/v4/contexts/V4NftRewards/V4NftRewardsProvider' +import { useV4UserNftCredits } from 'packages/v4/contexts/V4UserNftCreditsProvider' import { V4CurrencyOption } from 'packages/v4/models/v4CurrencyOption' import React from 'react' import { useProjectDispatch } from './redux/hooks' @@ -25,12 +24,7 @@ export const ReduxProjectCartProvider = ({ const { nftRewards: { rewardTiers }, } = useV4NftRewards() - - const { userAddress } = useWallet() - const { data: nftCredits } = useReadJb721TiersHookPayCreditsOf({ - address: userAddress, - }) - + const nftCredits = useV4UserNftCredits() const dispatch = useProjectDispatch() // Set the nfts on load @@ -40,8 +34,9 @@ export const ReduxProjectCartProvider = ({ // Set the user's NFT credits on load React.useEffect(() => { - dispatch(projectCartActions.setUserNftCredits(nftCredits ?? 0n)) - }, [dispatch, nftCredits]) + if (nftCredits.isLoading) return + dispatch(projectCartActions.setUserNftCredits(nftCredits.data ?? 0n)) + }, [dispatch, nftCredits.isLoading, nftCredits.data]) return <>{children} } diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/PayProjectModal.tsx b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/PayProjectModal.tsx index 466aede953..0fe15b45fa 100644 --- a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/PayProjectModal.tsx +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/PayProjectModal.tsx @@ -3,11 +3,13 @@ import EtherscanLink from 'components/EtherscanLink' import ExternalLink from 'components/ExternalLink' import { JuiceModal } from 'components/modals/JuiceModal' import { Formik } from 'formik' -import Image from "next/legacy/image" +import Image from 'next/legacy/image' +import { useV4UserNftCredits } from 'packages/v4/contexts/V4UserNftCreditsProvider' import { twMerge } from 'tailwind-merge' import { helpPagePath } from 'utils/helpPagePath' import { MessageSection } from './components/MessageSection' import { ReceiveSection } from './components/ReceiveSection' +import { usePayAmounts } from './hooks/usePayAmounts' import { PayProjectModalFormValues, usePayProjectModal, @@ -16,8 +18,6 @@ import { export const PayProjectModal: React.FC = () => { const { open, - primaryAmount, - secondaryAmount, validationSchema, isTransactionPending, isTransactionConfirmed, @@ -27,6 +27,7 @@ export const PayProjectModal: React.FC = () => { setOpen, onPaySubmit, } = usePayProjectModal() + const { formattedTotalAmount } = usePayAmounts() return ( @@ -50,7 +51,7 @@ export const PayProjectModal: React.FC = () => { position="top" okLoading={props.isSubmitting || isTransactionPending} okButtonForm="PayProjectModalForm" - okText={t`Pay ${primaryAmount}`} + okText={t`Pay ${formattedTotalAmount.primaryAmount}`} cancelText={ isTransactionPending || isTransactionConfirmed ? t`Close` @@ -97,19 +98,7 @@ export const PayProjectModal: React.FC = () => { ) : ( <>
-
- - Total amount - -
- {primaryAmount}{' '} - {secondaryAmount && ( - - ({secondaryAmount}) - - )} -
-
+ @@ -172,3 +161,60 @@ export const PayProjectModal: React.FC = () => { ) } + +const AmountSection = () => { + const { data: nftCredits } = useV4UserNftCredits() + const { formattedAmount, formattedNftCredits, formattedTotalAmount } = + usePayAmounts() + + const RowData = ({ + label, + primaryAmount, + secondaryAmount, + }: { + label: React.ReactNode + primaryAmount: React.ReactNode + secondaryAmount: React.ReactNode + }) => ( +
+ {label} +
+ {primaryAmount}{' '} + {secondaryAmount && ( + + ({secondaryAmount}) + + )} +
+
+ ) + + if (!nftCredits || nftCredits <= 0n || !formattedNftCredits) + return ( + + ) + + return ( +
+ + + +
+ ) +} diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/usePayAmounts.ts b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/usePayAmounts.ts new file mode 100644 index 0000000000..2ea6141cc6 --- /dev/null +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/PayProjectModal/hooks/usePayAmounts.ts @@ -0,0 +1,132 @@ +import { useCurrencyConverter } from 'hooks/useCurrencyConverter' +import { useV4UserNftCredits } from 'packages/v4/contexts/V4UserNftCreditsProvider' +import { V4_CURRENCY_ETH, V4_CURRENCY_USD } from 'packages/v4/utils/currency' +import { formatCurrencyAmount } from 'packages/v4/utils/formatCurrencyAmount' +import React from 'react' +import { fromWad, parseWad } from 'utils/format/formatNumber' +import { useProjectSelector } from '../../../redux/hooks' +import { usePayProjectModal } from './usePayProjectModal/usePayProjectModal' + +export const usePayAmounts = () => { + const converter = useCurrencyConverter() + const { payAmount } = useProjectSelector(state => state.projectCart) + const { primaryAmount, secondaryAmount } = usePayProjectModal() + const { data: nftCreditsData } = useV4UserNftCredits() + + const payAmountRaw = React.useMemo(() => { + if (!payAmount) return + + switch (payAmount.currency) { + case V4_CURRENCY_ETH: + return { + eth: parseWad(payAmount.amount), + usd: converter.weiToUsd(parseWad(payAmount.amount))!, + } + case V4_CURRENCY_USD: + return { + eth: converter.usdToWei(payAmount.amount), + usd: parseWad(payAmount.amount), + } + } + }, [converter, payAmount]) + + const appliedNFTCreditsRaw = React.useMemo(() => { + if (!payAmountRaw || !nftCreditsData) return + + const nftCreditsApplied = payAmountRaw.eth.lt(nftCreditsData) + ? payAmountRaw.eth + : nftCreditsData + + const eth = nftCreditsApplied + const usd = parseWad(converter.weiToUsd(nftCreditsApplied))! + + return { + eth, + usd, + } + }, [converter, nftCreditsData, payAmountRaw]) + + const formattedNftCredits = React.useMemo(() => { + if (!appliedNFTCreditsRaw || !payAmount) return + + switch (payAmount.currency) { + case V4_CURRENCY_ETH: + return { + primaryAmount: formatCurrencyAmount({ + amount: fromWad(appliedNFTCreditsRaw.eth), + currency: V4_CURRENCY_ETH, + }), + secondaryAmount: formatCurrencyAmount({ + amount: fromWad(appliedNFTCreditsRaw.usd), + currency: V4_CURRENCY_USD, + }), + } + case V4_CURRENCY_USD: + return { + primaryAmount: formatCurrencyAmount({ + amount: fromWad(appliedNFTCreditsRaw.usd), + currency: V4_CURRENCY_USD, + }), + secondaryAmount: formatCurrencyAmount({ + amount: fromWad(appliedNFTCreditsRaw.eth), + currency: V4_CURRENCY_ETH, + }), + } + } + }, [appliedNFTCreditsRaw, payAmount]) + + const formattedTotalAmount = React.useMemo(() => { + if (!payAmountRaw || !payAmount) return + + if (!appliedNFTCreditsRaw) { + return { + primaryAmount: primaryAmount, + secondaryAmount: secondaryAmount, + } + } + + const totalEth = payAmountRaw.eth.sub(appliedNFTCreditsRaw.eth) + const totalUsd = converter.weiToUsd(parseWad(totalEth)) + + const formattedEth = formatCurrencyAmount({ + amount: fromWad(totalEth), + currency: V4_CURRENCY_ETH, + }) + const formattedUsd = formatCurrencyAmount({ + amount: fromWad(totalUsd), + currency: V4_CURRENCY_USD, + }) + + switch (payAmount?.currency) { + case V4_CURRENCY_ETH: + return { + primaryAmount: formattedEth, + secondaryAmount: formattedUsd, + } + case V4_CURRENCY_USD: + return { + primaryAmount: formattedUsd, + secondaryAmount: formattedEth, + } + } + }, [ + appliedNFTCreditsRaw, + converter, + payAmount, + payAmountRaw, + primaryAmount, + secondaryAmount, + ]) + + return { + formattedAmount: { primaryAmount, secondaryAmount }, + formattedNftCredits: { + primaryAmount: formattedNftCredits?.primaryAmount, + secondaryAmount: formattedNftCredits?.secondaryAmount, + }, + formattedTotalAmount: { + primaryAmount: formattedTotalAmount?.primaryAmount, + secondaryAmount: formattedTotalAmount?.secondaryAmount, + }, + } +} 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 1909d8d166..2f339c92cb 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 @@ -12,6 +12,7 @@ import { } from 'juice-sdk-react' import { useProjectSelector } from 'packages/v4/components/ProjectDashboard/redux/hooks' import { useV4NftRewards } from 'packages/v4/contexts/V4NftRewards/V4NftRewardsProvider' +import { useV4UserNftCredits } from 'packages/v4/contexts/V4UserNftCreditsProvider' import { V4_CURRENCY_ETH } from 'packages/v4/utils/currency' import { ProjectPayReceipt } from 'packages/v4/views/V4ProjectDashboard/hooks/useProjectPageQueries' import { wagmiConfig } from 'packages/v4/wagmiConfig' @@ -39,6 +40,7 @@ export const usePayProjectTx = ({ ) => void }) => { const { userAddress } = useWallet() + const { data: nftCredits } = useV4UserNftCredits() const { payAmount, chosenNftRewards } = useProjectSelector( state => state.projectCart, ) @@ -70,12 +72,21 @@ export const usePayProjectTx = ({ const weiAmount = useMemo(() => { if (!payAmount) { return 0n - } else if (payAmount.currency === V4_CURRENCY_ETH) { - return parseEther(payAmount.amount.toString()) - } else { - return converter.usdToWei(payAmount.amount).toBigInt() } - }, [payAmount, converter]) + let weiAmount = + payAmount.currency === V4_CURRENCY_ETH + ? parseEther(payAmount.amount.toString()) + : converter.usdToWei(payAmount.amount).toBigInt() + if (nftCredits) { + if (nftCredits >= weiAmount) { + weiAmount = 0n + } else { + weiAmount -= nftCredits + } + } + + return weiAmount + }, [converter, nftCredits, payAmount]) const { rulesetMetadata: { data: rulesetMetadata }, @@ -103,7 +114,6 @@ export const usePayProjectTx = ({ formikHelpers: FormikHelpers, ) => { if ( - !weiAmount || !contracts.primaryNativeTerminal.data || !userAddress || !values.userAcceptsTerms diff --git a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4NftCreditsCallouts.tsx b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4NftCreditsCallouts.tsx index 9930cdb7bb..787d11e527 100644 --- a/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4NftCreditsCallouts.tsx +++ b/src/packages/v4/components/ProjectDashboard/V4PayRedeemCard/V4NftCreditsCallouts.tsx @@ -1,17 +1,13 @@ 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 { useV4UserNftCredits } from 'packages/v4/contexts/V4UserNftCreditsProvider' import { useProjectPageQueries } from 'packages/v4/views/V4ProjectDashboard/hooks/useProjectPageQueries' export function V4NftCreditsCallouts() { const { setProjectPageTab } = useProjectPageQueries() - const { userAddress } = useWallet() - const { data: nftCredits } = useReadJb721TiersHookPayCreditsOf({ - address: userAddress, - }) + const { data: nftCredits } = useV4UserNftCredits() if (!nftCredits || nftCredits <= 0n) { return null diff --git a/src/packages/v4/contexts/V4UserNftCreditsProvider.tsx b/src/packages/v4/contexts/V4UserNftCreditsProvider.tsx new file mode 100644 index 0000000000..d9bcaebb05 --- /dev/null +++ b/src/packages/v4/contexts/V4UserNftCreditsProvider.tsx @@ -0,0 +1,37 @@ +import { useWallet } from 'hooks/Wallet' +import { + useJBRulesetContext, + useReadJb721TiersHookPayCreditsOf, +} from 'juice-sdk-react' +import React, { PropsWithChildren } from 'react' + +const V4UserNftCreditsContext = React.createContext<{ + data: bigint | undefined + isLoading: boolean +}>({ + data: undefined, + isLoading: false, +}) + +export const V4UserNftCreditsProvider: React.FC = ({ + children, +}) => { + const { userAddress } = useWallet() + const { + rulesetMetadata: { data: rulesetMetadata }, + } = useJBRulesetContext() + const creds = useReadJb721TiersHookPayCreditsOf({ + address: rulesetMetadata?.dataHook, + args: userAddress ? [userAddress] : undefined, + }) + + return ( + + {children} + + ) +} + +export const useV4UserNftCredits = () => { + return React.useContext(V4UserNftCreditsContext) +} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/NftCreditsSection.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/NftCreditsSection.tsx index 35ace5f369..22f2b12fb4 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/NftCreditsSection.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/NftCreditsSection.tsx @@ -3,14 +3,15 @@ import TooltipIcon from 'components/TooltipIcon' import ETHAmount from 'components/currency/ETHAmount' import { BigNumber } from 'ethers' -export function NftCreditsSection({ credits }: { credits: BigNumber }) { +export function NftCreditsSection({ credits }: { credits: bigint }) { return ( <>
Your credits
- credits{' '} + {/* // TODO: make ETHAmount take BigInts */} + credits{' '} diff --git a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftsSection.tsx b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftsSection.tsx index 516f96aa61..a436a4aead 100644 --- a/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftsSection.tsx +++ b/src/packages/v4/views/V4ProjectDashboard/V4ProjectTabs/V4NftRewardsPanel/RedeemNftsSection/RedeemNftsSection.tsx @@ -1,13 +1,12 @@ -import { useWallet } from 'hooks/Wallet' -import { useState } from 'react' +import { useV4UserNftCredits } from 'packages/v4/contexts/V4UserNftCreditsProvider' +import { NftCreditsSection } from './NftCreditsSection' export function RedeemNftsSection() { - const [redeemNftsModalVisible, setRedeemNftsModalVisible] = useState(false) + // const [redeemNftsModalVisible, setRedeemNftsModalVisible] = useState(false) - const { userAddress } = useWallet() + const nftCredits = useV4UserNftCredits() // TODO: This needs to be implemented - return null // const { fundingCycleMetadata, primaryTerminalCurrentOverflow } = // useContext(V2V3ProjectContext) // const { data, loading } = useNftAccountBalance({ @@ -26,48 +25,50 @@ export function RedeemNftsSection() { // const hasRedeemableNfts = (data?.nfts?.length ?? 0) > 0 // const showRedeemSection = !loading && hasRedeemableNfts && !!userAddress - // const showCreditSection = !loadingCredits && credits && credits.gt(0) + const showCreditSection = + !nftCredits.isLoading && nftCredits.data && nftCredits.data > 0n // if (!showRedeemSection && !showCreditSection) return null - // return ( - //
- // {showCreditSection ? ( - //
- // - //
- // ) : null} + return ( +
+ {showCreditSection ? ( +
+ +
+ ) : null} - // {showRedeemSection ? ( - //
- //
- // Your NFTs - //
+ {/* TODO: Redeem nft section */} + {/* {showRedeemSection ? ( +
+
+ Your NFTs +
- //
- // +
+ - // - //
+ +
- // {redeemNftsModalVisible && ( - // setRedeemNftsModalVisible(false)} - // onConfirmed={() => setRedeemNftsModalVisible(false)} - // /> - // )} - //
- // ) : null} - //
- // ) + {redeemNftsModalVisible && ( + setRedeemNftsModalVisible(false)} + onConfirmed={() => setRedeemNftsModalVisible(false)} + /> + )} +
+ ) : null} */} +
+ ) } diff --git a/src/pages/v4/[chainName]/p/[projectId]/index.tsx b/src/pages/v4/[chainName]/p/[projectId]/index.tsx index bf74becab7..07a3dd836b 100644 --- a/src/pages/v4/[chainName]/p/[projectId]/index.tsx +++ b/src/pages/v4/[chainName]/p/[projectId]/index.tsx @@ -7,6 +7,7 @@ import { ReduxProjectCartProvider } from 'packages/v4/components/ProjectDashboar 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 { V4UserNftCreditsProvider } from 'packages/v4/contexts/V4UserNftCreditsProvider' import { useCurrentRouteChainId } from 'packages/v4/hooks/useCurrentRouteChainId' import { V4ProjectDashboard } from 'packages/v4/views/V4ProjectDashboard/V4ProjectDashboard' import { wagmiConfig } from 'packages/v4/wagmiConfig' @@ -72,9 +73,13 @@ const Providers: React.FC< > - - {children} - + + + + {children} + + +