Skip to content

Commit 50da0ed

Browse files
EmekaManuelEmeka ManuelL03TJ3
authored
Show network and admin fees (values) in Donation flow, collective and wallet screens #243 (#305)
* chore(): implemented fee details on the donate component UI * chore(): style property of warning box fixed * chore(): added fee section on donor collective card alongside tooltips * feat: integrate collective fees into DonorCollectiveCard component - Added useCollectiveFees hook to fetch protocol and manager fees. - Updated fee calculations and display in the DonorCollectiveCard. - Enhanced tooltips to show dynamic fee information based on fetched data. - Introduced calculateFeeAmounts and formatFlowRateToDaily utility functions for fee calculations. * fix: improve error handling and fee display in DonorCollectiveCard - Enhanced the useCollectiveFees hook to provide better error messages and handle loading states. - Updated fee calculations in DonorCollectiveCard to account for error states and loading indicators. - Added visual indicators for live fee usage and improved tooltip content for clarity. - Refactored fee fetching logic to ensure fallback values are only used when necessary. * fix: enhance error handling and fallback logic in useCollectiveFees hook - Improved error handling for fetching protocol fees from UBI and DirectPayments factories. - Added fallback values for protocol fees to ensure stability in case of RPC errors or factory failures. - Enhanced logging for better debugging and visibility of fee retrieval processes. * Revert "fix: enhance error handling and fallback logic in useCollectiveFees hook" This reverts commit 9866d72. * fix: update supportingLabel style in WalletCards component - Removed the deprecated 'font' property and retained 'fontWeight' for better consistency in styling. * fix: optimize fee calculation in formatFlowRateToDaily function - Refactored the fee calculation logic to use a parsed float value for daily amounts, improving readability and consistency in the return format. - Ensured that both G$ and USD amounts are calculated using the same parsed value for accuracy. * fix: improve error handling in fee calculation functions - Added try-catch blocks to handle invalid flowRate formats in calculateFeeAmounts and formatFlowRateToDaily functions. - Ensured that fallback values are returned in case of errors, enhancing stability and user experience. * feat: enhance DonateComponent to display dynamic fee information - Integrated useCollectiveFees hook to fetch and display protocol and manager fees dynamically. - Improved loading and error handling states for fee information, providing users with real-time updates. - Updated the UI to reflect fetched fee values or fallback defaults, ensuring clarity in donation fee structures. * refactor: remove debug logging from useCollectiveFees and GoodCollectiveSDK * feat: integrate realtime stats into DonorCollectiveCard component - Added useRealtimeStats hook to fetch live pool statistics for the donor collective. - Updated fee calculation logic to consider realtime stats, enhancing accuracy in fee display. - Improved the overall user experience by providing dynamic updates based on fetched statistics. * feat: display accumulated fees in ViewCollective component - Integrated useRealtimeStats hook to fetch and display accumulated fees for the collective. - Updated UI to include a new RowItem for displaying total fees, enhancing user visibility of financial metrics. - Improved fee calculation logic to utilize real-time statistics for accuracy in fee representation. * feat: refactor useCollectiveFees to simplify fee retrieval logic - Replaced manual fee calculation and network determination with a direct call to GoodCollectiveSDK's getCollectiveFees method. - Improved error handling for unsupported networks and fee fetching, enhancing stability and user experience. - Removed redundant code related to network name determination and pool type checks, streamlining the hook's functionality. * refactor: remove useRealtimeStats from DonorCollectiveCard component * refactor: update useCollectiveFees and useRealtimeStats to utilize provider for fee retrieval - Replaced useEthersSigner with useEthersProvider to improve provider handling. - Enhanced error handling for missing pool address and RPC provider availability. - Streamlined dependency arrays in hooks for better performance and clarity. * Apply suggestions from code review * Update packages/app/src/components/DonateComponent.tsx * Update packages/app/src/components/DonateComponent.tsx * style: adjust zIndex and overflow properties in WalletCards component * Update packages/app/src/components/DonateComponent.tsx * fix: correct import statement and add fee documentation link in defaults * style: remove zIndex from WalletCards styles and update zIndex logic for child components --------- Co-authored-by: Emeka Manuel <[email protected]> Co-authored-by: Lewis B <[email protected]>
1 parent 23b78c8 commit 50da0ed

