diff --git a/packages/app/package.json b/packages/app/package.json index 6156668f..add2b6aa 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -20,7 +20,7 @@ "@celo-tools/celo-ethers-wrapper": "^0.4.0", "@ethersproject/shims": "^5.7.0", "@gooddollar/good-design": "^0.1.57", - "@gooddollar/goodcollective-sdk": "^1.1.2", + "@gooddollar/goodcollective-sdk": "^1.2.1", "@gooddollar/web3sdk-v2": "^0.2.34", "@nerdwallet/apollo-cache-policies": "^3.2.0", "@react-native-aria/interactions": "0.2.3", diff --git a/packages/app/src/assets/MetaMask_Fox.svg b/packages/app/src/assets/MetaMaskLogo.svg similarity index 100% rename from packages/app/src/assets/MetaMask_Fox.svg rename to packages/app/src/assets/MetaMaskLogo.svg diff --git a/packages/app/src/assets/Stream-warning.svg b/packages/app/src/assets/Stream-warning.svg new file mode 100644 index 00000000..8571637c --- /dev/null +++ b/packages/app/src/assets/Stream-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/src/assets/index.ts b/packages/app/src/assets/index.ts index f40091a5..8d66a27b 100644 --- a/packages/app/src/assets/index.ts +++ b/packages/app/src/assets/index.ts @@ -5,7 +5,10 @@ export { default as BackIcon } from './BackIcon.svg'; export { default as BlackBrowedAlbatross } from './BlackBrowedAlbatross.svg'; export { default as CalendarIcon } from './CalendarIcon.svg'; export { default as CaliforniaCondor } from './CaliforniaCondor.svg'; +export { default as ClaimTX } from './ClaimTX.svg'; +export { default as ClaimTx } from './ClaimTx.svg'; export { default as CloseIcon } from './CloseIcon.svg'; +export { default as DonationTX } from './DonationTX.svg'; export { default as DonorBlue } from './Donor-blue.svg'; export { default as DonorGreenIcon } from './DonorGreenIcon.svg'; export { default as ErrorImg } from './ErrorImg.svg'; @@ -25,9 +28,11 @@ export { default as LightningIcon } from './LightningIcon.svg'; export { default as ListGreenIcon } from './List-greenIcon.svg'; export { default as Logout } from './Logout.svg'; export { default as MenuIcon } from './MenuIcon.svg'; +export { default as MetaMaskLogo } from './MetaMaskLogo.svg'; export { default as NoAvatar } from './NoAvatar.svg'; export { default as Ocean } from './Ocean.svg'; export { default as Paper } from './Paper.svg'; +export { default as PayoutTX } from './PayoutTX.svg'; export { default as PhoneImg } from './PhoneImg.svg'; export { default as PlaceholderAvatar } from './PlaceholderAvatar.svg'; export { default as QuestionImg } from './QuestionImg.svg'; @@ -40,23 +45,20 @@ export { default as SquaresIcon } from './SquaresIcon.svg'; export { default as StewardBlue } from './Steward-blue.svg'; export { default as StewardGreen } from './Steward-green.svg'; export { default as StewardOrange } from './Steward-orange.svg'; +export { default as StreamWarning } from './Stream-warning.svg'; +export { default as StreamStopTX } from './StreamStopTX.svg'; +export { default as StreamTX } from './StreamTX.svg'; +export { default as StreamUpdateTX } from './StreamUpdateTX.svg'; export { default as SupportImage } from './SupportImage.svg'; export { default as ThankYouImg } from './ThankYouImg.svg'; export { default as TransactionIcon } from './Transaction-Icon.svg'; export { default as TwitterIcon } from './TwitterIcon.svg'; export { default as VectorStroke } from './VectorStroke.svg'; export { default as VerifiedIcon } from './VerifiedIcon.svg'; +export { default as WalletConnectLogo } from './WalletConnectLogo.svg'; +export { default as WalletConnectLogoWhite } from './WalletConnectLogoWhite.svg'; export { default as WebIcon } from './WebIcon.svg'; export { default as Woman } from './Woman.svg'; export { default as chevronDown } from './chevron-down.svg'; export { default as chevronRight } from './chevron-right.svg'; export { default as empty } from './empty.svg'; -export { default as MetaMaskLogo } from './MetaMask_Fox.svg'; -export { default as WalletConnectLogo } from './WalletConnectLogo.svg'; -export { default as WalletConnectLogoWhite } from './WalletConnectLogoWhite.svg'; -export { default as StreamTX } from './StreamTX.svg'; -export { default as StreamStopTX } from './StreamStopTX.svg'; -export { default as StreamUpdateTX } from './StreamUpdateTX.svg'; -export { default as DonationTX } from './DonationTX.svg'; -export { default as ClaimTX } from './ClaimTX.svg'; -export { default as PayoutTX } from './PayoutTX.svg'; diff --git a/packages/app/src/components/ActionButton.tsx b/packages/app/src/components/ActionButton.tsx index 84ecd2fb..0bb862db 100644 --- a/packages/app/src/components/ActionButton.tsx +++ b/packages/app/src/components/ActionButton.tsx @@ -17,9 +17,10 @@ export const buttonStyles = { }, button: { width: '100%', - height: 47, + minHeight: 47, flex: 1, - justifyContent: 'space-between', + justifyContent: 'center', + textAlign: 'center', alignItems: 'center', paddingRight: 10, paddingLeft: 10, @@ -33,6 +34,7 @@ export const buttonStyles = { buttonText: { ...InterSemiBold, fontSize: 'md', + textAlign: 'center', }, }; @@ -45,7 +47,7 @@ const ActionButton = ({ href, text, bg, textColor, onPress }: ActionButtonProps) }, buttonText: { ...buttonStyles.buttonText, - height: 47, + minHeight: 47, display: 'flex', alignItems: 'center', }, diff --git a/packages/app/src/components/DonateComponent.tsx b/packages/app/src/components/DonateComponent.tsx index 6ee177fc..e94f9adb 100644 --- a/packages/app/src/components/DonateComponent.tsx +++ b/packages/app/src/components/DonateComponent.tsx @@ -1,50 +1,170 @@ 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, useBreakpointValue, 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 moment from 'moment'; -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 BaseModal from './modals/BaseModal'; +import { getDonateStyles } from '../utils'; import { useContractCalls, useGetTokenPrice } 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 { isDesktopView } = useScreenSize(); +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 impact', '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 shouldWarning = ( + currency: string, + donorCurrencyBalance: string, + priceImpact: number | undefined, + swapRouteStatus: SwapRouteState, + totalDecimalDonation: Decimal +) => { + 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; + + return { isNonZeroDonation, isInsufficientBalance, isInsufficientLiquidity, isUnacceptablePriceImpact }; +}; + +const SwapValue = ({ swapValue }: { swapValue: number }) => ( + + = + + {' '} + G${' '} + + {swapValue.toFixed(4)} + +); + +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, isMobileView } = useScreenSize(); const { id: collectiveId = '0x' } = useParams(); - const { address, isConnected } = useAccount(); + const { address } = useAccount(); const { chain } = useNetwork(); const [completeDonationModalVisible, setCompleteDonationModalVisible] = useState(false); const [errorMessage, setErrorMessage] = useState(undefined); const [approveSwapModalVisible, setApproveSwapModalVisible] = useState(false); const [thankYouModalVisible, setThankYouModalVisible] = useState(false); + const [startStreamingVisible, setStartStreamingVisible] = useState(false); + const [estimatedDuration, setEstimatedDuration] = useState<{ duration: number; endDate: string }>({ + duration: 0, + endDate: '', + }); + const { price: tokenPrice = 0 } = useGetTokenPrice('G$'); + const [swapValue, setSwapValue] = useState(undefined); const [isDonationComplete, setIsDonationComplete] = useState(false); const { navigate } = useCrossNavigate(); @@ -52,9 +172,46 @@ function DonateComponent({ collective }: DonateComponentProps) { navigate(`/profile/${address}`); } - const [frequency, setFrequency] = useState(Frequency.Monthly); + const [frequency, setFrequency] = useState(Frequency.OneTime); + const [streamRate, setRate] = useState(1); const [duration, setDuration] = useState(12); - const [decimalDonationAmount, setDecimalDonationAmount] = useState(0); + const [decimalDonationAmount, setDecimalDonationAmount] = useState(0); + + const [inputAmount, setInputAmount] = useState(undefined); + + const container = useBreakpointValue({ + base: { + width: '343', + paddingLeft: 2, + paddingRight: 2, + }, + sm: { + minWidth: '100%', + paddingLeft: 2, + paddingRight: 2, + }, + md: { + maxWidth: 800, + width: '100%', + paddingLeft: 4, + paddingRight: 4, + }, + xl: { + maxWidth: '100%', + paddingLeft: 4, + paddingRight: 4, + }, + }); + + const direction = useBreakpointValue({ + base: { + flexDirection: 'column', + }, + lg: { + flexDirection: 'row', + flexWrap: 'wrap', + }, + }); const tokenList = useTokenList(); const gdEnvSymbol = @@ -94,6 +251,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 +267,37 @@ 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, isInsufficientBalance, isInsufficientLiquidity, isUnacceptablePriceImpact } = + shouldWarning(currency, donorCurrencyBalance, priceImpact, swapRouteStatus, totalDecimalDonation); + + 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 +305,7 @@ function DonateComponent({ collective }: DonateComponentProps) { return await supportSingleWithSwap(); } } else if (currency.startsWith('G$')) { - return await supportFlow(); + handleStreamingWarning(); } let isApproveSuccess = isRequireApprove === false; @@ -143,7 +331,8 @@ function DonateComponent({ collective }: DonateComponentProps) { } } if (isApproveSuccess) { - await supportFlowWithSwap(); + // await supportFlowWithSwap(); + handleStreamingWarning(); } }, [ chain?.id, @@ -151,33 +340,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, @@ -201,521 +371,297 @@ function DonateComponent({ collective }: DonateComponentProps) { const { buttonCopy, buttonBgColor, buttonTextColor } = donateStyles; - const onChangeCurrency = (value: string) => setCurrency(value); - const onChangeAmount = (value: string) => setDecimalDonationAmount(formatDecimalStringInput(value)); - const onChangeFrequency = useCallback((value: string) => { + const onReset = () => { + setInputAmount(undefined); + setEstimatedDuration({ duration: 0, endDate: '' }); + setSwapValue(undefined); + }; + + const onChangeCurrency = (value: string) => { + setCurrency(value); + onReset(); + }; + + const estimateDuration = useCallback( + (v: string, altDuration?: number) => { + const calculateEstDuration = (value: number, rate: number) => value / (rate === 0 ? 1 : rate); + + let estDuration = altDuration ?? 0; + + if (frequency === Frequency.Monthly && !altDuration) { + if (currency.includes('G$')) { + estDuration = parseFloat(donorCurrencyBalance) / parseFloat(v); + } else { + estDuration = calculateEstDuration(parseFloat(v), parseFloat(streamRate.toString())); + } + } + + if (!altDuration && !currency.includes('G$')) { + const gdValue = parseFloat(v) / tokenPrice; + setSwapValue(gdValue); + } + + const estimatedEndDate = moment().add(estDuration, 'months').format('DD.MM.YY HH:mm'); + + setEstimatedDuration({ duration: estDuration, endDate: estimatedEndDate }); + setDuration(Math.max(1, estDuration)); + }, + [currency, donorCurrencyBalance, streamRate, frequency, tokenPrice] + ); + + const onChangeRate = (value: string) => { + setRate(value.endsWith('.') ? value : Number(value)); + + if (!Number(value)) return; + const estDuration = parseFloat(decimalDonationAmount) / Math.max(parseFloat(value), 1); + estimateDuration(value, estDuration); + }; + + const onChangeAmount = useCallback( + (v: string) => { + setInputAmount(v); + + if (![''].includes(v)) setConfirmNoAmount(false); + if (v.endsWith('.') || ['0', ''].includes(v)) return; + + setDecimalDonationAmount(formatDecimalStringInput(v)); + + estimateDuration(v); + }, + [estimateDuration] + ); + + const onChangeFrequency = (value: string) => { if (value === Frequency.OneTime) { setDuration(1); } setFrequency(value as Frequency); - }, []); - const onChangeDuration = (value: string) => setDuration(Number(value)); + onReset(); + }; + const onCloseErrorModal = () => setErrorMessage(undefined); + const onCloseThankYouModal = () => { + setThankYouModalVisible(false); + navigate(`/profile/${address}`); + }; + + const isWarning = isInsufficientBalance || isInsufficientLiquidity || isUnacceptablePriceImpact || confirmNoAmount; 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 - - - - - - - - Donation Frequency - - How often do you want to donate this {!isDesktopView &&
} amount? -
-
- -
- - {frequency !== 'One-Time' && ( - - For How Long: - - + + {/* 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 as Frequency)} - - - )} - -
- )} - - - <> - {!isDesktopView && ( - <> - - {frequency !== 'One-Time' && ( - - For How Long: - - - - {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'} - - + + + + Donate + + + Support {collective.ipfs.name} + by donating any amount you want either one time or streaming at a monthly rate. + + + + + {/* 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' && isNonZeroDonation && swapValue ? ( + + ) : null} + + + + {frequency !== 'One-Time' && !currency.includes('G$') ? ( + <> + + + 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'} - - - - - - )} - - {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'} - - - - - - )} - - + + + {' '} + + ) : null} + {frequency !== 'One-Time' && isNonZeroDonation && !isEmpty(estimatedDuration.endDate) ? ( + + + + Estimated duration: + + + {parseFloat(estimatedDuration.duration.toFixed(2))} months + + + + + Estimated End Date: + + + {estimatedDuration.endDate} + + + + ) : null} + + + {frequency !== 'One-Time' && currency === 'CELO' && isNonZeroDonation && swapValue ? ( + + + Total Donation Swap Amount: + + + + CELO {decimalDonationAmount} + + + + + ) : null} + + + + {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} + + +
+ + ); -} - -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..82a92ce3 --- /dev/null +++ b/packages/app/src/components/DonateFrequency.tsx @@ -0,0 +1,47 @@ +import { useState } from 'react'; +import { Box, HStack, Radio } from 'native-base'; + +import { useScreenSize } from '../theme/hooks'; + +interface DropdownProps { + onSelect: (value: string) => void; + options?: string[]; +} + +const WhiteDot = () => ; + +const FrequencySelector = ({ onSelect, options = ['One-Time', 'Monthly'] }: DropdownProps) => { + const [value, setValue] = useState('One-Time'); + const { isTabletView } = useScreenSize(); + + 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..ae57425c 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,27 +20,27 @@ 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 && ( + + {open ? ( {options.map((option) => ( ))} - )} - + ) : null} + ); -} +}; 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/Layout/Layout.tsx b/packages/app/src/components/Layout/Layout.tsx index 83aacf52..dc12b005 100644 --- a/packages/app/src/components/Layout/Layout.tsx +++ b/packages/app/src/components/Layout/Layout.tsx @@ -24,7 +24,7 @@ function Layout({ children, breadcrumbPath }: LayoutProps) { const scrollViewHeight = safeAreaHeight - 105; const { address } = useAccount(); - const { isDesktopView } = useScreenSize(); + const { isDesktopView, isMobileView, isTabletView } = useScreenSize(); const location = useLocation(); const { navigate } = useCrossNavigate(); @@ -39,8 +39,9 @@ function Layout({ children, breadcrumbPath }: LayoutProps) { const scrollViewStyles = [ styles.scrollView, - { maxHeight: scrollViewHeight, minHeight: scrollViewHeight }, + { ...(!isMobileView && { maxHeight: scrollViewHeight, minHeight: scrollViewHeight }) }, { paddingBottom: isCollectivePage ? 61 : 0 }, + { paddingHorizontal: isTabletView ? 48 : isMobileView ? 8 : 24 }, ]; return ( diff --git a/packages/app/src/components/NumberInput.tsx b/packages/app/src/components/NumberInput.tsx new file mode 100644 index 00000000..b684ea34 --- /dev/null +++ b/packages/app/src/components/NumberInput.tsx @@ -0,0 +1,91 @@ +import { useCallback } from 'react'; +import { Box, HStack, Input, Text, VStack } from 'native-base'; +import { noop } from 'lodash'; + +import Dropdown from './Dropdown'; + +const NumberInput = ({ + type, + dropdownValue, + onSelect = noop, + onChangeAmount, + options = [], + isWarning = false, + inputValue = undefined, +}: { + type: string; + dropdownValue: string; + onSelect?: (v: string) => void; + onChangeAmount: (v: string) => void; + options?: { value: string; label: string }[]; + isWarning?: boolean; + inputValue?: string | undefined; +}) => { + const onChange = useCallback( + (v: string) => { + if (!/^\d+(\.\d{0,18})?$/.test(v)) { + console.error('Invalid input', v); + if (v !== '') { + return; + } + } + + onChangeAmount(v); + }, + [onChangeAmount] + ); + return ( + + + {type === 'token' ? : } + + + + + + {type === 'duration' ? ( + + + / Month + + + ) : ( + <> + )} + + + ); +}; + +export default NumberInput; diff --git a/packages/app/src/components/StopDonationActionButton.tsx b/packages/app/src/components/StopDonationActionButton.tsx index ced4af7f..d12526e8 100644 --- a/packages/app/src/components/StopDonationActionButton.tsx +++ b/packages/app/src/components/StopDonationActionButton.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import ErrorModal from './modals/ErrorModal'; -import StopDonationModal from './modals/StopDonationModal'; +import { QuestionImg } from '../assets'; +import BaseModal from './modals/BaseModal'; import ProcessingModal from './modals/ProcessingModal'; import { useDeleteFlow } from '../hooks/useContractCalls/useDeleteFlow'; import RoundedButton from './RoundedButton'; @@ -30,12 +30,23 @@ export const StopDonationActionButton = ({ donorCollective }: { donorCollective: color={Colors.orange[300]} onPress={handleStopDonation} /> - setErrorMessage(undefined)} - message={errorMessage ?? ''} + errorMessage={errorMessage ?? ''} + onConfirm={() => setErrorMessage(undefined)} + /> + -
); diff --git a/packages/app/src/components/modals/ApproveSwapModal.tsx b/packages/app/src/components/modals/ApproveSwapModal.tsx deleted file mode 100644 index 50e08544..00000000 --- a/packages/app/src/components/modals/ApproveSwapModal.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Modal, StyleSheet, Text, View, Image, TouchableOpacity, Platform } from 'react-native'; -import { InterRegular, InterSemiBold } from '../../utils/webFonts'; -import { Colors } from '../../utils/colors'; -import { modalStyles } from '../shared'; -import { CloseIcon, ApproveTokenImg } from '../../assets'; - -interface AproveSwapModalProps { - openModal: boolean; - setOpenModal: any; -} - -const ApproveSwapModal = ({ openModal, setOpenModal }: AproveSwapModalProps) => { - return ( - - - - - setOpenModal(false)}> - - - - APPROVE TOKEN SWAP - - To approve the exchange from your donation currency to this GoodCollective's currency, sign with your - wallet. - - woman - - - - ); -}; - -const styles = StyleSheet.create({ - centeredView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - marginTop: 22, - }, - modalView: { - maxWidth: '90%', - maxHeight: '90%', - // @ts-ignore - overflowY: Platform.select({ - native: 'scroll', - default: 'auto', - }), - margin: 20, - backgroundColor: Colors.blue[100], - borderRadius: 20, - paddingTop: 24, - paddingHorizontal: 24, - paddingBottom: 40, - gap: 24, - alignItems: 'center', - shadowColor: Colors.black, - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 5, - }, - title: { - fontSize: 30, - textAlign: 'center', - marginHorizontal: 0, - ...InterSemiBold, - }, - paragraph: { - ...InterRegular, - fontSize: 18, - textAlign: 'center', - width: '100%', - lineHeight: 27, - }, - image: { - alignSelf: 'center', - width: 295, - height: 224, - }, - closeIcon: { - width: 24, - height: 24, - alignSelf: 'flex-end', - }, - - button: { - backgroundColor: Colors.orange[100], - width: '100%', - borderRadius: 30, - paddingTop: 12, - paddingRight: 22, - paddingBottom: 12, - paddingLeft: 20, - gap: 8, - alignContent: 'center', - }, - buttonText: { - ...InterSemiBold, - fontSize: 18, - textAlign: 'center', - alignSelf: 'center', - color: Colors.orange[300], - }, - - textStyle: { - color: 'white', - fontWeight: 'bold', - textAlign: 'center', - }, - modalText: { - marginBottom: 15, - textAlign: 'center', - }, -}); - -export default ApproveSwapModal; diff --git a/packages/app/src/components/modals/BaseModal.tsx b/packages/app/src/components/modals/BaseModal.tsx new file mode 100644 index 00000000..7bcfed7e --- /dev/null +++ b/packages/app/src/components/modals/BaseModal.tsx @@ -0,0 +1,135 @@ +import { Modal, Platform } from 'react-native'; +import { Image, Pressable, Text, VStack } from 'native-base'; + +import ActionButton from '../ActionButton'; +import { CloseIcon, ThankYouImg } from '../../assets'; + +const modalView = { + maxHeight: '90%', + // @ts-ignore + overflowY: Platform.select({ + native: 'scroll', + default: 'auto', + }) as any, + margin: 20, + backgroundColor: 'goodPurple.300', + borderRadius: 20, + paddingTop: 8, + paddingX: 8, + paddingBottom: 10, + ...Platform.select({ + web: { + gap: 24, + maxWidth: '420', + }, + native: { + maxWidth: '90%', + }, + }), + alignItems: 'center', +}; + +const defaultModalProps = { + error: { + dTitle: 'Something went wrong', + dParagraphs: (errorMessage: string) => ['Please try again later.', 'Reason: ' + (errorMessage ?? 'unknown')], + dConfirmButtonText: 'OK', + dImage: ThankYouImg, + }, +}; + +type BaseModalProps = { + type?: 'error'; + openModal: boolean; + setOpenModal: (open: boolean) => void; + onConfirm?: () => void; + title?: string; + paragraphs?: any[] | undefined; + confirmButtonText?: string; + image?: any; + errorMessage?: string; + withClose?: boolean; +}; + +export const BaseModal = ({ + type, + openModal, + setOpenModal, + onConfirm, + title, + paragraphs, + confirmButtonText, + image, + errorMessage = '', + withClose = true, +}: BaseModalProps) => { + const onClose = () => setOpenModal(false); + const { dTitle, dParagraphs, dConfirmButtonText, dImage } = + type === 'error' + ? defaultModalProps[type as keyof typeof defaultModalProps] + : { dTitle: title, dParagraphs: paragraphs, dConfirmButtonText: confirmButtonText, dImage: image }; + + const paragraph = + type === 'error' && typeof dParagraphs === 'function' + ? dParagraphs?.(errorMessage) + : (dParagraphs as any[] | undefined); + + return ( + + + + {withClose ? ( + + + + + + ) : null} + + + {dTitle?.toUpperCase()} + + + {paragraph + ? // eslint-disable-next-line @typescript-eslint/no-shadow + paragraph.map((paragraph: string, index: number) => + paragraph ? ( + + {paragraph} + + ) : null + ) + : null} + + woman + {dConfirmButtonText ? ( + + ) : null} + + + + + ); +}; + +export default BaseModal; diff --git a/packages/app/src/components/modals/CompleteDonationModal.tsx b/packages/app/src/components/modals/CompleteDonationModal.tsx deleted file mode 100644 index 9be5312b..00000000 --- a/packages/app/src/components/modals/CompleteDonationModal.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { Modal, StyleSheet, Text, View, Image, TouchableOpacity, Platform } from 'react-native'; -import { InterRegular, InterSemiBold } from '../../utils/webFonts'; -import { Colors } from '../../utils/colors'; -import { modalStyles } from '../shared'; -import { CloseIcon, PhoneImg } from '../../assets'; - -interface CompleteDonationModalProps { - openModal: boolean; - setOpenModal: any; -} - -const CompleteDonationModal = ({ openModal, setOpenModal }: CompleteDonationModalProps) => { - const onClickClose = () => setOpenModal(false); - - return ( - - - - - - - - - COMPLETE YOUR DONATION - To complete your donation, sign with your wallet. - woman - - - - ); -}; - -const styles = StyleSheet.create({ - centeredView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - marginTop: 22, - }, - modalView: { - maxWidth: '90%', - maxHeight: '90%', - overflowY: Platform.select({ - native: 'scroll', - default: 'auto', - }), - margin: 20, - backgroundColor: Colors.blue[100], - borderRadius: 20, - paddingTop: 24, - paddingHorizontal: 24, - paddingBottom: 40, - gap: 24, - alignItems: 'center', - shadowColor: Colors.black, - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 5, - }, - title: { - fontSize: 30, - textAlign: 'center', - marginHorizontal: 0, - ...InterSemiBold, - }, - paragraph: { - ...InterRegular, - fontSize: 18, - textAlign: 'center', - width: '100%', - lineHeight: 27, - }, - image: { - alignSelf: 'center', - width: 190, - height: 224, - }, - closeIcon: { - width: 24, - height: 24, - alignSelf: 'flex-end', - }, - - button: { - backgroundColor: Colors.orange[100], - width: '100%', - borderRadius: 30, - paddingTop: 12, - paddingRight: 22, - paddingBottom: 12, - paddingLeft: 20, - gap: 8, - alignContent: 'center', - }, - buttonText: { - ...InterSemiBold, - fontSize: 18, - textAlign: 'center', - alignSelf: 'center', - color: Colors.orange[300], - }, - - textStyle: { - color: 'white', - fontWeight: 'bold', - textAlign: 'center', - }, - modalText: { - marginBottom: 15, - textAlign: 'center', - }, -}); - -export default CompleteDonationModal; diff --git a/packages/app/src/components/modals/ErrorModal.tsx b/packages/app/src/components/modals/ErrorModal.tsx deleted file mode 100644 index 0aebe43c..00000000 --- a/packages/app/src/components/modals/ErrorModal.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { Modal, StyleSheet, Text, View, Image, TouchableOpacity, Platform } from 'react-native'; -import { InterRegular, InterSemiBold } from '../../utils/webFonts'; -import { Colors } from '../../utils/colors'; -import { CloseIcon, ThankYouImg } from '../../assets'; - -interface ErrorModalProps { - openModal: boolean; - setOpenModal: any; - message: string; -} - -const ErrorModal = ({ openModal, setOpenModal, message }: ErrorModalProps) => { - const onCloseClicked = () => setOpenModal(false); - return ( - - - - - - - - - - SOMETHING WENT WRONG - Please try again later. - Reason: {message} - woman - - OK - - - - - ); -}; - -const styles = StyleSheet.create({ - centeredView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - marginTop: 22, - }, - modalView: { - maxWidth: '90%', - maxHeight: '90%', - // @ts-ignore - overflowY: Platform.select({ - native: 'scroll', - default: 'auto', - }), - margin: 20, - backgroundColor: Colors.blue[100], - borderRadius: 20, - paddingTop: 24, - paddingHorizontal: 24, - paddingBottom: 40, - gap: 24, - alignItems: 'center', - shadowColor: Colors.black, - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 5, - }, - title: { - fontSize: 30, - textAlign: 'center', - marginHorizontal: 0, - ...InterSemiBold, - }, - paragraph: { - ...InterRegular, - fontSize: 18, - textAlign: 'center', - width: '100%', - lineHeight: 27, - }, - image: { - alignSelf: 'center', - width: 190, - height: 224, - }, - closeIcon: { - width: 24, - height: 24, - alignSelf: 'flex-end', - }, - - button: { - backgroundColor: Colors.orange[100], - width: '80%', - borderRadius: 30, - paddingTop: 12, - paddingRight: 22, - paddingBottom: 12, - paddingLeft: 20, - gap: 8, - alignContent: 'center', - }, - buttonText: { - ...InterSemiBold, - fontSize: 18, - textAlign: 'center', - alignSelf: 'center', - color: Colors.orange[300], - }, - - textStyle: { - color: 'white', - fontWeight: 'bold', - textAlign: 'center', - }, - modalText: { - marginBottom: 15, - textAlign: 'center', - }, - modalCloseIconWrapper: { width: '100%', alignContent: 'flex-end' }, - modalCloseIcon: { width: 24, height: 24, alignSelf: 'flex-end' }, -}); - -export default ErrorModal; diff --git a/packages/app/src/components/modals/StopDonationModal.tsx b/packages/app/src/components/modals/StopDonationModal.tsx deleted file mode 100644 index 82be1c7a..00000000 --- a/packages/app/src/components/modals/StopDonationModal.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { Modal, StyleSheet, Text, View, Image, TouchableOpacity, Platform } from 'react-native'; -import { InterRegular, InterSemiBold } from '../../utils/webFonts'; -import { Colors } from '../../utils/colors'; -import { QuestionImg } from '../../assets'; - -interface StopDonationModalProps { - openModal: boolean; - setOpenModal: any; -} - -const StopDonationModal = ({ openModal, setOpenModal }: StopDonationModalProps) => { - return ( - - - - ARE YOU SURE YOU WANT TO STOP YOUR DONATION? - - If so, please sign with your wallet. If not, please click below to return to the GoodCollective you support. - - - woman - setOpenModal(false)}> - GO BACK - - - - - ); -}; - -const styles = StyleSheet.create({ - centeredView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - marginTop: 22, - }, - modalView: { - maxWidth: '90%', - maxHeight: '90%', - // @ts-ignore - overflowY: Platform.select({ - native: 'scroll', - default: 'auto', - }), - margin: 20, - backgroundColor: Colors.blue[100], - borderRadius: 20, - paddingTop: 24, - paddingHorizontal: 24, - paddingBottom: 40, - gap: 24, - alignItems: 'center', - shadowColor: Colors.black, - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 5, - }, - title: { - fontSize: 30, - textAlign: 'center', - marginHorizontal: 0, - ...InterSemiBold, - }, - paragraph: { - ...InterRegular, - fontSize: 18, - textAlign: 'center', - width: '100%', - lineHeight: 27, - }, - image: { - alignSelf: 'center', - width: 190, - height: 224, - }, - closeIcon: { - width: 24, - height: 24, - alignSelf: 'flex-end', - }, - - button: { - backgroundColor: Colors.orange[100], - width: '100%', - borderRadius: 30, - paddingTop: 12, - paddingRight: 22, - paddingBottom: 12, - paddingLeft: 20, - gap: 8, - alignContent: 'center', - }, - buttonText: { - ...InterSemiBold, - fontSize: 18, - textAlign: 'center', - alignSelf: 'center', - color: Colors.orange[300], - }, - - textStyle: { - color: 'white', - fontWeight: 'bold', - textAlign: 'center', - }, - modalText: { - marginBottom: 15, - textAlign: 'center', - }, -}); - -export default StopDonationModal; diff --git a/packages/app/src/components/modals/ThankYouModal.tsx b/packages/app/src/components/modals/ThankYouModal.tsx deleted file mode 100644 index fc15ddb5..00000000 --- a/packages/app/src/components/modals/ThankYouModal.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { Modal, StyleSheet, Text, View, Image, TouchableOpacity, Platform } from 'react-native'; -import { InterRegular, InterSemiBold } from '../../utils/webFonts'; -import { Colors } from '../../utils/colors'; -import { CloseIcon, ThankYouImg } from '../../assets'; -import { IpfsCollective } from '../../models/models'; -import useCrossNavigate from '../../routes/useCrossNavigate'; -import { modalStyles } from '../shared'; -import { useEffect, useState } from 'react'; - -interface ThankYouModalProps { - openModal: boolean; - address?: `0x${string}`; - collective: IpfsCollective; - isStream: boolean; -} - -const ThankYouModal = ({ openModal, address, collective, isStream }: ThankYouModalProps) => { - const [isOpen, setOpenModal] = useState(openModal); - const { navigate } = useCrossNavigate(); - const onClick = () => navigate(`/profile/${address}`); - useEffect(() => { - setOpenModal(openModal); - }, [openModal]); - return ( - - - - - setOpenModal(false)}> - - - - THANK YOU! - {`You have just donated to ${collective.name} GoodCollective!`} - {isStream && ( - - {`To stop your donation, visit the ${collective.name} GoodCollective page.`} - - )} - woman - - GO TO PROFILE - - - - - ); -}; - -const styles = StyleSheet.create({ - centeredView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - marginTop: 22, - }, - modalView: { - maxWidth: '90%', - maxHeight: '90%', - // @ts-ignore - overflowY: Platform.select({ - native: 'scroll', - default: 'auto', - }), - margin: 20, - backgroundColor: Colors.blue[100], - borderRadius: 20, - paddingTop: 24, - paddingHorizontal: 24, - paddingBottom: 40, - gap: 24, - alignItems: 'center', - shadowColor: Colors.black, - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 5, - }, - title: { - fontSize: 30, - textAlign: 'center', - marginHorizontal: 0, - ...InterSemiBold, - }, - paragraph: { - ...InterRegular, - fontSize: 18, - textAlign: 'center', - width: '100%', - lineHeight: 27, - }, - image: { - alignSelf: 'center', - width: 286, - height: 214, - }, - closeIcon: { - width: 24, - height: 24, - alignSelf: 'flex-end', - }, - - button: { - backgroundColor: Colors.orange[100], - width: '100%', - borderRadius: 30, - paddingTop: 12, - paddingBottom: 12, - gap: 8, - alignContent: 'center', - }, - buttonText: { - ...InterSemiBold, - fontSize: 18, - textAlign: 'center', - alignSelf: 'center', - color: Colors.orange[300], - }, - textStyle: { - color: 'white', - fontWeight: 'bold', - textAlign: 'center', - }, - modalText: { - marginBottom: 15, - textAlign: 'center', - }, -}); - -export default ThankYouModal; diff --git a/packages/app/src/hooks/useGetTokenBalance.ts b/packages/app/src/hooks/useGetTokenBalance.ts index 73a408d2..318d03f1 100644 --- a/packages/app/src/hooks/useGetTokenBalance.ts +++ b/packages/app/src/hooks/useGetTokenBalance.ts @@ -1,7 +1,6 @@ import { SupportedNetwork } from '../models/constants'; import { useEffect, useState } from 'react'; import { fetchBalance } from 'wagmi/actions'; -import { useToken } from './useTokenList'; export const useGetTokenBalance = ( currencyAddress: string, 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 }; } } diff --git a/packages/app/src/models/constants.ts b/packages/app/src/models/constants.ts index ded429f6..e13db213 100644 --- a/packages/app/src/models/constants.ts +++ b/packages/app/src/models/constants.ts @@ -38,7 +38,7 @@ export const coingeckoTokenMapping: Record = { export enum Frequency { OneTime = 'One-Time', - Monthly = 'Monthly', + Monthly = 'Monthly', // streaming } // constructed from Frequency diff --git a/packages/app/src/pages/ActivityLogPage.tsx b/packages/app/src/pages/ActivityLogPage.tsx index e2f120a1..83fa27b9 100644 --- a/packages/app/src/pages/ActivityLogPage.tsx +++ b/packages/app/src/pages/ActivityLogPage.tsx @@ -42,7 +42,6 @@ const styles = StyleSheet.create({ body: { gap: 24, backgroundColor: Colors.white, - maxWidth: 1280, }, container: { diff --git a/packages/app/src/theme/theme.ts b/packages/app/src/theme/theme.ts index 5e2bddbe..e5ff5681 100644 --- a/packages/app/src/theme/theme.ts +++ b/packages/app/src/theme/theme.ts @@ -12,6 +12,7 @@ export const nbTheme = extendTheme({ // text goodGrey: { + 25: '#959090', 50: '#F3F3F3', 100: '#F4F4F4', 200: '#E6E6E6', @@ -94,6 +95,13 @@ export const nbTheme = extendTheme({ lineHeight: '150%' /* 15px */, }, variants: { + bold: () => ({ + fontFamily: 'Inter', + fontSize: 'sm', + fontStyle: 'normal', + fontWeight: 700, + lineHeight: '150%' /* 30px */, + }), // title/heading fontsize variants '2xs-grey': () => ({ fontSize: 10, diff --git a/packages/app/src/utils/index.tsx b/packages/app/src/utils/index.tsx index 38b9b0f0..c9567dce 100644 --- a/packages/app/src/utils/index.tsx +++ b/packages/app/src/utils/index.tsx @@ -44,7 +44,7 @@ const BUTTON_STATE = { textColor: Colors.gray[300], }, isZeroDonation: { - copy: 'Donation amount is zero', + copy: 'Confirm', bgColor: Colors.gray[1000], textColor: Colors.gray[300], }, diff --git a/yarn.lock b/yarn.lock index bf013e1c..a6f184a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4489,7 +4489,7 @@ __metadata: "@celo-tools/celo-ethers-wrapper": ^0.4.0 "@ethersproject/shims": ^5.7.0 "@gooddollar/good-design": ^0.1.57 - "@gooddollar/goodcollective-sdk": ^1.1.2 + "@gooddollar/goodcollective-sdk": ^1.2.1 "@gooddollar/web3sdk-v2": ^0.2.34 "@nerdwallet/apollo-cache-policies": ^3.2.0 "@react-native-aria/interactions": 0.2.3 @@ -4615,7 +4615,7 @@ __metadata: languageName: unknown linkType: soft -"@gooddollar/goodcollective-sdk@^1.1.2, @gooddollar/goodcollective-sdk@workspace:packages/sdk-js": +"@gooddollar/goodcollective-sdk@^1.2.1, @gooddollar/goodcollective-sdk@workspace:packages/sdk-js": version: 0.0.0-use.local resolution: "@gooddollar/goodcollective-sdk@workspace:packages/sdk-js" dependencies: