From c3f95479e90944529c15370a89aed3611dbf0549 Mon Sep 17 00:00:00 2001 From: LewisB Date: Wed, 30 Oct 2024 12:40:28 +0700 Subject: [PATCH] fix: update donation page and steps to new design --- .../app/src/components/DonateComponent.tsx | 909 ++++++++---------- .../app/src/components/DonateFrequency.tsx | 44 + packages/app/src/components/Dropdown.tsx | 83 +- packages/app/src/components/NumberInput.tsx | 90 ++ .../app/src/components/ViewCollective.tsx | 38 +- packages/app/src/hooks/useSwapRoute.tsx | 16 +- 6 files changed, 570 insertions(+), 610 deletions(-) create mode 100644 packages/app/src/components/DonateFrequency.tsx create mode 100644 packages/app/src/components/NumberInput.tsx diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index 6ee177fc..d04ce5ba 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -1,40 +1,121 @@ import { useCallback, useMemo, useState } from 'react'; -import { Image, StyleSheet, Text, TextInput, View } from 'react-native'; -import { Link } from 'native-base'; +import { Image, View } from 'react-native'; +import { Box, HStack, Link, Text, VStack } from 'native-base'; import { useAccount, useNetwork } from 'wagmi'; import { useParams } from 'react-router-native'; import Decimal from 'decimal.js'; import { waitForTransaction } from '@wagmi/core'; import { TransactionReceipt } from 'viem'; +import { isEmpty } from 'lodash'; -import { InterRegular, InterSemiBold, InterSmall } from '../utils/webFonts'; import RoundedButton from './RoundedButton'; -import CompleteDonationModal from './modals/CompleteDonationModal'; -import { Colors } from '../utils/colors'; import { useScreenSize } from '../theme/hooks'; -import Dropdown from './Dropdown'; -import { getDonateStyles, getFrequencyPlural } from '../utils'; -import { useContractCalls, useGetTokenPrice } from '../hooks'; +import BaseModal from './modals/BaseModal'; +import { getDonateStyles } from '../utils'; +import { useContractCalls } from '../hooks'; import { Collective } from '../models/models'; import { useGetTokenBalance } from '../hooks/useGetTokenBalance'; -import { acceptablePriceImpact, Frequency, frequencyOptions, GDEnvTokens, SupportedNetwork } from '../models/constants'; +import { acceptablePriceImpact, Frequency, GDEnvTokens, SupportedNetwork } from '../models/constants'; import { InfoIconOrange } from '../assets'; -import { formatFiatCurrency } from '../lib/formatFiatCurrency'; -import ErrorModal from './modals/ErrorModal'; import { SwapRouteState, useSwapRoute } from '../hooks/useSwapRoute'; import { useApproveSwapTokenCallback } from '../hooks/useApproveSwapTokenCallback'; -import ApproveSwapModal from './modals/ApproveSwapModal'; + import { useToken, useTokenList } from '../hooks/useTokenList'; import { formatDecimalStringInput } from '../lib/formatDecimalStringInput'; -import ThankYouModal from './modals/ThankYouModal'; import useCrossNavigate from '../routes/useCrossNavigate'; +import FrequencySelector from './DonateFrequency'; +import NumberInput from './NumberInput'; +import { ApproveTokenImg, PhoneImg, StreamWarning, ThankYouImg } from '../assets'; interface DonateComponentProps { collective: Collective; } -function DonateComponent({ collective }: DonateComponentProps) { +const PriceImpact = ({ priceImpact }: any) => ( + + Due to low liquidity between your chosen currency and GoodDollar, + + your donation amount will reduce by {priceImpact?.toFixed(2)}%{' '} + + when swapped. + +); + +const WarningExplanation = ({ type }: any) => ( + + + {type === 'liquidity' + ? 'There is not enough liquidity between your chosen currency and GoodDollar to proceed.' + : 'There is not enough balance in your wallet to proceed.'} + + +); + +const warningProps = { + priceImpact: { + title: 'Price impact warning!', + Explanation: PriceImpact, + suggestion: ['Proceed, and accept the price slip', 'Select another Donation Currency above'], + href: 'https://gooddollar.notion.site/How-do-I-buy-GoodDollars-94e821e06f924f6ea739df7db02b5a2d', + }, + liquidity: { + title: 'Insufficient liquidity!', + Explanation: WarningExplanation, + suggestion: ['Try with another currency', 'Reduce your donation amount'], + href: 'https://gooddollar.notion.site/How-do-I-buy-GoodDollars-94e821e06f924f6ea739df7db02b5a2d', + }, + balance: { + title: 'Insufficient balance!', + Explanation: WarningExplanation, + suggestion: ['Reduce your donation amount', 'Try with another currency'], + href: 'https://gooddollar.notion.site/How-do-I-buy-GoodDollars-94e821e06f924f6ea739df7db02b5a2d', + }, + noAmount: { + title: 'Enter an amount above', + }, +}; + +const WarningBox = ({ content, explanationProps = {} }: any) => { + const Explanation = content.Explanation; + + return ( + + + + + + {content.title} + + + {!isEmpty(explanationProps) ? : null} + + {content.suggestion ? ( + + + You may: + + + + {content.suggestion.map((suggestion: string, index: number) => ( + + {index + 1}. {suggestion} + + ))} + + 3. + Purchase and use GoodDollar + + + + + ) : null} + + + ); +}; + +const DonateComponent = ({ collective }: DonateComponentProps) => { const { isDesktopView } = useScreenSize(); const { id: collectiveId = '0x' } = useParams(); @@ -45,6 +126,7 @@ function DonateComponent({ collective }: DonateComponentProps) { const [errorMessage, setErrorMessage] = useState(undefined); const [approveSwapModalVisible, setApproveSwapModalVisible] = useState(false); const [thankYouModalVisible, setThankYouModalVisible] = useState(false); + const [startStreamingVisible, setStartStreamingVisible] = useState(false); const [isDonationComplete, setIsDonationComplete] = useState(false); const { navigate } = useCrossNavigate(); @@ -52,7 +134,7 @@ function DonateComponent({ collective }: DonateComponentProps) { navigate(`/profile/${address}`); } - const [frequency, setFrequency] = useState(Frequency.Monthly); + const [frequency, setFrequency] = useState(Frequency.OneTime); const [duration, setDuration] = useState(12); const [decimalDonationAmount, setDecimalDonationAmount] = useState(0); @@ -68,7 +150,7 @@ function DonateComponent({ collective }: DonateComponentProps) { const currencyOptions: { value: string; label: string }[] = useMemo(() => { let options = Object.keys(tokenList).reduce>((acc, key) => { - if (!key.startsWith('G$') || key === gdEnvSymbol) { + if (['CELO'].includes(key) || key === gdEnvSymbol) { acc.push({ value: key, label: key }); } return acc; @@ -84,6 +166,7 @@ function DonateComponent({ collective }: DonateComponentProps) { rawMinimumAmountOut, priceImpact, status: swapRouteStatus, + gasEstimate, } = useSwapRoute(currency, GDToken, decimalDonationAmount, duration); const { handleApproveToken, isRequireApprove } = useApproveSwapTokenCallback( @@ -94,6 +177,7 @@ function DonateComponent({ collective }: DonateComponentProps) { collectiveId as `0x${string}` ); const approvalNotReady = handleApproveToken === undefined && currency.startsWith('G$') === false; + // const approvalNotReady = false; const { supportFlowWithSwap, supportFlow, supportSingleTransferAndCall, supportSingleWithSwap } = useContractCalls( collectiveId, @@ -109,7 +193,44 @@ function DonateComponent({ collective }: DonateComponentProps) { swapPath ); + const [confirmNoAmount, setConfirmNoAmount] = useState(false); + + const token = useToken(currency); + // const currencyDecimals = token.decimals; + const donorCurrencyBalance = useGetTokenBalance(token.address, address, chain?.id, true); + + const totalDecimalDonation = new Decimal(duration * decimalDonationAmount); + // const totalDonationFormatted = totalDecimalDonation.toDecimalPlaces(currencyDecimals, Decimal.ROUND_DOWN).toString(); + + const isNonZeroDonation = totalDecimalDonation.gt(0); + const isInsufficientBalance = + isNonZeroDonation && (!donorCurrencyBalance || totalDecimalDonation.gt(donorCurrencyBalance)); + const isInsufficientLiquidity = + isNonZeroDonation && currency.startsWith('G$') === false && swapRouteStatus === SwapRouteState.NO_ROUTE; + const isUnacceptablePriceImpact = + isNonZeroDonation && currency.startsWith('G$') === false && priceImpact + ? priceImpact > acceptablePriceImpact + : false; + + const handleStreamingWarning = useCallback(async () => { + if (startStreamingVisible) { + setStartStreamingVisible(false); + if (currency.startsWith('G$')) { + return await supportFlow(); + } else { + return await supportFlowWithSwap(); + } + } + + setStartStreamingVisible(true); + }, [currency, startStreamingVisible, supportFlow, supportFlowWithSwap]); + const handleDonate = useCallback(async () => { + if (!isNonZeroDonation) { + setConfirmNoAmount(true); + return; + } + if (frequency === Frequency.OneTime) { if (currency.startsWith('G$')) { return await supportSingleTransferAndCall(); @@ -117,7 +238,7 @@ function DonateComponent({ collective }: DonateComponentProps) { return await supportSingleWithSwap(); } } else if (currency.startsWith('G$')) { - return await supportFlow(); + handleStreamingWarning(); } let isApproveSuccess = isRequireApprove === false; @@ -143,7 +264,8 @@ function DonateComponent({ collective }: DonateComponentProps) { } } if (isApproveSuccess) { - await supportFlowWithSwap(); + // await supportFlowWithSwap(); + handleStreamingWarning(); } }, [ chain?.id, @@ -151,33 +273,14 @@ function DonateComponent({ collective }: DonateComponentProps) { frequency, isRequireApprove, handleApproveToken, - supportFlow, - supportFlowWithSwap, + handleStreamingWarning, + // supportFlow, + // supportFlowWithSwap, supportSingleTransferAndCall, supportSingleWithSwap, + isNonZeroDonation, ]); - const token = useToken(currency); - const currencyDecimals = token.decimals; - const donorCurrencyBalance = useGetTokenBalance(token.address, address, chain?.id, true); - - const totalDecimalDonation = new Decimal(duration * decimalDonationAmount); - const totalDonationFormatted = totalDecimalDonation.toDecimalPlaces(currencyDecimals, Decimal.ROUND_DOWN).toString(); - - const isNonZeroDonation = totalDecimalDonation.gt(0); - const isInsufficientBalance = - isNonZeroDonation && (!donorCurrencyBalance || totalDecimalDonation.gt(donorCurrencyBalance)); - const isInsufficientLiquidity = - isNonZeroDonation && currency.startsWith('G$') === false && swapRouteStatus === SwapRouteState.NO_ROUTE; - const isUnacceptablePriceImpact = - isNonZeroDonation && currency.startsWith('G$') === false && priceImpact - ? priceImpact > acceptablePriceImpact - : false; - - const { price } = useGetTokenPrice(currency); - const donationAmountUsdValue = price ? formatFiatCurrency(decimalDonationAmount * price) : undefined; - const totalDonationUsdValue = price ? formatFiatCurrency(totalDecimalDonation.mul(price).toNumber()) : undefined; - const donateStyles = useMemo(() => { return getDonateStyles({ noAddress: !address, @@ -202,520 +305,260 @@ function DonateComponent({ collective }: DonateComponentProps) { const { buttonCopy, buttonBgColor, buttonTextColor } = donateStyles; const onChangeCurrency = (value: string) => setCurrency(value); - const onChangeAmount = (value: string) => setDecimalDonationAmount(formatDecimalStringInput(value)); + + const onChangeAmount = (v: string) => { + if (![''].includes(v)) setConfirmNoAmount(false); + setDecimalDonationAmount(formatDecimalStringInput(v)); + }; + const onChangeFrequency = useCallback((value: string) => { if (value === Frequency.OneTime) { setDuration(1); } setFrequency(value as Frequency); }, []); + const onChangeDuration = (value: string) => setDuration(Number(value)); const onCloseErrorModal = () => setErrorMessage(undefined); + const onCloseThankYouModal = () => { + setThankYouModalVisible(false); + navigate(`/profile/${address}`); + }; - return ( - - - Donate - - Support {collective.ipfs.name}{' '} - {isDesktopView && ( - <> -
- - )} - by donating any amount you want either one time or on a recurring monthly basis. -
-
- - - {!isDesktopView && ( - <> - - Donation Currency: - You can donate using any cryptocurrency. - - - - - - - {currency} - - - - {donationAmountUsdValue} USD - - - - - )} - - {isDesktopView && ( - - - - Donation Currency: - You can donate using any cryptocurrency. - - - - - - - {currency} - - - - {donationAmountUsdValue} USD - - - - + const isWarning = isInsufficientBalance || isInsufficientLiquidity || isUnacceptablePriceImpact || confirmNoAmount; - - - Donation Frequency - - How often do you want to donate this {!isDesktopView &&
} amount? -
-
- -
- - {frequency !== 'One-Time' && ( - - For How Long: - - - - {getFrequencyPlural(frequency as Frequency)} - - - )} - -
- )} - - - <> - {!isDesktopView && ( - <> - - {frequency !== 'One-Time' && ( - - For How Long: - - + return ( + + {/* todo: find simpler solution to render different modals */} + + + + + setStartStreamingVisible(false)} + title="STREAMS CONTINUE UNLESS YOU STOP THEM!" + paragraphs={[ + 'The stream will end if your GoodDollar wallet balance is depleted.', + 'You may cancel the stream at any time by visiting the GoodCollective page or Superfluid dApp.', + ]} + onConfirm={handleStreamingWarning} + confirmButtonText="OK, Continue" + image={StreamWarning} + /> - {getFrequencyPlural(frequency)} - - - )} - - )} - - - {isConnected && ( - - - - Review Your Donation - - Your donation will be made in GoodDollars, the currency in use by this GoodCollective.{'\n'} - If recurrent, your donation will be streamed using Superfluid.{'\n'} - Pressing “Confirm” will trigger your donation.{'\n'} - - + + {/* Modals */} + + + Donate + + + Support {collective.ipfs.name} + by donating any amount you want either one time or on a recurring monthly basis. + + + + + {/* Donation frequency */} + + + + Donation Frequency + + How do you want to donate + + + + {frequency === Frequency.Monthly ? ( + + Your donation will be streamed using Superfluid. + + How does Superfluid work? - - - Donation Amount: - - - {currency} {decimalDonationAmount} - - {donationAmountUsdValue} USD - - - - {frequency !== 'One-Time' && ( - - Donation Duration: - - - {duration} {getFrequencyPlural(frequency)} - - - + ) : ( + )} - {frequency !== Frequency.OneTime && ( - - Total Amount: - - - {currency} {totalDonationFormatted} + + {/* Amount and token */} + + + + How much? + + You can donate using any token on Celo. + + + + + + {frequency !== 'One-Time' && currency === 'CELO' ? ( + <> + + + At what streaming rate? - {totalDonationUsdValue} USD - - - )} - - - - {isInsufficientLiquidity && ( - - - - - Insufficient liquidity! - - There is not enough liquidity between your chosen currency and GoodDollar to proceed. - - - - You may: - - 1. Try with another currency {'\n'} - 2. Reduce your donation amount {'\n'} - - 3. Purchase and use GoodDollar {'\n'} - - - - - - )} - - {isInsufficientBalance && ( - - - - - Insufficient balance! - There is not enough balance in your wallet to proceed. - - - You may: - - 1. Reduce your donation amount {'\n'} - 2. Try with another currency {'\n'} - - 3. Purchase and use GoodDollar {'\n'} - + {/* tod: Fix this box */} + + + {' '} + + ) : null} + {frequency !== 'One-Time' && isNonZeroDonation ? ( + + + + Estimated duration: + + + 5 months + + + + + Estimated End Date: + + + 15.03.25 08:34 + + + {gasEstimate ? ( + + + One-time estimated gas fee: - - - - )} - {isUnacceptablePriceImpact && ( - - - - - Price impace warning! - - Due to low liquidity between your chosen currency and GoodDollar, - - {' '} - your donation amount will reduce by {priceImpact?.toFixed(2)}%{' '} - - when swapped. - - - - You may: - - 1. Proceed and accept the price slip {'\n'} - 2. Select another Donation Currency above {'\n'} - - 3. Purchase and use GoodDollar {'\n'} - + + CELO {gasEstimate} - - - - )} - - - Pressing “Confirm” will begin the donation streaming process. You will need to confirm using your - connected wallet. You may be asked to sign multiple transactions. + + ) : null} + + ) : null} + + + {frequency !== 'One-Time' && currency === 'CELO' && isNonZeroDonation ? ( + + + Total Donation Swap Amount: - + + + CELO {decimalDonationAmount} + + + = + + {' '} + G${' '} + + 111.000 + + + + ) : null} + + + {isConnected ? ( + + {isWarning + ? Object.keys(warningProps).map((key) => { + const whichWarning = + key === 'priceImpact' + ? isUnacceptablePriceImpact + : key === 'balance' + ? isInsufficientBalance + : key === 'noAmount' + ? !isNonZeroDonation && confirmNoAmount + : isInsufficientLiquidity; + return whichWarning ? ( + + ) : null; + }) + : null} + {isNonZeroDonation ? ( + + You are about to begin a donation stream + + Pressing “Confirm” will begin the donation streaming process. You will need to confirm using your + connected wallet. You may be asked to sign multiple transactions. + + + ) : null} + + - )} - - - - - - - - + ) : null} + + ); -} - -const styles = StyleSheet.create({ - body: { - gap: 24, - paddingBottom: 32, - paddingTop: 32, - paddingHorizontal: 16, - backgroundColor: Colors.white, - }, - bodyDesktop: { - borderRadius: 30, - marginTop: 12, - }, - title: { - lineHeight: 25, - fontSize: 20, - textAlign: 'left', - ...InterSemiBold, - marginBottom: 16, - }, - description: { - color: Colors.gray[200], - fontSize: 16, - lineHeight: 24, - textAlign: 'left', - ...InterSmall, - }, - divider: { - width: '100%', - height: 1, - backgroundColor: Colors.gray[600], - }, - form: { - alignItems: 'center', - flexGrow: 1, - justifyContent: 'center', - }, - upperForm: { - flexDirection: 'row', - flex: 1, - alignItems: 'center', - justifyContent: 'center', - width: '50%', - paddingLeft: '25%', - }, - actionContent: { - gap: 8, - }, - actionBox: { - gap: 16, - flex: 1, - zIndex: -1, - }, - row: { - flexDirection: 'row', - gap: 8, - }, - desktopActionBox: { - flex: 1, - flexDirection: 'column', - justifyContent: 'space-between', - }, - headerLabel: { - fontSize: 18, - lineHeight: 27, - color: Colors.gray[100], - ...InterSmall, - }, - subHeading: { - fontSize: 20, - lineHeight: 27, - ...InterSemiBold, - color: Colors.gray[100], - paddingLeft: 10, - width: 159, - }, - lowerText: { - fontSize: 12, - lineHeight: 18, - textAlign: 'center', - color: Colors.gray[900], - - ...InterRegular, - }, - frequencyDetails: { - height: 32, - gap: 8, - backgroundColor: Colors.purple[100], - justifyContent: 'center', - alignItems: 'center', - borderColor: Colors.gray[600], - borderBottomWidth: 1, - flex: 1, - flexDirection: 'row', - }, - desktopFrequencyDetails: { - maxHeight: 59, - }, - durationInput: { - fontSize: 18, - lineHeight: 27, - ...InterSemiBold, - width: '20%', - color: Colors.purple[400], - textAlign: 'center', - }, - durationLabel: { - ...InterSmall, - fontSize: 18, - lineHeight: 27, - color: Colors.purple[400], - textAlignVertical: 'bottom', - }, - downIcon: { - width: 24, - height: 24, - }, - descriptionLabel: { - fontSize: 12, - lineHeight: 18, - textAlign: 'right', - color: Colors.gray[200], - ...InterSmall, - }, - lastDesc: { - color: Colors.gray[200], - fontSize: 16, - lineHeight: 24, - textAlign: 'center', - ...InterSemiBold, - }, - warningView: { - width: '100%', - borderRadius: 4, - paddingHorizontal: 12, - paddingVertical: 8, - gap: 8, - backgroundColor: Colors.orange[100], - flex: 1, - flexDirection: 'row', - flexWrap: 'wrap', - }, - infoIcon: { - width: 16, - height: 16, - }, - actionHeader: { - gap: 4, - width: '100%', - }, - warningTitle: { - ...InterSemiBold, - color: Colors.orange[300], - fontSize: 14, - lineHeight: 21, - }, - warningLine: { - ...InterSmall, - color: Colors.orange[300], - fontSize: 14, - lineHeight: 21, - }, - reviewContainer: { - width: '100%', - gap: 24, - padding: 8, - borderRadius: 4, - backgroundColor: Colors.green[400], - }, - reviewSubtitle: { - fontSize: 18, - lineHeight: 27, - color: Colors.green[300], - ...InterSemiBold, - }, - reviewRow: { - flex: 1, - flexDirection: 'row', - justifyContent: 'space-between', - }, - reviewDesc: { - fontSize: 14, - lineHeight: 21, - color: Colors.gray[100], - ...InterSmall, - }, - italic: { fontStyle: 'italic' }, - frequencyWrapper: { gap: 17, zIndex: -1 }, - donationAction: { width: 'auto', flexGrow: 1 }, - donationCurrencyHeader: { flexDirection: 'row', width: 'auto', gap: 20 }, -}); +}; export default DonateComponent; diff --git a/packages/app/src/components/DonateFrequency.tsx b/packages/app/src/components/DonateFrequency.tsx new file mode 100644 index 00000000..3e0f4654 --- /dev/null +++ b/packages/app/src/components/DonateFrequency.tsx @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import { Box, HStack, Radio } from 'native-base'; + +interface DropdownProps { + onSelect: (value: string) => void; + options?: string[]; +} + +const WhiteDot = () => ; + +const FrequencySelector = ({ onSelect, options = ['One-Time', 'Monthly'] }: DropdownProps) => { + const [value, setValue] = useState('One-Time'); + + return ( + + { + setValue(v); + onSelect(v); + }} + style={{ flexDirection: 'row' }} + width="100%" + flexDir="row" + justifyContent="space-between"> + {options.map((option) => ( + } + value={option}> + {option === 'Monthly' ? 'Streaming' : option} + + ))} + + + ); +}; + +export default FrequencySelector; diff --git a/packages/app/src/components/Dropdown.tsx b/packages/app/src/components/Dropdown.tsx index c9c9cf22..f4d083bf 100644 --- a/packages/app/src/components/Dropdown.tsx +++ b/packages/app/src/components/Dropdown.tsx @@ -1,24 +1,18 @@ import { Image, Platform, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import { useState } from 'react'; +import { HStack, Pressable } from 'native-base'; + import { Colors } from '../utils/colors'; import { InterSemiBold, InterSmall } from '../utils/webFonts'; import { chevronDown } from '../assets'; -function renderDropdownItemText(current: string, selection: string) { +const renderDropdownItemText = (current: string, selection: string) => { if (current === selection) { return {selection}; } else { return {selection}; } -} - -function getDropdownBGC(openModal: boolean) { - if (openModal) { - return Colors.blue[100]; - } else { - return Colors.purple[100]; - } -} +}; interface DropdownProps { onSelect: (value: string) => void; @@ -26,26 +20,26 @@ interface DropdownProps { options: { value: string; label: string }[]; } -function Dropdown({ onSelect, value, options }: DropdownProps) { +const Dropdown = ({ onSelect, value, options }: DropdownProps) => { const [open, setOpen] = useState(false); return ( - - + { setOpen(!open); }}> {value} - + {open && ( {options.map((option) => ( @@ -61,42 +55,11 @@ function Dropdown({ onSelect, value, options }: DropdownProps) { ))} )} - + ); -} +}; const styles = StyleSheet.create({ - body: { - gap: 24, - paddingBottom: 32, - paddingTop: 32, - paddingHorizontal: 16, - backgroundColor: Colors.white, - }, - title: { - lineHeight: 25, - fontSize: 20, - textAlign: 'left', - ...InterSemiBold, - }, - form: { - alignItems: 'center', - width: '70%', - justifyContent: 'center', - }, - row: { - flexDirection: 'row', - gap: 8, - }, - button: { - gap: 2, - borderRadius: 12, - padding: 16, - minWidth: 105, - width: '100%', - height: 59, - justifyContent: 'space-between', - }, buttonText: { color: Colors.purple[400], fontSize: 18, @@ -121,7 +84,7 @@ const styles = StyleSheet.create({ paddingBottom: 10, position: 'absolute', zIndex: 2, - top: 60, + top: 50, left: 0, borderRadius: 12, shadowColor: Colors.black, @@ -141,13 +104,6 @@ const styles = StyleSheet.create({ minHeight: 60, alignItems: 'center', }, - dropdownSeparator: { - width: '100%', - height: 2, - backgroundColor: Colors.gray[600], - marginTop: 5, - marginBottom: 5, - }, dropdownMyProfileText: { fontSize: 18, marginLeft: 15, @@ -159,7 +115,6 @@ const styles = StyleSheet.create({ color: Colors.purple[400], textAlign: 'center', }, - dropdown: { position: 'relative' }, }); export default Dropdown; diff --git a/packages/app/src/components/NumberInput.tsx b/packages/app/src/components/NumberInput.tsx new file mode 100644 index 00000000..8e7c51c4 --- /dev/null +++ b/packages/app/src/components/NumberInput.tsx @@ -0,0 +1,90 @@ +import { useCallback, useState } from 'react'; +import { Box, HStack, Input, Text, VStack } from 'native-base'; +import { noop } from 'lodash'; + +import Dropdown from './Dropdown'; + +const NumberInput = ({ + type, + value, + onSelect = noop, + onChangeAmount, + options = [], + isWarning = false, +}: { + type: string; + value: string; + onSelect?: (v: string) => void; + onChangeAmount: (v: string) => void; + options?: { value: string; label: string }[]; + isWarning?: boolean; +}) => { + const [inputValue, setInputValue] = useState(0); + + const onChange = useCallback( + (v: string) => { + if (!/^\d+(\.\d{0,18})?$/.test(v)) { + console.error('Invalid input', v); + setInputValue((prev: any) => (v === '' ? 0 : prev)); + onChangeAmount(v); + return; + } + setInputValue(Number(v)); + onChangeAmount(v); + }, + [onChangeAmount] + ); + + return ( + + + {type === 'token' ? : } + + + + + + {type === 'duration' ? ( + + + / Month + + + ) : ( + <> + )} + + + ); +}; + +export default NumberInput; diff --git a/packages/app/src/components/ViewCollective.tsx b/packages/app/src/components/ViewCollective.tsx index ab3af162..051b0ece 100644 --- a/packages/app/src/components/ViewCollective.tsx +++ b/packages/app/src/components/ViewCollective.tsx @@ -9,7 +9,8 @@ import StewardList from './StewardsList/StewardsList'; import TransactionList from './TransactionList/TransactionList'; import { InterSemiBold, InterSmall } from '../utils/webFonts'; import useCrossNavigate from '../routes/useCrossNavigate'; -import StopDonationModal from './modals/StopDonationModal'; + +import BaseModal from './modals/BaseModal'; import { Colors } from '../utils/colors'; import { useScreenSize } from '../theme/hooks'; @@ -25,6 +26,7 @@ import { LastRowIcon, ListGreenIcon, Ocean, + QuestionImg, ReceiveLightIcon, SendIcon, SquaresIcon, @@ -35,7 +37,6 @@ import { } from '../assets/'; import { calculateGoodDollarAmounts } from '../lib/calculateGoodDollarAmounts'; import { useDeleteFlow } from '../hooks/useContractCalls/useDeleteFlow'; -import ErrorModal from './modals/ErrorModal'; import FlowingDonationsRowItem from './FlowingDonationsRowItem'; import { defaultInfoLabel, SUBGRAPH_POLL_INTERVAL } from '../models/constants'; import env from '../lib/env'; @@ -239,12 +240,22 @@ function ViewCollective({ collective }: ViewCollectiveProps) {
- setErrorMessage(undefined)} - message={errorMessage ?? ''} + onConfirm={() => setErrorMessage(undefined)} + /> + -
); @@ -392,12 +403,23 @@ function ViewCollective({ collective }: ViewCollectiveProps) { />
- setErrorMessage(undefined)} - message={errorMessage ?? ''} + errorMessage={errorMessage ?? ''} + onConfirm={() => setErrorMessage(undefined)} + /> + - ); diff --git a/packages/app/src/hooks/useSwapRoute.tsx b/packages/app/src/hooks/useSwapRoute.tsx index 928539a0..99171e85 100644 --- a/packages/app/src/hooks/useSwapRoute.tsx +++ b/packages/app/src/hooks/useSwapRoute.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react'; import { AlphaRouter, OnChainQuoteProvider, @@ -8,14 +9,16 @@ import { } from '@uniswap/smart-order-router'; import { CurrencyAmount, Percent, Token, TradeType } from '@uniswap/sdk-core'; import { useAccount, useNetwork } from 'wagmi'; +import { encodeRouteToPath } from '@uniswap/v3-sdk'; +import { Protocol } from '@uniswap/router-sdk'; +import Decimal from 'decimal.js'; +import { ethers } from 'ethers'; + import { SupportedNetwork } from '../models/constants'; import { useEthersProvider } from './useEthers'; import { calculateRawTotalDonation } from '../lib/calculateRawTotalDonation'; -import Decimal from 'decimal.js'; -import { useEffect, useState } from 'react'; -import { encodeRouteToPath } from '@uniswap/v3-sdk'; + import { useToken } from './useTokenList'; -import { Protocol } from '@uniswap/router-sdk'; export enum SwapRouteState { LOADING, @@ -37,6 +40,7 @@ export function useSwapRoute( rawMinimumAmountOut?: string; priceImpact?: number; status: SwapRouteState; + gasEstimate?: string; } { const { address } = useAccount(); const { chain } = useNetwork(); @@ -121,6 +125,8 @@ export function useSwapRoute( const quote = new Decimal(route.quote.toFixed(18)); const rawMinimumAmountOut = route.trade.minimumAmountOut(slippageTolerance).numerator.toString(); const priceImpact = parseFloat(route.trade.priceImpact.toFixed(4)); - return { path, quote, rawMinimumAmountOut, priceImpact, status: SwapRouteState.READY }; + const gasEstimate = ethers.utils.formatUnits(route.estimatedGasUsed, 'gwei'); + + return { path, quote, rawMinimumAmountOut, priceImpact, status: SwapRouteState.READY, gasEstimate }; } }