Skip to content

Commit

Permalink
Merge pull request #212 from GoodDollar/203+185-monthly-and-single-swap
Browse files Browse the repository at this point in the history
Monthly only stream and donate in any currency
  • Loading branch information
sirpy authored Jul 10, 2024
2 parents af6292b + 5e9ce9a commit 83da2df
Show file tree
Hide file tree
Showing 35 changed files with 797 additions and 312 deletions.
14 changes: 7 additions & 7 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"preview:web": "vite preview -c web/vite.config.ts"
},
"dependencies": {
"@apollo/client": "^3.7.14",
"@apollo/client": "^3.10.8",
"@celo-tools/celo-ethers-wrapper": "^0.4.0",
"@ethersproject/shims": "^5.7.0",
"@gooddollar/good-design": "^0.1.31",
Expand All @@ -28,10 +28,10 @@
"@react-native-firebase/analytics": "16.7.0",
"@react-native-firebase/app": "16.7.0",
"@superfluid-finance/sdk-core": "^0.3.2",
"@uniswap/router-sdk": "^1.7.1",
"@uniswap/sdk-core": "^4.0.7",
"@uniswap/smart-order-router": "^3.20.0",
"@uniswap/v3-sdk": "^3.10.0",
"@uniswap/router-sdk": "^1.9.3",
"@uniswap/sdk-core": "^5.3.1",
"@uniswap/smart-order-router": "^3.35.12",
"@uniswap/v3-sdk": "^3.13.1",
"@usedapp/core": "^1.2.10",
"@wagmi/core": "^1.4.5",
"@walletconnect/modal-react-native": "^1.0.0-rc.9",
Expand All @@ -46,15 +46,15 @@
"decimal.js": "^10.4.3",
"ethers": "^5.6.2",
"fast-text-encoding": "^1.0.6",
"graphql": "^16.6.0",
"graphql": "^16.9.0",
"lodash": "^4.17.21",
"mixpanel-react-native": "^2.3.1",
"mobile-device-detect": "^0.4.3",
"moment": "^2.29.4",
"moment-duration-format": "^2.3.2",
"native-base": "^3.4.28",
"node-libs-react-native": "^1.2.1",
"qs": "^6.11.2",
"qs": "^6.12.2",
"react": "18",
"react-dom": "18",
"react-native": "^0.71.11",
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { MongoDbApolloProvider } from './components/providers/MongoDbApolloProvi
function App(): JSX.Element {
const { publicClient, webSocketPublicClient } = configureChains(
[celo, mainnet],
[infuraProvider({ apiKey: '88284fbbacd3472ca3361d1317a48fa5' }), publicProvider()]
[publicProvider(), infuraProvider({ apiKey: '88284fbbacd3472ca3361d1317a48fa5' })]
);

const connectors = [
Expand Down
53 changes: 30 additions & 23 deletions packages/app/src/components/DonateComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,18 @@ function DonateComponent({ collective }: DonateComponentProps) {
}

const [currency, setCurrency] = useState<string>('G$');
const [frequency, setFrequency] = useState<Frequency>(Frequency.OneTime);
const [duration, setDuration] = useState(1);
const [frequency, setFrequency] = useState<Frequency>(Frequency.Monthly);
const [duration, setDuration] = useState(12);
const [decimalDonationAmount, setDecimalDonationAmount] = useState(0);

const tokenList = useTokenList();
const isOneTime = frequency === Frequency.OneTime;
const currencyOptions: { value: string; label: string }[] = useMemo(() => {
let options = Object.keys(tokenList).map((key) => ({
value: key,
label: key,
}));
if (isOneTime) {
options = [options.find((option) => option.value === 'G$')!];
}
return options;
}, [tokenList, isOneTime]);
}, [tokenList]);

const {
path: swapPath,
Expand All @@ -85,7 +81,7 @@ function DonateComponent({ collective }: DonateComponentProps) {
);
const approvalNotReady = handleApproveToken === undefined && currency !== 'G$';

const { supportFlowWithSwap, supportFlow, supportSingleTransferAndCall } = useContractCalls(
const { supportFlowWithSwap, supportFlow, supportSingleTransferAndCall, supportSingleWithSwap } = useContractCalls(
collectiveId,
currency,
decimalDonationAmount,
Expand All @@ -101,7 +97,11 @@ function DonateComponent({ collective }: DonateComponentProps) {

const handleDonate = useCallback(async () => {
if (frequency === Frequency.OneTime) {
return await supportSingleTransferAndCall();
if (currency === 'G$') {
return await supportSingleTransferAndCall();
} else {
return await supportSingleWithSwap();
}
} else if (currency === 'G$') {
return await supportFlow();
}
Expand Down Expand Up @@ -140,6 +140,7 @@ function DonateComponent({ collective }: DonateComponentProps) {
supportFlow,
supportFlowWithSwap,
supportSingleTransferAndCall,
supportSingleWithSwap,
]);

const currencyDecimals = useToken(currency).decimals;
Expand All @@ -151,7 +152,7 @@ function DonateComponent({ collective }: DonateComponentProps) {
const isNonZeroDonation = totalDecimalDonation.gt(0);
const isInsufficientBalance =
isNonZeroDonation && (!donorCurrencyBalance || totalDecimalDonation.gt(donorCurrencyBalance));
const isInsufficientLiquidity = isNonZeroDonation && currency !== 'G$' && swapRouteStatus !== SwapRouteState.READY;
const isInsufficientLiquidity = isNonZeroDonation && currency !== 'G$' && swapRouteStatus === SwapRouteState.NO_ROUTE;
const isUnacceptablePriceImpact =
isNonZeroDonation && currency !== 'G$' && priceImpact ? priceImpact > acceptablePriceImpact : false;

Expand Down Expand Up @@ -184,16 +185,12 @@ function DonateComponent({ collective }: DonateComponentProps) {

const onChangeCurrency = (value: string) => setCurrency(value);
const onChangeAmount = (value: string) => setDecimalDonationAmount(formatDecimalStringInput(value));
const onChangeFrequency = useCallback(
(value: string) => {
if (currency !== 'G$' && value === Frequency.OneTime) {
setCurrency('G$');
setDecimalDonationAmount(0);
}
setFrequency(value as Frequency);
},
[currency]
);
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);

Expand Down Expand Up @@ -245,7 +242,7 @@ function DonateComponent({ collective }: DonateComponentProps) {
{isDesktopResolution && (
<View style={styles.donationCurrencyHeader}>
<View style={styles.donationAction}>
<View>
<View style={styles.actionBox}>
<Text style={styles.title}>Donation Currency:</Text>
<Text style={styles.description}>You can donate using any cryptocurrency. </Text>
</View>
Expand Down Expand Up @@ -446,7 +443,10 @@ function DonateComponent({ collective }: DonateComponentProps) {
<Text style={styles.warningTitle}>Price impace warning!</Text>
<Text style={styles.warningLine}>
Due to low liquidity between your chosen currency and GoodDollar,
<Text style={{ ...InterSemiBold }}>your donation amount will reduce by 36% </Text>
<Text style={{ ...InterSemiBold }}>
{' '}
your donation amount will reduce by {priceImpact?.toFixed(2)}%{' '}
</Text>
when swapped.
</Text>
</View>
Expand Down Expand Up @@ -485,7 +485,9 @@ function DonateComponent({ collective }: DonateComponentProps) {
fontSize={18}
seeType={false}
onPress={handleDonate}
isLoading={swapRouteStatus === SwapRouteState.LOADING}
disabled={
(currency !== 'G$' && swapRouteStatus !== SwapRouteState.READY) ||
address === undefined ||
chain?.id === undefined ||
!(chain.id in SupportedNetwork) ||
Expand All @@ -497,7 +499,12 @@ function DonateComponent({ collective }: DonateComponentProps) {
<ErrorModal openModal={!!errorMessage} setOpenModal={onCloseErrorModal} message={errorMessage ?? ''} />
<ApproveSwapModal openModal={approveSwapModalVisible} setOpenModal={setApproveSwapModalVisible} />
<CompleteDonationModal openModal={completeDonationModalVisible} setOpenModal={setCompleteDonationModalVisible} />
<ThankYouModal openModal={thankYouModalVisible} address={address} collective={collective} />
<ThankYouModal
openModal={thankYouModalVisible}
address={address}
collective={collective}
isStream={frequency !== Frequency.OneTime}
/>
</View>
);
}
Expand Down
2 changes: 0 additions & 2 deletions packages/app/src/components/DonorsList/DonorsListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { Colors } from '../../utils/colors';
import { InterRegular, InterSemiBold } from '../../utils/webFonts';
import { DonorCollective } from '../../models/models';
import useCrossNavigate from '../../routes/useCrossNavigate';
import Decimal from 'decimal.js';
import { formatAddress } from '../../lib/formatAddress';
import { ethers } from 'ethers';
import { useEnsName } from 'wagmi';
import { useFlowingBalance } from '../../hooks/useFlowingBalance';

Expand Down
7 changes: 5 additions & 2 deletions packages/app/src/components/RoundedButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Image, Text, TouchableOpacity, View, StyleSheet } from 'react-native';
import { Image, Text, TouchableOpacity, View, StyleSheet, ActivityIndicator } from 'react-native';
import { InterSemiBold } from '../utils/webFonts';
import { ForwardIcon } from '../assets';
import { Colors } from '../utils/colors';

interface RoundedButtonProps {
title: string;
Expand All @@ -11,6 +12,7 @@ interface RoundedButtonProps {
onPress?: () => void;
maxWidth?: number | string;
disabled?: boolean;
isLoading?: boolean;
}

function RoundedButton({
Expand All @@ -22,6 +24,7 @@ function RoundedButton({
onPress,
maxWidth,
disabled,
isLoading,
}: RoundedButtonProps) {
const dynamicTextStyle = {
color: color,
Expand All @@ -34,7 +37,7 @@ function RoundedButton({
disabled={disabled}
style={[styles.button, { backgroundColor, maxWidth: maxWidth ?? 'auto' }]}
onPress={onPress}>
<Text style={[styles.nonSeeTypeText, dynamicTextStyle]}>{title}</Text>
{isLoading ? (<ActivityIndicator size="large" color={Colors.blue[200]} />) : (<Text style={[styles.nonSeeTypeText, dynamicTextStyle]}>{title}</Text>)}
</TouchableOpacity>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Image, Text, View } from 'react-native';
import { Colors } from '../../utils/colors';
import { ReceiveIcon, SendIcon } from '../../assets';
import Decimal from 'decimal.js';
import { ethers } from 'ethers';
import { Link } from 'native-base';
import { styles } from './styles';
import { formatEther } from 'viem';

interface TransactionListItemProps {
userIdentifier: string;
Expand All @@ -15,7 +14,7 @@ interface TransactionListItemProps {
}

function TransactionListItem({ userIdentifier, isDonation, amount, txHash, rawNetworkFee }: TransactionListItemProps) {
const formattedFee: string = new Decimal(ethers.utils.formatEther(rawNetworkFee ?? 0)).toString();
const formattedFee: string = formatEther(BigInt(rawNetworkFee ?? 0)).toString();
const formattedHash = txHash.slice(0, 40) + '...';

return (
Expand Down
11 changes: 7 additions & 4 deletions packages/app/src/components/modals/ThankYouModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ interface ThankYouModalProps {
openModal: boolean;
address?: `0x${string}`;
collective: IpfsCollective;
isStream: boolean;
}

const ThankYouModal = ({ openModal, address, collective }: ThankYouModalProps) => {
const ThankYouModal = ({ openModal, address, collective, isStream }: ThankYouModalProps) => {
const { navigate } = useCrossNavigate();
const onClick = () => navigate(`/profile/${address}`);

Expand All @@ -21,9 +22,11 @@ const ThankYouModal = ({ openModal, address, collective }: ThankYouModalProps) =
<View style={styles.modalView}>
<Text style={styles.title}>THANK YOU!</Text>
<Text style={styles.paragraph}>{`You have just donated to ${collective.name} GoodCollective!`}</Text>
<Text style={styles.paragraph}>
{`To stop your donation, visit the ${collective.name} GoodCollective page.`}
</Text>
{isStream && (
<Text style={styles.paragraph}>
{`To stop your donation, visit the ${collective.name} GoodCollective page.`}
</Text>
)}
<Image source={ThankYouImg} alt="woman" style={styles.image} />
<TouchableOpacity style={styles.button} onPress={onClick}>
<Text style={styles.buttonText}>GO TO PROFILE</Text>
Expand Down
94 changes: 94 additions & 0 deletions packages/app/src/hooks/apollo/useCreateCoinGeckoApolloClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useEffect, useState } from 'react';
import { RestLink } from 'apollo-link-rest';
import { InvalidationPolicyCache, RenewalPolicy } from '@nerdwallet/apollo-cache-policies';
import { ApolloClient, ApolloError, from, NormalizedCacheObject, TypedDocumentNode } from '@apollo/client';
import { DocumentNode } from 'graphql/language';
import { AsyncStorageWrapper, persistCache } from 'apollo3-cache-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';

import { errorLink, retryLink } from '../../utils/apolloLinkUtils';

// use apolloclient for caching not to hit rate limits
export const useCreateCoinGeckoApolloClient = (): ApolloClient<any> | undefined => {
const [apolloClient, setApolloClient] = useState<ApolloClient<NormalizedCacheObject> | undefined>();

useEffect(() => {
async function initApollo() {
const cache = new InvalidationPolicyCache({
invalidationPolicies: {
timeToLive: 60 * 1000, // 1 minute
renewalPolicy: RenewalPolicy.AccessAndWrite,
types: {
currency: {
timeToLive: 60 * 1000,
},
},
},
});

await persistCache({
cache,
storage: new AsyncStorageWrapper(AsyncStorage),
});

const restLink = new RestLink({
uri: `https://api.coingecko.com/api/v3/simple`,
endpoints: {
byAddress: 'https://api.coingecko.com/api/v3/simple/token_price/celo',
bySymbol: 'https://api.coingecko.com/api/v3/simple/price',
},
responseTransformer: async (response) => {
const results = await response.json();
return Object.entries(results).map(([key, value]) => ({ address: key, ...(value as object) }));
},
});

try {
const client = new ApolloClient({
cache,
link: from([errorLink, retryLink, restLink]),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
query: {
fetchPolicy: 'cache-first',
},
},
});
setApolloClient(client);
} catch (error) {
console.error(error);
} finally {
return;
}
}

initApollo().catch(console.error);
}, []);

return apolloClient;
};

export function useCoinGeckoQuery<TData, TVariables extends Record<string, any> = Record<string, any>>(
query: DocumentNode | TypedDocumentNode<TData, TVariables>,
options?: Record<string, any>
): { data?: any; loading: boolean; error?: ApolloError } {
const client = useCreateCoinGeckoApolloClient();

const [data, setData] = useState<TData>();
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<ApolloError>();

useEffect(() => {
if (client && !options?.disabled) {
client.query({ query, ...options }).then((result) => {
setData(result.data);
setLoading(result.loading);
setError(result.error);
});
}
}, [client, options, query]);

return { data, loading, error };
}
4 changes: 2 additions & 2 deletions packages/app/src/hooks/apollo/useCreateMongoDbApolloClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const useCreateMongoDbApolloClient = (): ApolloClient<any> | undefined =>

const httpLink = new HttpLink({
uri: mongoDbUri,
fetch: async (uri, options) => {
fetch: async (url, options) => {
const accessToken = await getValidAccessToken();
if (!options) {
options = {};
Expand All @@ -55,7 +55,7 @@ export const useCreateMongoDbApolloClient = (): ApolloClient<any> | undefined =>
options.headers = {};
}
(options.headers as Record<string, any>).Authorization = `Bearer ${accessToken}`;
return fetch(uri, options);
return fetch(url as string, options);
},
});

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export * from './useStewardById';
export * from './useContractCalls';
export * from './useGetTokenPrice';
export * from './useIsStewardVerified';
export * from './useEthersSigner';
export * from './useEthers';
Loading

0 comments on commit 83da2df

Please sign in to comment.