diff --git a/lang/ca.json b/lang/ca.json index e882174206..13bf4d2ace 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -7,7 +7,6 @@ "bipoc-comomunities": "Comunitats BIPOC", "children-health": "Salut Infantil", "climate-action": "Acció climàtica", - "label.state": "Estat", "component.already_donated.incorrect_estimate": "You have already donated to this project so the estimated matching will be incorrect.", "component.already_donated.once_more": "Already Donated! Donate once more", "component.archive_cover.archived": "ARCHIVED", @@ -592,6 +591,7 @@ "label.mission_vission": "Misió i Visió", "label.modify_flow_rate": "Modificar la taxa de flux", "label.modify_recurring_donation": "Modificar Donació Recurrent", + "label.modify_recurring_donation_amount": "Modifica l'import de la donació recurrent", "label.modify_stream_balance": "Modificar el Saldo de la Transmissió", "label.month": "{count, plural, one { Mes} other { Mesos} }", "label.monthly": "Mensualment", @@ -872,14 +872,17 @@ "label.start_donating.desc": "Giveth és el lloc per donar o recaptar fons per a projectes increïbles sense comissions.", "label.start_new_donation": "Iniciar nova donació", "label.start_referring!": "Start Referring!", + "label.state": "Estat", "label.status": "Estat", "label.streamed_rewards": "Recompenses en streaming", "label.streaming": "Streaming", "label.streams_powered_by": "Fluxos impulsats per", - "label.stream_balance": "Diposita Tokens o utilitza el Saldo de Transmissió", + "label.deposit_token_use_balance": "Diposita Tokens o utilitza el Saldo de Transmissió", + "label.stream_balance": "Saldo de la Transmissió", "label.stream_balances": "Saldos de la Transmissió", "label.stream_balances_description": "Tokens que ja has dipositat per a streaming. Pots utilitzar-los per crear donacions recurrents.", - "label.stream_balance_runs_out_in": "Recarrega el teu Saldo de Transmissió dins de", + "label.top_up_your_stream_balance_within": "Recarrega el teu Saldo de Transmissió dins de", + "label.stream_balance_runs_out_in": "El saldo de la transmissió s'esgota en", "label.stream_flowrate": "caudal del fluxe", "label.stream_flowrate_when_you_claim": "caudal del fluxe quan reclames les recompenses líquides!", "label.stream_progress": "Progrés del {token}stream", diff --git a/lang/en.json b/lang/en.json index 5addee030e..2c1890b4b6 100644 --- a/lang/en.json +++ b/lang/en.json @@ -5,7 +5,6 @@ "animals": "Animals", "art": "Art", "bipoc-comomunities": "Bipoc Communities", - "label.state": "State", "children-health": "Children's Health", "climate-action": "Climate Action", "component.already_donated.incorrect_estimate": "You have already donated to this project so the estimated matching will be incorrect.", @@ -592,6 +591,7 @@ "label.mission_vission": "Mission & Vision", "label.modify_flow_rate": "Modify flow rate", "label.modify_recurring_donation": "Modify Recurring Donation", + "label.modify_recurring_donation_amount": "Modify Recurring Donation Amount", "label.modify_stream_balance": "Modify Stream Balance", "label.month": "{count, plural, one { Month} other { Months} }", "label.monthly": "Monthly", @@ -872,14 +872,17 @@ "label.start_donating.desc": "Giveth is the place to donate to or raise funds for awesome projects with zero added feeds.", "label.start_new_donation": "Start new donation", "label.start_referring!": "Start Referring!", + "label.state": "State", "label.status": "Status", "label.streamed_rewards": "Streamed Rewards", "label.streaming": "Streaming", "label.streams_powered_by": "Streams powered by", - "label.stream_balance": "Deposit Tokens or use Stream Balance", + "label.deposit_token_use_balance": "Deposit Tokens or use Stream Balance", + "label.stream_balance": "Stream Balance", "label.stream_balances": "Streamable Token Balances", "label.stream_balances_description": "Tokens you already deposited for streaming. You can use them to create recurring donations.", - "label.stream_balance_runs_out_in": "Top-up your Stream balance within", + "label.stream_balance_runs_out_in": "Stream balance runs out in", + "label.top_up_your_stream_balance_within": "Top-up your Stream balance within", "label.stream_flowrate": "stream flowrate", "label.stream_flowrate_when_you_claim": "stream flowrate when you claim liquid rewards!", "label.stream_progress": "{token}stream progress", diff --git a/lang/es.json b/lang/es.json index 975bd3ab98..abb5aeba43 100644 --- a/lang/es.json +++ b/lang/es.json @@ -7,7 +7,6 @@ "bipoc-comomunities": "Comunidades Bipoc", "children-health": "Salud de los niños", "climate-action": "Acción Climática", - "label.state": "Estado", "component.already_donated.incorrect_estimate": "Ya has donado a este proyecto, por lo que la estimación de monto complementado será incorrecta.", "component.already_donated.once_more": "¡Ya has donado! Dona una vez más", "component.archive_cover.archived": "ARCHIVADO", @@ -592,6 +591,7 @@ "label.mission_vission": "Misión & Visión", "label.modify_flow_rate": "Modificar la tasa de flujo", "label.modify_recurring_donation": "Modificar Donación Recurrente", + "label.modify_recurring_donation_amount": "Modificar la cantidad de donación recurrente", "label.modify_stream_balance": "Modificar el Saldo de Transmisión", "label.month": "{count, plural, one { Mes} other { Meses} }", "label.monthly": "Mensualmente", @@ -872,14 +872,17 @@ "label.start_donating.desc": "Giveth es el lugar para donar o recibir financiación a proyectos increíbles sin comisiones.", "label.start_new_donation": "Iniciar nueva donación", "label.start_referring!": "¡Comienza a referir!", + "label.state": "Estado", "label.status": "Estado", "label.streamed_rewards": "Recompensas en streaming", "label.streaming": "Flujo", "label.streams_powered_by": "Transmisiones potenciadas por", - "label.stream_balance": "Deposita Tokens o usa el Saldo de Transmisión", + "label.deposit_token_use_balance": "Deposita Tokens o usa el Saldo de Transmisión", + "label.stream_balance": "Saldo de Transmisión", "label.stream_balances": "Saldos de Transmisión", "label.stream_balances_description": "Tokens que ya has depositado para streaming. Puedes usarlos para crear donaciones recurrentes.", - "label.stream_balance_runs_out_in": "Recarga tu Saldo de Transmisión dentro de", + "label.top_up_your_stream_balance_within": "Recarga tu Saldo de Transmisión dentro de", + "label.stream_balance_runs_out_in": "El Saldo de Transmisión se agota en", "label.stream_flowrate": "flujo de stream", "label.stream_flowrate_when_you_claim": "flujo de stream cuando reclamas recompensas líquidas!", "label.stream_progress": "Progreso del {token}stream", diff --git a/src/apollo/gql/gqlSuperfluid.ts b/src/apollo/gql/gqlSuperfluid.ts index 5d63e2dbd8..8feddd79f7 100644 --- a/src/apollo/gql/gqlSuperfluid.ts +++ b/src/apollo/gql/gqlSuperfluid.ts @@ -46,10 +46,11 @@ export const UPDATE_RECURRING_DONATION = gql` mutation updateRecurringDonationQuery( $projectId: Int! $networkId: Int! - $txHash: String! - $flowRate: String! $currency: String! - $anonymous: Boolean! + $txHash: String + $flowRate: String + $anonymous: Boolean + $status: String ) { updateRecurringDonationParams( projectId: $projectId @@ -58,12 +59,14 @@ export const UPDATE_RECURRING_DONATION = gql` anonymous: $anonymous flowRate: $flowRate currency: $currency + status: $status ) { txHash networkId currency flowRate anonymous + status } } `; diff --git a/src/apollo/gql/gqlUser.ts b/src/apollo/gql/gqlUser.ts index f8579eecac..16104cb6bb 100644 --- a/src/apollo/gql/gqlUser.ts +++ b/src/apollo/gql/gqlUser.ts @@ -144,6 +144,10 @@ export const FETCH_USER_RECURRING_DONATIONS = gql` id title slug + anchorContracts { + address + isActive + } } finished createdAt diff --git a/src/apollo/types/types.ts b/src/apollo/types/types.ts index e755b564b3..6efc955d6f 100644 --- a/src/apollo/types/types.ts +++ b/src/apollo/types/types.ts @@ -282,6 +282,7 @@ export interface IWalletRecurringDonation { totalDonated: string; networkId: number; finished: boolean; + anonymous: boolean; } export interface IMediumBlogPost { @@ -473,3 +474,12 @@ export interface IGetQfRoundHistory { uniqueDonors: number; estimatedMatching: IEstimatedMatching; } + +export enum RECURRING_DONATION_STATUS { + PENDING = 'pending', + VERIFIED = 'verified', + ENDED = 'ended', + FAILED = 'failed', + ARCHIVED = 'archived', + ACTIVE = 'active', +} diff --git a/src/components/ToggleSwitch.tsx b/src/components/ToggleSwitch.tsx index b39b3cf6cb..8ef891b31b 100644 --- a/src/components/ToggleSwitch.tsx +++ b/src/components/ToggleSwitch.tsx @@ -26,7 +26,7 @@ const ToggleSwitch: FC = ({ $disabled={disabled} className={className} > - + {}} /> diff --git a/src/components/views/donate/ModifySuperToken/DepositSuperToken.tsx b/src/components/views/donate/ModifySuperToken/DepositSuperToken.tsx index 232b84867b..100f7a7f28 100644 --- a/src/components/views/donate/ModifySuperToken/DepositSuperToken.tsx +++ b/src/components/views/donate/ModifySuperToken/DepositSuperToken.tsx @@ -193,8 +193,10 @@ export const DepositSuperToken: FC = ({ token={token!} /> diff --git a/src/components/views/donate/ModifySuperToken/StreamInfo.tsx b/src/components/views/donate/ModifySuperToken/StreamInfo.tsx index f7d0c27507..fba5eaa604 100644 --- a/src/components/views/donate/ModifySuperToken/StreamInfo.tsx +++ b/src/components/views/donate/ModifySuperToken/StreamInfo.tsx @@ -36,7 +36,7 @@ export const StreamInfo: FC = ({ {formatMessage({ - id: 'label.stream_balance', + id: 'label.deposit_token_use_balance', })} diff --git a/src/components/views/donate/ModifySuperToken/WithDrawSuperToken.tsx b/src/components/views/donate/ModifySuperToken/WithDrawSuperToken.tsx index db71849197..96f2f0303b 100644 --- a/src/components/views/donate/ModifySuperToken/WithDrawSuperToken.tsx +++ b/src/components/views/donate/ModifySuperToken/WithDrawSuperToken.tsx @@ -161,8 +161,10 @@ export const WithDrawSuperToken: FC = ({ token={token!} /> diff --git a/src/components/views/donate/RecurringDonationCard.tsx b/src/components/views/donate/RecurringDonationCard.tsx index 1606900041..f39c589e49 100644 --- a/src/components/views/donate/RecurringDonationCard.tsx +++ b/src/components/views/donate/RecurringDonationCard.tsx @@ -55,7 +55,7 @@ import links from '@/lib/constants/links'; * If the slider value is between 90 and 100, it maps it to a range of 50 to 100. * This makes the first 90% of the slider represent 0-50% of the range, and the last 10% represent 50-100%. */ -function mapValue(value: number) { +export function mapValue(value: number) { if (value <= 90) { return value * (50 / 90); } else { @@ -70,7 +70,7 @@ function mapValue(value: number) { * If the value is between 50 and 100, it maps it to a range of 90 to 100. * This is used to set the slider's position based on the value from the range. */ -function mapValueInverse(value: number) { +export function mapValueInverse(value: number) { if (value <= 50) { return value * (90 / 50); } else { @@ -135,10 +135,16 @@ export const RecurringDonationCard = () => { const tokenBalance = balance?.value; const tokenStream = tokenStreams[selectedToken?.token.id || '']; const totalStreamPerSec = - tokenStream?.reduce( - (acc, stream) => acc + BigInt(stream.currentFlowRate), - totalPerSec, - ) || totalPerSec; + tokenStream + ?.filter( + ts => + project.anchorContracts?.length > 0 && + ts.receiver.id !== project.anchorContracts[0]?.address, + ) + .reduce( + (acc, stream) => acc + BigInt(stream.currentFlowRate), + totalPerSec, + ) || totalPerSec; const streamRunOutInMonth = totalStreamPerSec > 0 ? amount / totalStreamPerSec / ONE_MONTH_SECONDS @@ -225,7 +231,9 @@ export const RecurringDonationCard = () => { - {formatMessage({ id: 'label.stream_balance' })} + {formatMessage({ + id: 'label.deposit_token_use_balance', + })} } @@ -326,16 +334,16 @@ export const RecurringDonationCard = () => { min={0} max={100} step={0.1} - railStyle={{ - backgroundColor: sliderColor[200], - }} - trackStyle={{ - backgroundColor: sliderColor[500], - }} - handleStyle={{ - backgroundColor: sliderColor[500], - border: `3px solid ${sliderColor[200]}`, - opacity: 1, + styles={{ + rail: { backgroundColor: sliderColor[200] }, + track: { + backgroundColor: sliderColor[500], + }, + handle: { + backgroundColor: sliderColor[500], + border: `3px solid ${sliderColor[200]}`, + opacity: 1, + }, }} onChange={(value: any) => { const _value = Array.isArray(value) @@ -380,7 +388,7 @@ export const RecurringDonationCard = () => { {formatMessage({ - id: 'label.stream_balance_runs_out_in', + id: 'label.top_up_your_stream_balance_within', })} {selectedToken?.token.isSuperToken && ( @@ -693,16 +701,16 @@ const RecurringSection = styled(Flex)` // text-align: left; // `; -const SelectTokenWrapper = styled(Flex)` +export const SelectTokenWrapper = styled(Flex)` cursor: pointer; gap: 16px; `; -const SelectTokenPlaceHolder = styled(B)` +export const SelectTokenPlaceHolder = styled(B)` white-space: nowrap; `; -const InputWrapper = styled(Flex)` +export const InputWrapper = styled(Flex)` border: 2px solid ${neutralColors.gray[300]}; border-radius: 8px; overflow: hidden; @@ -727,7 +735,7 @@ const Input = styled(AmountInput)` } `; -const IconWrapper = styled.div` +export const IconWrapper = styled.div` cursor: pointer; color: ${brandColors.giv[500]}; `; diff --git a/src/components/views/donate/RecurringDonationModal/RecurringDonationModal.tsx b/src/components/views/donate/RecurringDonationModal/RecurringDonationModal.tsx index 86d1c137c0..0a96999c3d 100644 --- a/src/components/views/donate/RecurringDonationModal/RecurringDonationModal.tsx +++ b/src/components/views/donate/RecurringDonationModal/RecurringDonationModal.tsx @@ -442,8 +442,8 @@ const RecurringDonationInnerModal: FC = ({ )} = ({ - amount, - totalPerMonth, + superTokenBalance, + streamFlowRatePerMonth, symbol, }) => { const { formatMessage } = useIntl(); - const totalPerSecond = totalPerMonth / ONE_MONTH_SECONDS; + const totalPerSecond = streamFlowRatePerMonth / ONE_MONTH_SECONDS; const secondsUntilRunOut = - totalPerSecond > 0 ? amount / totalPerSecond : 0n; + totalPerSecond > 0 ? superTokenBalance / totalPerSecond : 0n; const date = new Date(); date.setSeconds(date.getSeconds() + Number(secondsUntilRunOut.toString())); + return (

diff --git a/src/components/views/userProfile/donationsTab/ProfileDonationsTab.tsx b/src/components/views/userProfile/donationsTab/ProfileDonationsTab.tsx index 278e1c4fa1..29a7dc3c25 100644 --- a/src/components/views/userProfile/donationsTab/ProfileDonationsTab.tsx +++ b/src/components/views/userProfile/donationsTab/ProfileDonationsTab.tsx @@ -64,7 +64,7 @@ const ProfileDonationsTab: FC = () => { key={id} onClick={() => setTab(id)} className={`tab ${tab === id ? 'active' : ''}`} - isActive={tab === id} + $isActive={tab === id} > {formatMessage({ id: label })} @@ -86,7 +86,7 @@ const Tabs = styled(Flex)` `; interface ITab { - isActive: boolean; + $isActive: boolean; } const Tab = styled(P)` @@ -94,8 +94,8 @@ const Tab = styled(P)` border-radius: 48px; cursor: pointer; transition: background-color 0.2s ease-in-out; - ${({ isActive }) => - isActive && + ${({ $isActive }) => + $isActive && css` background-color: ${neutralColors.gray[100]}; box-shadow: 0px 3px 20px 0px rgba(212, 218, 238, 0.4); diff --git a/src/components/views/userProfile/donationsTab/recurringTab/ActiveProjectsSection.tsx b/src/components/views/userProfile/donationsTab/recurringTab/ActiveProjectsSection.tsx index d352a589fb..0ddab5d4e2 100644 --- a/src/components/views/userProfile/donationsTab/recurringTab/ActiveProjectsSection.tsx +++ b/src/components/views/userProfile/donationsTab/recurringTab/ActiveProjectsSection.tsx @@ -28,6 +28,7 @@ export interface IOrder { } export const ActiveProjectsSection = () => { + const [, setTrigger] = useState(false); const [showArchive, setShowArchive] = useState(false); const [loading, setLoading] = useState(false); const [donations, setDonations] = useState([]); @@ -133,6 +134,9 @@ export const ActiveProjectsSection = () => { order={order} changeOrder={changeOrder} myAccount={myAccount} + refetch={() => { + setTrigger(prev => !prev); + }} /> )} {loading && ( diff --git a/src/components/views/userProfile/donationsTab/recurringTab/EndStreamModal.tsx b/src/components/views/userProfile/donationsTab/recurringTab/EndStreamModal.tsx new file mode 100644 index 0000000000..6d96e46f22 --- /dev/null +++ b/src/components/views/userProfile/donationsTab/recurringTab/EndStreamModal.tsx @@ -0,0 +1,186 @@ +import { + Flex, + IconAlertTriangleOutline32, + mediaQueries, +} from '@giveth/ui-design-system'; +import { useIntl } from 'react-intl'; +import { useState, type FC } from 'react'; +import styled from 'styled-components'; +import { Framework } from '@superfluid-finance/sdk-core'; +import { useAccount } from 'wagmi'; +import { Modal } from '@/components/modals/Modal'; +import { useModalAnimation } from '@/hooks/useModalAnimation'; +import { IModal } from '@/types/common'; +import InlineToast, { EToastType } from '@/components/toasts/InlineToast'; +import { ActionButton } from './ModifyStreamModal/ModifyStreamInnerModal'; +import config, { isProduction } from '@/configuration'; +import { IWalletRecurringDonation } from '@/apollo/types/types'; +import { getEthersProvider, getEthersSigner } from '@/helpers/ethers'; +import { wagmiConfig } from '@/wagmiConfigs'; +import { endRecurringDonation } from '@/services/donation'; +import { showToastError } from '@/lib/helpers'; + +enum EEndStreamSteps { + CONFIRM, + ENDING, + SUCCESS, +} + +export interface IEndStreamModalProps extends IModal { + donation: IWalletRecurringDonation; + refetch: () => void; +} + +export const EndStreamModal: FC = ({ ...props }) => { + const { isAnimating, closeModal } = useModalAnimation(props.setShowModal); + const { formatMessage } = useIntl(); + + return ( + } + > + + + ); +}; + +interface IEndStreamInnerModalProps extends IEndStreamModalProps {} + +const EndStreamInnerModal: FC = ({ + setShowModal, + donation, + refetch, +}) => { + const [step, setStep] = useState(EEndStreamSteps.CONFIRM); + const { formatMessage } = useIntl(); + const { address } = useAccount(); + + const onDeleteStream = async () => { + setStep(EEndStreamSteps.ENDING); + try { + if (!donation.project.anchorContracts.length) { + throw new Error('Project Anchor contract address not found'); + } + + if (!address) { + throw new Error('Please connect your wallet first'); + } + + const _superToken = config.OPTIMISM_CONFIG.SUPER_FLUID_TOKENS.find( + s => s.underlyingToken.symbol === donation.currency, + ); + if (!_superToken) { + throw new Error('SuperToken not found'); + } + const provider = await getEthersProvider(wagmiConfig); + const signer = await getEthersSigner(wagmiConfig); + if (!provider || !signer) + throw new Error('Provider or signer not found'); + + const _options = { + chainId: config.OPTIMISM_CONFIG.id, + provider: provider, + resolverAddress: isProduction + ? undefined + : '0x554c06487bEc8c890A0345eb05a5292C1b1017Bd', + }; + const sf = await Framework.create(_options); + + let superToken; + if (_superToken.symbol === 'ETHx') { + superToken = await sf.loadNativeAssetSuperToken(_superToken.id); + } else { + superToken = await sf.loadWrapperSuperToken(_superToken.id); + } + + const deleteOp = superToken.deleteFlow({ + sender: address, + receiver: donation.project.anchorContracts[0].address, + }); + + const tx = await deleteOp.exec(signer); + + try { + const info = { + projectId: +donation.project.id, + chainId: config.OPTIMISM_NETWORK_NUMBER, + txHash: tx.hash, + superToken: _superToken, + }; + const projectBackendRes = await endRecurringDonation(info); + console.log('Project Donation End Info', projectBackendRes); + refetch(); + } catch (error) { + showToastError(error); + } + + const res = await tx.wait(); + if (!res.status) { + throw new Error('Transaction failed'); + } + setStep(EEndStreamSteps.SUCCESS); + } catch (error: any) { + setStep(EEndStreamSteps.CONFIRM); + if (error?.code !== 'ACTION_REJECTED') { + showToastError(error); + } + console.log('Error on recurring donation', { error }); + } + }; + + return step === EEndStreamSteps.CONFIRM || + step === EEndStreamSteps.ENDING ? ( + + + + setShowModal(false)} + buttonType='texty-gray' + disabled={step === EEndStreamSteps.ENDING} + /> + onDeleteStream()} + disabled={step === EEndStreamSteps.ENDING} + loading={step === EEndStreamSteps.ENDING} + /> + + + ) : ( + + + { + setShowModal(false); + }} + /> + + ); +}; + +const Wrapper = styled(Flex)` + text-align: left; + flex-direction: column; + align-items: stretch; + justify-content: stretch; + gap: 16px; + width: 100%; + padding: 16px 24px 24px 24px; + ${mediaQueries.tablet} { + width: 530px; + } +`; diff --git a/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/ModifyStreamInnerModal.tsx b/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/ModifyStreamInnerModal.tsx new file mode 100644 index 0000000000..0e83a86301 --- /dev/null +++ b/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/ModifyStreamInnerModal.tsx @@ -0,0 +1,359 @@ +import { + B, + Button, + Caption, + Flex, + IconHelpFilled16, + P, + brandColors, + mediaQueries, + neutralColors, + semanticColors, +} from '@giveth/ui-design-system'; +import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'; +import { useIntl } from 'react-intl'; +import styled from 'styled-components'; +import { useAccount, useBalance } from 'wagmi'; +import { formatUnits } from 'viem'; +import Slider from 'rc-slider'; +import BigNumber from 'bignumber.js'; +import Image from 'next/image'; +import { FlowRateTooltip } from '@/components/GIVeconomyPages/GIVstream.sc'; +import { IconWithTooltip } from '@/components/IconWithToolTip'; +import { TokenIcon } from '@/components/views/donate/TokenIcon/TokenIcon'; +import { limitFraction } from '@/helpers/number'; +import { ONE_MONTH_SECONDS } from '@/lib/constants/constants'; +import { + mapValue, + mapValueInverse, +} from '@/components/views/donate/RecurringDonationCard'; +import InlineToast, { EToastType } from '@/components/toasts/InlineToast'; +import { ISuperfluidStream, IToken } from '@/types/superFluid'; +import { ITokenStreams } from '@/context/donate.context'; +import { + EDonationSteps, + IModifyDonationInfo, + IModifyStreamModalProps, +} from './ModifyStreamModal'; + +interface IModifyStreamInnerModalProps extends IModifyStreamModalProps { + setStep: (step: EDonationSteps) => void; + superToken: IToken; + tokenStreams: ITokenStreams; + setModifyInfo: Dispatch>; +} + +interface IGeneralInfo { + projectStream?: ISuperfluidStream; + otherStreamsTotalFlowRate: bigint; +} + +export const ModifyStreamInnerModal: FC = ({ + donation, + superToken, + setStep, + tokenStreams, + setModifyInfo, +}) => { + const [percentage, setPercentage] = useState(0); + const [info, setInfo] = useState({ + otherStreamsTotalFlowRate: 0n, + }); + const { formatMessage } = useIntl(); + const { address } = useAccount(); + + // Get the balance of the super token + const { data: balance } = useBalance({ + token: superToken.id, + address, + }); + + // Calculate the total amount to donate per month + const flowRatePerMonth = + BigInt( + new BigNumber((balance?.value || 0n).toString()) + .multipliedBy(percentage) + .toFixed(0), + ) / 100n; + + // Calculate the flow rate per second + const flowRatePerSec = flowRatePerMonth / ONE_MONTH_SECONDS; + + // Calculate the total stream per second + const totalStreamPerSec = flowRatePerSec + info.otherStreamsTotalFlowRate; + + // Calculate the total stream per month + const totalStreamPerMonth = totalStreamPerSec * ONE_MONTH_SECONDS; + + // Calculate the stream run out in month + const streamRunOutInMonth = + totalStreamPerSec > 0 + ? (balance?.value || 0n) / totalStreamPerMonth + : 0n; + + // Check if the total stream exceed + const isTotalStreamExceed = + streamRunOutInMonth < 1n && totalStreamPerSec > 0; + + // Calculate the color of the slider + const sliderColor = isTotalStreamExceed + ? semanticColors.punch + : brandColors.giv; + + const tokenStream = tokenStreams[superToken.id || '']; + + useEffect(() => { + if ( + !tokenStream || + tokenStream.length === 0 || + !balance?.value || + percentage > 0 // don't manipulate percentage if it's already set + ) + return; + const _streamInfo: IGeneralInfo = { + otherStreamsTotalFlowRate: 0n, + }; + for (let i = 0; i < tokenStream.length; i++) { + const ts = tokenStream[i]; + if ( + ts.receiver.id === donation.project.anchorContracts[0]?.address + ) { + _streamInfo.projectStream = ts; + const _percentage = BigNumber( + ( + BigInt(ts.currentFlowRate) * + ONE_MONTH_SECONDS * + 100n + ).toString(), + ).dividedBy(balance?.value.toString()); + setPercentage(parseFloat(_percentage.toString())); + } else { + _streamInfo.otherStreamsTotalFlowRate += BigInt( + ts.currentFlowRate, + ); + } + } + + setInfo(_streamInfo); + }, [balance?.value, donation.project.anchorContracts, tokenStream]); + + return ( + + + + {formatMessage({ id: 'label.stream_balance' })} + + } + direction='right' + align='bottom' + > + + {formatMessage({ + id: 'tooltip.flowrate', + })} + + + + + + + {superToken.underlyingToken?.symbol} + + + {limitFraction( + formatUnits( + balance?.value || 0n, + balance?.decimals || 18, + ), + )} +   + {superToken?.symbol} + + + + + {formatMessage({ + id: 'label.amount_to_donate_monthly', + })} + + + { + const _value = Array.isArray(value) + ? value[0] + : value; + setPercentage(mapValue(_value)); + }} + value={mapValueInverse(percentage)} + /> + + + + + {formatMessage({ + id: 'label.donating_to', + })} + + {donation.project.title} + + + + {balance?.value !== 0n && percentage !== 0 + ? limitFraction( + formatUnits( + flowRatePerMonth, + superToken?.decimals || 18, + ), + ) + : 0} + + {superToken?.symbol} + + {formatMessage({ id: 'label.per_month' })} + + + + + + + {formatMessage({ + id: 'label.stream_balance_runs_out_in', + })} + + + + {streamRunOutInMonth.toString()} + + + {formatMessage( + { id: 'label.months' }, + { + count: streamRunOutInMonth.toString(), + }, + )} + + + + {tokenStream?.length > 0 && ( + + {formatMessage( + { + id: 'label.you_are_supporting_other_projects_with_this_stream', + }, + { + count: tokenStream.length - 1, + }, + )} + + )} + + + + { + setModifyInfo({ + flowRatePerMonth: flowRatePerMonth, + streamFlowRatePerMonth: totalStreamPerMonth, + superTokenBalance: balance?.value!, + token: superToken, + }); + setStep(EDonationSteps.CONFIRM); + }} + disabled={ + balance?.value === undefined || + balance?.value === 0n || + isTotalStreamExceed || + percentage === 0 + } + /> + +

{formatMessage({ id: 'label.streams_powered_by' })}

+ Superfluid logo + + + ); +}; + +export const Wrapper = styled(Flex)` + text-align: left; + flex-direction: column; + align-items: stretch; + justify-content: stretch; + gap: 16px; + width: 100%; + padding: 16px 24px 24px 24px; + ${mediaQueries.tablet} { + width: 430px; + } +`; + +const TokenInfoWrapper = styled(Flex)` + border: 2px solid ${neutralColors.gray[300]}; + border-radius: 8px; + overflow: hidden; + & > * { + padding: 13px 16px; + } + align-items: center; +`; + +const TokenSymbol = styled(Flex)` + min-width: 140px; +`; + +const TokenBalance = styled(B)` + border-left: 2px solid ${neutralColors.gray[300]}; +`; + +const SliderWrapper = styled.div` + width: 100%; + position: relative; +`; + +const StyledSlider = styled(Slider)``; + +const OtherStreamsInfo = styled(Caption)` + padding: 8px; + border-radius: 8px; + background: var(--Neutral-Gray-200, #f7f7f9); +`; + +export const ActionButton = styled(Button)` + width: 100%; +`; + +const StreamInfo = styled(Flex)` + flex-direction: column; + margin-top: 16px; +`; + +const SuperfluidLogoContainer = styled(Flex)` + margin-top: 32px; +`; diff --git a/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/ModifyStreamModal.tsx b/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/ModifyStreamModal.tsx new file mode 100644 index 0000000000..5ea25a14e0 --- /dev/null +++ b/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/ModifyStreamModal.tsx @@ -0,0 +1,81 @@ +import { IconDonation32 } from '@giveth/ui-design-system'; +import { FC, useMemo, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { Modal } from '@/components/modals/Modal'; +import { useModalAnimation } from '@/hooks/useModalAnimation'; +import { IModal } from '@/types/common'; +import { IWalletRecurringDonation } from '@/apollo/types/types'; +import config from '@/configuration'; +import { useUserStreams } from '@/hooks/useUserStreams'; +import { ModifyStreamInnerModal } from './ModifyStreamInnerModal'; +import { UpdateStreamInnerModal } from './UpdateStreamInnerModal'; +import { IToken } from '@/types/superFluid'; + +export enum EDonationSteps { + MODIFY, + CONFIRM, + DONATING, + SUCCESS, +} + +export interface IModifyDonationInfo { + superTokenBalance: bigint; + flowRatePerMonth: bigint; + streamFlowRatePerMonth: bigint; + token: IToken; +} + +export interface IModifyStreamModalProps extends IModal { + donation: IWalletRecurringDonation; + refetch: () => void; +} + +export const ModifyStreamModal: FC = ({ + ...props +}) => { + const [step, setStep] = useState(EDonationSteps.MODIFY); + const [modifyInfo, setModifyInfo] = useState(); + const { isAnimating, closeModal } = useModalAnimation(props.setShowModal); + const { formatMessage } = useIntl(); + const tokenStreams = useUserStreams(); + + const superToken = useMemo( + () => + config.OPTIMISM_CONFIG.SUPER_FLUID_TOKENS.find( + s => s.underlyingToken.symbol === props.donation.currency, + ), + [props.donation.currency], + ); + + return ( + } + > + {step === EDonationSteps.MODIFY ? ( + + ) : ( + + )} + + ); +}; diff --git a/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/TXLink.tsx b/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/TXLink.tsx new file mode 100644 index 0000000000..53d7fa392a --- /dev/null +++ b/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/TXLink.tsx @@ -0,0 +1,44 @@ +import { + Flex, + GLink, + IconExternalLink16, + brandColors, +} from '@giveth/ui-design-system'; +import { FC } from 'react'; +import { useIntl } from 'react-intl'; +import styled from 'styled-components'; +import { ChainType } from '@/types/config'; +import { formatTxLink } from '@/lib/helpers'; +import config from '@/configuration'; + +interface ITXLinkProps { + tx: string; +} + +export const TXLink: FC = ({ tx }) => { + const { formatMessage } = useIntl(); + return ( + + + {formatMessage({ id: 'label.view_on_block_explorer' })} + + + + ); +}; + +const Wrapper = styled(Flex)` + color: ${brandColors.pinky[500]}; + :hover { + color: ${brandColors.pinky[700]}; + } +`; diff --git a/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/UpdateStreamInnerModal.tsx b/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/UpdateStreamInnerModal.tsx new file mode 100644 index 0000000000..f4974c08c4 --- /dev/null +++ b/src/components/views/userProfile/donationsTab/recurringTab/ModifyStreamModal/UpdateStreamInnerModal.tsx @@ -0,0 +1,190 @@ +import React, { FC, useState } from 'react'; +import { useIntl } from 'react-intl'; +import { useAccount } from 'wagmi'; +import { Framework } from '@superfluid-finance/sdk-core'; + +import styled from 'styled-components'; +import { EDonationSteps, IModifyStreamModalProps } from './ModifyStreamModal'; +import { ActionButton, Wrapper } from './ModifyStreamInnerModal'; +import { Item } from '@/components/views/donate/RecurringDonationModal/Item'; +import { IToken } from '@/types/superFluid'; +import { RunOutInfo } from '@/components/views/donate/RunOutInfo'; +import { useTokenPrice } from '@/hooks/useTokenPrice'; +import config, { isProduction } from '@/configuration'; +import { getEthersProvider, getEthersSigner } from '@/helpers/ethers'; +import { ONE_MONTH_SECONDS } from '@/lib/constants/constants'; +import { showToastError } from '@/lib/helpers'; +import { updateRecurringDonation } from '@/services/donation'; +import { wagmiConfig } from '@/wagmiConfigs'; +import InlineToast, { EToastType } from '@/components/toasts/InlineToast'; +import { TXLink } from './TXLink'; + +interface IModifyStreamInnerModalProps extends IModifyStreamModalProps { + step: EDonationSteps; + setStep: (step: EDonationSteps) => void; + token: IToken; + superTokenBalance: bigint; + flowRatePerMonth: bigint; + streamFlowRatePerMonth: bigint; +} + +export const UpdateStreamInnerModal: FC = ({ + donation, + step, + setStep, + token, + superTokenBalance, + flowRatePerMonth, + streamFlowRatePerMonth, + setShowModal, + refetch, +}) => { + const [tx, setTx] = useState(''); + const { formatMessage } = useIntl(); + const tokenPrice = useTokenPrice(token); + const { address } = useAccount(); + + const onDonate = async () => { + setStep(EDonationSteps.DONATING); + try { + const projectAnchorContract = + donation.project.anchorContracts[0]?.address; + if (!projectAnchorContract) { + throw new Error('Project anchor address not found'); + } + if (!address || !token) { + throw new Error('address not found'); + } + const provider = await getEthersProvider(wagmiConfig); + const signer = await getEthersSigner(wagmiConfig); + + if (!provider || !signer) + throw new Error('Provider or signer not found'); + + const _options = { + chainId: config.OPTIMISM_CONFIG.id, + provider: provider, + resolverAddress: isProduction + ? undefined + : '0x554c06487bEc8c890A0345eb05a5292C1b1017Bd', + }; + const sf = await Framework.create(_options); + + // ETHx is not a Wrapper Super Token and should load separately + let superToken; + if (token.symbol === 'ETHx') { + superToken = await sf.loadNativeAssetSuperToken(token.id); + } else { + superToken = await sf.loadWrapperSuperToken(token.id); + } + + const _flowRatePerSec = flowRatePerMonth / ONE_MONTH_SECONDS; + + const options = { + sender: address, + receiver: projectAnchorContract, + flowRate: _flowRatePerSec.toString(), + }; + + let projectFlowOp = superToken.updateFlow(options); + + const tx = await projectFlowOp.exec(signer); + setTx(tx.hash); + + let donationId = 0; + // saving project donation to backend + try { + const projectDonationInfo = { + projectId: +donation.project.id, + anonymous: donation.anonymous, + chainId: config.OPTIMISM_NETWORK_NUMBER, + txHash: tx.hash, + flowRate: _flowRatePerSec, + superToken: token, + }; + console.log('Start Update Project Donation Info'); + const projectBackendRes = + await updateRecurringDonation(projectDonationInfo); + console.log('Project Donation Update Info', projectBackendRes); + refetch(); + } catch (error) { + console.log('error', error); + } + + const res = await tx.wait(); + if (!res.status) { + throw new Error('Transaction failed'); + } + setStep(EDonationSteps.SUCCESS); + if (tx.hash) { + } + } catch (error: any) { + setStep(EDonationSteps.CONFIRM); + if (error?.code !== 'ACTION_REJECTED') { + showToastError(error); + } + console.log('Error on recurring donation', { error }); + } + }; + + return ( + + + + {step === EDonationSteps.CONFIRM ? ( + <> + onDonate()} + /> + setStep(EDonationSteps.MODIFY)} + buttonType='texty-gray' + /> + + ) : step === EDonationSteps.DONATING ? ( + <> + + {tx && } + + + ) : step === EDonationSteps.SUCCESS ? ( + <> + + {tx && } + { + setShowModal(false); + }} + /> + + ) : null} + + ); +}; + +const StyledToast = styled(InlineToast)` + margin: 0; +`; diff --git a/src/components/views/userProfile/donationsTab/recurringTab/RecurringDonationsTable.tsx b/src/components/views/userProfile/donationsTab/recurringTab/RecurringDonationsTable.tsx index adaec3046a..a14c068a67 100644 --- a/src/components/views/userProfile/donationsTab/recurringTab/RecurringDonationsTable.tsx +++ b/src/components/views/userProfile/donationsTab/recurringTab/RecurringDonationsTable.tsx @@ -31,6 +31,7 @@ interface RecurringDonationTable { order: IOrder; changeOrder: (orderBy: ERecurringDonationSortField) => void; myAccount?: boolean; + refetch: () => void; } const RecurringDonationTable: FC = ({ @@ -38,6 +39,7 @@ const RecurringDonationTable: FC = ({ order, changeOrder, myAccount, + refetch, }) => { const { formatMessage, locale } = useIntl(); @@ -72,11 +74,16 @@ const RecurringDonationTable: FC = ({ {myAccount && ( - - {formatMessage({ id: 'label.status' })} - + <> + + {formatMessage({ id: 'label.status' })} + + + {formatMessage({ id: 'label.actions' })} + + )} - {formatMessage({ id: 'label.actions' })} + {donations.map(donation => ( @@ -111,13 +118,18 @@ const RecurringDonationTable: FC = ({ {donation.currency} {myAccount && ( - - - + <> + + + + + + + )} - - - ))} diff --git a/src/components/views/userProfile/donationsTab/recurringTab/StreamActionButton.tsx b/src/components/views/userProfile/donationsTab/recurringTab/StreamActionButton.tsx index 1f425063da..f0a32054d2 100644 --- a/src/components/views/userProfile/donationsTab/recurringTab/StreamActionButton.tsx +++ b/src/components/views/userProfile/donationsTab/recurringTab/StreamActionButton.tsx @@ -1,46 +1,34 @@ import { - GLink, IconEdit16, IconEye16, IconUpdate16, IconWalletOutline16, - neutralColors, } from '@giveth/ui-design-system'; import styled from 'styled-components'; import { type FC, useState } from 'react'; import { useIntl } from 'react-intl'; -import { EProjectStatus } from '@/apollo/types/gqlEnums'; import { Dropdown, IOption } from '@/components/Dropdown'; import { capitalizeAllWords } from '@/lib/helpers'; -import { isRecurringActive } from '@/configuration'; +import { ModifyStreamModal } from './ModifyStreamModal/ModifyStreamModal'; +import { IWalletRecurringDonation } from '@/apollo/types/types'; +import { EndStreamModal } from './EndStreamModal'; interface IStreamActionButtonProps { - finished: boolean; + donation: IWalletRecurringDonation; + refetch: () => void; } export const StreamActionButton: FC = ({ - finished, + donation, + refetch, }) => { - const isCancelled = status === EProjectStatus.CANCEL; + const [showModify, setShowModify] = useState(false); + const [showEnd, setShowEnd] = useState(false); const { formatMessage } = useIntl(); - const [isHover, setIsHover] = useState(false); - - const options: IOption[] = finished + const options: IOption[] = donation.finished ? [ - { - label: formatMessage({ id: 'label.modify_flow_rate' }), - icon: , - }, - { - label: formatMessage({ - id: 'label.end_recurring_donation', - }), - icon: , - }, - ] - : [ { label: formatMessage({ id: 'label.start_new_donation' }), icon: , @@ -51,66 +39,58 @@ export const StreamActionButton: FC = ({ ), icon: , }, + ] + : [ + { + label: formatMessage({ id: 'label.modify_flow_rate' }), + icon: , + cb: () => setShowModify(true), + }, + { + label: formatMessage({ + id: 'label.end_recurring_donation', + }), + icon: , + cb: () => setShowEnd(true), + }, ]; const dropdownStyle = { padding: '4px 16px', borderRadius: '8px', - background: isHover ? 'white' : '', }; - return isRecurringActive ? ( - setIsHover(true)} - onMouseLeave={() => setIsHover(false)} - $isOpen={isHover} - $isCancelled={isCancelled} - > - {isCancelled ? ( - CANCELLED - ) : ( - + + {showModify && ( + )} - - ) : ( - setIsHover(true)} - onMouseLeave={() => setIsHover(false)} - $isOpen={isHover} - $isCancelled={isCancelled} - size='Big' - > - {isCancelled ? ( - CANCELLED - ) : ( - )} - + ); }; -const CancelledWrapper = styled.div` - padding: 4px 16px; -`; - -const Actions = styled.div<{ $isCancelled: boolean; $isOpen: boolean }>` - cursor: ${props => (props.$isCancelled ? 'default' : 'pointer')}; +const Actions = styled.div` + cursor: pointer; border-radius: 8px; padding: 8px 10px; -`; - -const ActionsOld = styled(GLink)<{ $isCancelled: boolean; $isOpen: boolean }>` - color: ${props => - props.$isCancelled ? neutralColors.gray[500] : neutralColors.gray[900]}; - cursor: ${props => (props.$isCancelled ? 'default' : 'pointer')}; + :hover { + background-color: white; + } `; diff --git a/src/services/donation.ts b/src/services/donation.ts index 750e0d9b70..01fab9dce6 100644 --- a/src/services/donation.ts +++ b/src/services/donation.ts @@ -17,6 +17,7 @@ import { CREATE_RECURRING_DONATION, UPDATE_RECURRING_DONATION, } from '@/apollo/gql/gqlSuperfluid'; +import { RECURRING_DONATION_STATUS } from '@/apollo/types/types'; const SAVE_DONATION_ITERATIONS = 5; @@ -206,3 +207,39 @@ export const updateRecurringDonation = async ( return donationId; }; + +export interface IEndRecurringDonation { + projectId: number; + chainId: number; + txHash: string; + superToken: IToken; +} + +export const endRecurringDonation = async (props: IEndRecurringDonation) => { + let donationId = 0; + const { chainId, txHash, projectId, superToken } = props; + try { + const { data } = await client.mutate({ + mutation: UPDATE_RECURRING_DONATION, + variables: { + projectId, + networkId: chainId, + txHash, + currency: superToken.underlyingToken?.symbol || 'ETH', + status: RECURRING_DONATION_STATUS.ENDED, + }, + }); + donationId = data.updateRecurringDonation; + return donationId; + } catch (error: any) { + captureException(error, { + tags: { + section: SENTRY_URGENT, + }, + }); + console.log('endRecurringDonation error: ', error); + throw error; + } + + return donationId; +};