File tree

13 files changed

+934
-76
lines changed

13 files changed

+934
-76
lines changed

packages/app/src/components/DonateComponent.tsx

Lines changed: 113 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,36 @@
1+
import { waitForTransactionReceipt } from '@wagmi/core';
2+
import Decimal from 'decimal.js';
3+
import { isEmpty } from 'lodash';
4+
import moment from 'moment';
5+
import { Box, HStack, Link, Text, useBreakpointValue, VStack } from 'native-base';
16
import { useCallback, useMemo, useState } from 'react';
27
import { Image, View } from 'react-native';
3-
import { Box, HStack, Link, Text, useBreakpointValue, VStack } from 'native-base';
4-
import { useAccount } from 'wagmi';
58
import { useParams } from 'react-router-native';
6-
import Decimal from 'decimal.js';
7-
import { waitForTransactionReceipt } from '@wagmi/core';
8-
import { config } from './../config';
99
import { TransactionReceipt } from 'viem';
10-
import { isEmpty } from 'lodash';
11-
import moment from 'moment';
10+
import { useAccount } from 'wagmi';
11+
import { config } from './../config';
1212

13-
import RoundedButton from './RoundedButton';
1413
import { useScreenSize } from '../theme/hooks';
14+
import RoundedButton from './RoundedButton';
1515

16-
import BaseModal from './modals/BaseModal';
17-
import { getDonateStyles } from '../utils';
16+
import { InfoIconOrange } from '../assets';
1817
import { useContractCalls, useGetTokenPrice } from '../hooks';
19-
import { Collective } from '../models/models';
18+
import { useApproveSwapTokenCallback } from '../hooks/useApproveSwapTokenCallback';
2019
import { useGetTokenBalance } from '../hooks/useGetTokenBalance';
21-
import { acceptablePriceImpact, Frequency, GDEnvTokens, SupportedNetwork } from '../models/constants';
22-
import { InfoIconOrange } from '../assets';
2320
import { SwapRouteState, useSwapRoute } from '../hooks/useSwapRoute';
24-
import { useApproveSwapTokenCallback } from '../hooks/useApproveSwapTokenCallback';
25-
21+
import { useCollectiveFees } from '../hooks/useCollectiveFees';
22+
import { acceptablePriceImpact, Frequency, GDEnvTokens, SupportedNetwork } from '../models/constants';
23+
import { Collective } from '../models/models';
24+
import { getDonateStyles } from '../utils';
25+
import BaseModal from './modals/BaseModal';
26+
import { ApproveTokenImg, PhoneImg, StreamWarning, ThankYouImg } from '../assets';
2627
import { useToken, useTokenList } from '../hooks/useTokenList';
2728
import { formatDecimalStringInput } from '../lib/formatDecimalStringInput';
29+
import { formatNumberWithCommas } from '../lib/formatFiatCurrency';
2830
import useCrossNavigate from '../routes/useCrossNavigate';
2931
import FrequencySelector from './DonateFrequency';
3032
import NumberInput from './NumberInput';
31-
import { ApproveTokenImg, PhoneImg, StreamWarning, ThankYouImg } from '../assets';
32-
import { formatNumberWithCommas } from '../lib/formatFiatCurrency';
33+
import env from '../lib/env';
3334
type ConfigChainId = (typeof config.chains)[number]['id'];
3435

