diff --git a/.gitignore b/.gitignore index 84d33977..a966e206 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ .DS_Store *.pem .idea +*.client-test.* # debug npm-debug.log* diff --git a/features/assets/components/AssetCard.tsx b/features/assets/components/AssetCard.tsx index 113f2bbe..90fdf805 100644 --- a/features/assets/components/AssetCard.tsx +++ b/features/assets/components/AssetCard.tsx @@ -1,17 +1,18 @@ import { useIBCAssetInfo } from 'hooks/useIBCAssetInfo' import { useTokenDollarValue } from 'hooks/useTokenDollarValue' import { - ArrowUp, + ArrowUpIcon, Button, dollarValueFormatterWithDecimals, - IconWrapper, ImageForTokenLogo, styled, Text, } from 'junoblocks' -import { HTMLProps } from 'react' +import { HTMLProps, useState } from 'react' import { __TRANSFERS_ENABLED__ } from 'util/constants' +import { DepositRedirectDialog } from './DepositRedirectDialog' + export enum AssetCardState { fetching = 'FETCHING', active = 'ACTIVE', @@ -34,15 +35,26 @@ export const AssetCard = ({ state, ...htmlProps }: AssetCardProps) => { - const { symbol, name, logoURI } = useIBCAssetInfo(tokenSymbol) || {} + const { symbol, name, logoURI, external_deposit_uri } = + useIBCAssetInfo(tokenSymbol) || {} + const [showingRedirectDepositDialog, setShowingRedirectDepositDialog] = + useState(false) const [dollarValue] = useTokenDollarValue(tokenSymbol) - const handleDepositClick = () => + const shouldPerformDepositOutsideApp = Boolean(external_deposit_uri) + + const handleDepositClick = () => { + // bail early if redirecting the user to perform deposit externally + if (shouldPerformDepositOutsideApp) { + return setShowingRedirectDepositDialog(true) + } + onActionClick({ tokenSymbol: symbol, actionType: 'deposit', }) + } const handleWithdrawClick = () => onActionClick({ @@ -66,51 +78,77 @@ export const AssetCard = ({ const rendersActiveAppearance = balance > 0 return ( - - - - -
- - {rendersActiveAppearance ? balance : null} {name} - - {rendersActiveAppearance && ( - - $ - {dollarValueFormatterWithDecimals(dollarValue * balance, { - includeCommaSeparation: true, - })} + <> + + + + +
+ + {rendersActiveAppearance ? balance : null} {name} - )} -
-
-
+ {rendersActiveAppearance && ( + + $ + {dollarValueFormatterWithDecimals(dollarValue * balance, { + includeCommaSeparation: true, + })} + + )} +
+
+
- - {balance > 0 && ( - - )} - + + {shouldPerformDepositOutsideApp ? ( + + ) : ( + <> + {balance > 0 && ( + + )} + + + )} + -
+ + {shouldPerformDepositOutsideApp && ( + setShowingRedirectDepositDialog(false)} + tokenSymbol={tokenSymbol} + href={external_deposit_uri} + /> + )} + ) } diff --git a/features/assets/components/DepositRedirectDialog.tsx b/features/assets/components/DepositRedirectDialog.tsx new file mode 100644 index 00000000..9d315302 --- /dev/null +++ b/features/assets/components/DepositRedirectDialog.tsx @@ -0,0 +1,39 @@ +import { Button, Dialog, DialogContent, DialogHeader, Text } from 'junoblocks' + +type DepositRedirectDialogProps = { + isShowing: boolean + onRequestClose: () => void + tokenSymbol: string + href: string +} + +export const DepositRedirectDialog = ({ + isShowing, + onRequestClose, + tokenSymbol, + href, +}: DepositRedirectDialogProps) => { + return ( + + + External asset deposit + + + + You will be redirected to an external service to deposit your{' '} + {tokenSymbol} on the chain. + + + + + ) +} diff --git a/hooks/useIbcAssetList.ts b/hooks/useIbcAssetList.ts index 60fe08f2..5c559f45 100644 --- a/hooks/useIbcAssetList.ts +++ b/hooks/useIbcAssetList.ts @@ -14,6 +14,7 @@ export type IBCAssetInfo = { channel: string logoURI: string deposit_gas_fee?: number + external_deposit_uri?: string } export type IBCAssetList = { diff --git a/public/ibc_assets.json b/public/ibc_assets.json index fc7fdbb1..738307e0 100644 --- a/public/ibc_assets.json +++ b/public/ibc_assets.json @@ -24,7 +24,8 @@ "channel": "info", "juno_channel": "channel-7", "juno_denom": "ibc/test", - "logoURI": "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg?v=014" + "logoURI": "https://cryptologos.cc/logos/usd-coin-usdc-logo.svg?v=014", + "external_deposit_uri": "http://localhost:3000" } ] } diff --git a/queries/tokenDollarValueQuery.ts b/queries/tokenDollarValueQuery.ts deleted file mode 100644 index b655d996..00000000 --- a/queries/tokenDollarValueQuery.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { TokenInfo } from '../hooks/useTokenList' - -export async function tokenDollarValueQuery(tokenIds: Array) { - const prices = await fetchTokensPrice(tokenIds) - return tokenIds.map((id): number => prices[id]?.usd || 0) -} - -async function fetchTokensPrice(tokenIds: Array) { - const response = await fetch( - `https://api.coingecko.com/api/v3/simple/price?ids=${tokenIds.join( - ',' - )}&vs_currencies=usd`, - { - method: 'GET', - } - ) - - if (!response.ok) { - throw new Error('Cannot fetch dollar price from the API.') - } - - return response.json() -} diff --git a/queries/tokenDollarValueQuery.tsx b/queries/tokenDollarValueQuery.tsx new file mode 100644 index 00000000..983deee8 --- /dev/null +++ b/queries/tokenDollarValueQuery.tsx @@ -0,0 +1,56 @@ +import { ErrorIcon, Toast } from 'junoblocks' +import React from 'react' +import { toast } from 'react-hot-toast' + +import { TokenInfo } from '../hooks/useTokenList' + +const pricingServiceIsDownAlert = createAlertPricingServiceIsDown() + +export async function tokenDollarValueQuery(tokenIds: Array) { + const prices = await fetchTokensPrice(tokenIds) + return tokenIds.map((id): number => prices[id]?.usd || 0) +} + +async function fetchTokensPrice(tokenIds: Array) { + const response = await fetch( + `https://api.coingecko.com/api/v3/simple/price?ids=${tokenIds.join( + ',' + )}&vs_currencies=usd`, + { + method: 'GET', + } + ) + + if (!response.ok) { + pricingServiceIsDownAlert() + throw new Error('Cannot fetch dollar price from the API.') + } + + return response.json() +} + +function createAlertPricingServiceIsDown() { + let hasRenderedAlert + let timeout + + function renderAlert() { + toast.custom((t) => ( + } + title="Oops, sorry! Our pricing service is temporarily down" + onClose={() => toast.dismiss(t.id)} + /> + )) + } + + return () => { + if (hasRenderedAlert) { + clearTimeout(timeout) + timeout = setTimeout(renderAlert, 60 * 1000) + return + } + + hasRenderedAlert = true + renderAlert() + } +} diff --git a/util/externalLinkPopup.ts b/util/externalLinkPopup.ts new file mode 100644 index 00000000..5830cd2d --- /dev/null +++ b/util/externalLinkPopup.ts @@ -0,0 +1,62 @@ +type ExternalLinkPopupArgs = { + url: string + title: string + width: number + height: number +} + +export function externalLinkPopup({ + url, + title, + width: w, + height: h, +}: ExternalLinkPopupArgs) { + const dualScreenLeft = + window.screenLeft !== undefined ? window.screenLeft : window.screenX + const dualScreenTop = + window.screenTop !== undefined ? window.screenTop : window.screenY + + const width = window.innerWidth + ? window.innerWidth + : document.documentElement.clientWidth + ? document.documentElement.clientWidth + : screen.width + const height = window.innerHeight + ? window.innerHeight + : document.documentElement.clientHeight + ? document.documentElement.clientHeight + : screen.height + + const systemZoom = width / window.screen.availWidth + const left = (width - w) / 2 / systemZoom + dualScreenLeft + const top = (height - h) / 2 / systemZoom + dualScreenTop + const newWindow = window.open( + url, + title, + ` + scrollbars=yes, + width=${w / systemZoom}, + height=${h / systemZoom}, + top=${top}, + left=${left}, + location=yes, + status=yes + ` + ) + + if (window.focus) newWindow.focus() + + return newWindow +} + +export function externalLinkPopupAutoWidth( + args: Omit +) { + const width = Math.max(450, Math.round(window.innerWidth * 0.35)) + const height = Math.max(300, Math.round(window.innerHeight * 0.8)) + return externalLinkPopup({ + ...args, + width, + height, + }) +}