3536
interface DonateComponentProps {
@@ -40,7 +41,7 @@ const PriceImpact = ({ priceImpact }: any) => (
4041
<Text color="goodOrange.500" display="flex" flexWrap="wrap">
4142
<Text>Due to low liquidity between your chosen currency and GoodDollar, </Text>
4243
<Text variant="bold" fontWeight="700">
43-
your donation amount will reduce by {priceImpact?.toFixed(2)}%{' '}
44+
your donation amount will reduce by {priceImpact?.toFixed(2)}%
4445
</Text>
4546
when swapped.
4647
</Text>
@@ -113,11 +114,10 @@ const shouldWarning = (
113114
};
114115

115116
const SwapValue = ({ swapValue }: { swapValue: number }) => (
116-
<Text textAlign="right" fontSize="sm " color="goodGrey.25">
117+
<Text textAlign="right" fontSize="sm " color="gray.400">
117118
=
118119
<Text variant="bold" fontWeight="700">
119-
{' '}
120-
G${' '}
120+
G$
121121
</Text>
122122
{formatNumberWithCommas(swapValue.toString(), 2)}
123123
</Text>
@@ -127,7 +127,17 @@ const WarningBox = ({ content, explanationProps = {} }: any) => {
127127
const Explanation = content.Explanation;
128128

129129
return (
130-
<HStack space={2} backgroundColor="goodOrange.200" maxWidth="343" paddingY={3} paddingX={2}>
130+
<HStack
131+
space={2}
132+
backgroundColor="goodOrange.200"
133+
maxWidth="500"
134+
borderRadius={15}
135+
display={'flex'}
136+
flexDirection="row"
137+
alignItems="center"
138+
justifyContent="center"
139+
paddingY={3}
140+
paddingX={2}>
131141
<Image source={{ uri: InfoIconOrange }} style={{ width: 16, height: 16 }} />
132142
<VStack space={4} maxWidth="100%">
133143
<VStack space={1}>
@@ -176,7 +186,10 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
176186
const [approveSwapModalVisible, setApproveSwapModalVisible] = useState(false);
177187
const [thankYouModalVisible, setThankYouModalVisible] = useState(false);
178188
const [startStreamingVisible, setStartStreamingVisible] = useState(false);
179-
const [estimatedDuration, setEstimatedDuration] = useState<{ duration: number; endDate: string }>({
189+
const [estimatedDuration, setEstimatedDuration] = useState<{
190+
duration: number;
191+
endDate: string;
192+
}>({
180193
duration: 0,
181194
endDate: '',
182195
});
@@ -206,6 +219,9 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
206219
const [currency, setCurrency] = useState<string>(gdEnvSymbol || 'G$');
207220
const { price: altTokenPrice = 0 } = useGetTokenPrice(currency);
208221

222+
// Get dynamic fee information from the collective
223+
const { fees: collectiveFees, loading: feesLoading, error: feesError } = useCollectiveFees(collective.address);
224+
209225
const decimalDonationAmount = formatDecimalStringInput(inputAmount || '0');
210226

211227
const container = useBreakpointValue({
@@ -219,7 +235,6 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
219235
paddingRight: 2,
220236
},
221237
md: {
222-
// maxWidth: 800,
223238
width: '100%',
224239
paddingLeft: 4,
225240
paddingRight: 4,
@@ -268,7 +283,6 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
268283
);
269284

270285
const approvalNotReady = handleApproveToken === undefined && currency.startsWith('G$') === false;
271-
// const approvalNotReady = false;
272286

273287
const { supportFlowWithSwap, supportFlow, supportSingleTransferAndCall, supportSingleWithSwap } = useContractCalls(
274288
collectiveId,
@@ -285,15 +299,12 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
285299
);
286300

287301
const token = useToken(currency);
288-
// const currencyDecimals = token.decimals;
289302
const donorCurrencyBalance = useGetTokenBalance(token.address, address, chain?.id, true);
290303

291304
const totalDecimalDonation = new Decimal(decimalDonationAmount * (currency.includes('G$') ? 1 : Number(duration)));
292305

293306
const swapValue = currency.includes('G$') ? 0 : (totalDecimalDonation.toNumber() * altTokenPrice) / tokenPrice;
294307

295-
// const totalDonationFormatted = totalDecimalDonation.toDecimalPlaces(currencyDecimals, Decimal.ROUND_DOWN).toString();
296-
297308
const { isNonZeroDonation, isInsufficientBalance, isInsufficientLiquidity, isUnacceptablePriceImpact } =
298309
shouldWarning(currency, donorCurrencyBalance, priceImpact, swapRouteStatus, totalDecimalDonation);
299310

@@ -425,7 +436,10 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
425436

426437
const estimatedEndDate = moment().add(estDuration, 'months').format('DD.MM.YY HH:mm');
427438

428-
setEstimatedDuration({ duration: estDuration, endDate: estimatedEndDate });
439+
setEstimatedDuration({
440+
duration: estDuration,
441+
endDate: estimatedEndDate,
442+
});
429443
},
430444
[currency, donorCurrencyBalance, frequency]
431445
);
@@ -534,7 +548,7 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
534548
<Text variant="bold" fontSize="lg">
535549
Donation Frequency
536550
</Text>
537-
<Text>How do you want to donate</Text>
551+
<Text>How do you want to donate ? </Text>
538552
</VStack>
539553
<FrequencySelector onSelect={onChangeFrequency} />
540554

@@ -551,7 +565,7 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
551565
<Box flexGrow={1} />
552566
)}
553567
</VStack>
554-
{/* Amount and token */}
568+
555569
<VStack space={2} mb={8} zIndex={1}>
556570
<VStack space={2} zIndex={1}>
557571
<Text variant="bold" fontSize="lg">
@@ -562,16 +576,17 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
562576
<NumberInput
563577
type="token"
564578
dropdownValue={currency}
565-
inputValue={inputAmount?.toString()}
579+
inputValue={inputAmount}
566580
onSelect={onChangeCurrency}
567581
onChangeAmount={onChangeAmount}
568582
options={currencyOptions}
569583
isWarning={isWarning}
570-
withDuration={frequency !== 'One-Time'}
584+
withDuration={frequency !== Frequency.OneTime}
571585
/>
572-
{frequency === 'One-Time' && !currency.startsWith('G$') && isNonZeroDonation && swapValue ? (
573-
<SwapValue {...{ swapValue }} />
574-
) : null}
586+
587+
{frequency === Frequency.OneTime && !currency.startsWith('G$') && isNonZeroDonation && swapValue > 0 && (
588+
<SwapValue swapValue={swapValue} />
589+
)}
575590
</VStack>
576591

577592
<VStack space={2} maxWidth={650}>
@@ -586,10 +601,14 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
586601
<NumberInput
587602
type="number"
588603
dropdownValue={currency}
589-
inputValue={duration?.toString() ?? '1'}
604+
inputValue={duration.toString()}
590605
onSelect={onChangeCurrency}
591606
onChangeAmount={onChangeRate}
592-
/>{' '}
607+
/>
608+
{/* Swap value display under duration input */}
609+
{!currency.startsWith('G$') && isNonZeroDonation && swapValue > 0 && (
610+
<SwapValue swapValue={swapValue} />
611+
)}
593612
</>
594613
) : null}
595614
{currency.includes('G$') &&
@@ -617,19 +636,63 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
617636
) : null}
618637
</VStack>
619638
</HStack>
620-
{frequency !== 'One-Time' && currency === 'CELO' && isNonZeroDonation && swapValue ? (
621-
<VStack space={2} alignItems="flex-start">
622-
<Text variant="bold" fontSize="lg">
623-
Total Donation Swap Amount:
639+
640+
{/* Fee Information */}
641+
<VStack space={2} backgroundColor="gray.50" p={2} borderRadius="lg">
642+
{feesLoading ? (
643+
<Text fontSize="sm" color="gray.700">
644+
Loading fee information...
645+
</Text>
646+
) : feesError ? (
647+
<Text fontSize="sm" color="gray.700">
648+
Unable to load fee information. Using default fees.
624649
</Text>
625-
<VStack space="0">
626-
<Text variant="bold" color="goodPurple.400" fontSize="2xl" textAlign="right">
627-
CELO {decimalDonationAmount}
650+
) : collectiveFees ? (
651+
<Text fontSize="sm" color="gray.700">
652+
All donations incur a {''}
653+
<Text fontWeight="bold" color="black">
654+
{(collectiveFees.protocolFeeBps / 100).toFixed(1)}%
628655
</Text>
629-
<SwapValue {...{ swapValue }} />
630-
</VStack>
631-
</VStack>
632-
) : null}
656+
protocol fee, which contributes directly to {''}
657+
<Link href={env.REACT_APP_FEE_DOCS_LINK} _text={{ color: 'blue.500', textDecoration: 'underline' }}>
658+
GoodDollar UBI
659+
</Link>
660+
.
661+
<Text>
662+
All donations incur a {''}
663+
<Text fontWeight="bold" color="black">
664+
{(collectiveFees.managerFeeBps / 100).toFixed(1)}%
665+
</Text>
666+
</Text>
667+
<Link href={env.REACT_APP_FEE_DOCS_LINK} _text={{ color: 'blue.500', textDecoration: 'underline' }}>
668+
Manager Fee
669+
</Link>
670+
.
671+
</Text>
672+
) : (
673+
<Text fontSize="sm" color="gray.700">
674+
All donations incur a {''}
675+
<Text fontWeight="bold" color="black">
676+
5.0%
677+
</Text>
678+
protocol fee, which contributes directly to {''}
679+
<Link href={env.REACT_APP_FEE_DOCS_LINK} _text={{ color: 'blue.500', textDecoration: 'underline' }}>
680+
GoodDollar UBI
681+
</Link>
682+
.
683+
<Text>
684+
All donations incur a {''}
685+
<Text fontWeight="bold" color="black">
686+
3.0%
687+
</Text>
688+
</Text>
689+
<Link href={env.REACT_APP_FEE_DOCS_LINK} _text={{ color: 'blue.500', textDecoration: 'underline' }}>
690+
Manager Fee
691+
</Link>
692+
.
693+
</Text>
694+
)}
695+
</VStack>
633696
</VStack>
634697

635698
<View style={{ gap: 16, flex: 1, zIndex: -1 }}>
@@ -670,7 +733,7 @@ const DonateComponent = ({ collective }: DonateComponentProps) => {
670733
</Text>
671734
)}
672735
<Text>
673-
Pressing Confirm will begin the donation {frequency !== Frequency.OneTime ? 'streaming ' : ''}process.
736+
Pressing "Confirm" will begin the donation {frequency !== Frequency.OneTime ? 'streaming ' : ''}process.
674737
You will need to confirm using your connected wallet. You may be asked to sign multiple transactions.
675738
</Text>
676739
</VStack>

packages/app/src/components/ViewCollective.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { defaultInfoLabel, GDToken, SUBGRAPH_POLL_INTERVAL } from '../models/con
3737
import env from '../lib/env';
3838
import { useGetTokenBalance } from '../hooks/useGetTokenBalance';
3939
import { useFlowingBalance } from '../hooks/useFlowingBalance';
40+
import { useRealtimeStats } from '../hooks/useRealtimeStats';
4041
import { GoodDollarAmount } from './GoodDollarAmount';
4142
import { styles as walletCardStyles } from '../components/WalletCards/styles';
4243
import { formatFlowRate } from '../lib/formatFlowRate';
@@ -207,13 +208,20 @@ function ViewCollective({ collective }: ViewCollectiveProps) {
207208
const maybeDonorCollective = useDonorCollectiveByAddresses(address ?? '', poolAddress, SUBGRAPH_POLL_INTERVAL);
208209

209210
const { price: tokenPrice } = useGetTokenPrice('G$');
211+
const { stats } = useRealtimeStats(poolAddress);
210212

211213
const { wei: formattedTotalRewards, usdValue: totalRewardsUsdValue } = calculateGoodDollarAmounts(
212214
totalRewards,
213215
tokenPrice,
214216
2
215217
);
216218

219+
const { wei: formattedTotalFees, usdValue: totalFeesUsdValue } = calculateGoodDollarAmounts(
220+
stats?.totalFees || '0',
221+
tokenPrice,
222+
2
223+
);
224+
217225
if (isDesktopView) {
218226
return (
219227
<View style={{ gap: 24, flex: 1 }}>
@@ -302,6 +310,13 @@ function ViewCollective({ collective }: ViewCollectiveProps) {
302310
currency="G$"
303311
balance={totalRewardsUsdValue ?? 0}
304312
/>
313+
<RowItem
314+
imageUrl={ListGreenIcon}
315+
rowInfo="Accumulated Fees"
316+
rowData={formattedTotalFees ?? '0'}
317+
currency="G$"
318+
balance={totalFeesUsdValue ?? 0}
319+
/>
305320
<FlowingDonationsRowItem
306321
imageUrl={SquaresIcon}
307322
rowInfo="Current Pool"

0 commit comments

Comments
 (0)