From df77eb9ebb327ddcb9351ced7d8074e47f590e1a Mon Sep 17 00:00:00 2001 From: Edgar Khanzadian Date: Wed, 13 Dec 2023 13:34:43 +0400 Subject: [PATCH] feat: add fund btc screen --- ...llipses.png => receive-funds-ellipses.png} | Bin .../choose-asset-container.tsx | 25 ++++++ .../choose-crypto-asset.layout.tsx | 8 +- .../choose-asset-to-fund.tsx | 81 ++++++++++++++++++ .../fund/components/fiat-providers.utils.ts | 26 ++++-- .../fund/components/fund-account-tile.tsx | 8 +- .../fund/{ => components}/fund.layout.tsx | 32 +++++-- .../pages/fund/components/icon-components.tsx | 30 +++++++ .../fund/components/receive-funds-item.tsx | 40 +++++++++ .../fund/components/receive-stx-item.tsx | 33 ------- .../{components => }/fiat-providers-list.tsx | 28 ++++-- src/app/pages/fund/fund.tsx | 47 +++++++--- .../pages/home/components/account-actions.tsx | 2 +- .../choose-crypto-asset.tsx | 21 +---- .../swap/hooks/use-alex-sdk-fiat-price.tsx | 2 + src/app/routes/app-routes.tsx | 21 +++-- src/app/store/accounts/blockchain/utils.ts | 22 +++++ src/shared/models/currencies.model.ts | 7 +- src/shared/route-urls.ts | 3 +- tests/fixtures/fixtures.ts | 5 ++ .../fund-choose-currency.page.ts | 31 +++++++ tests/page-object-models/home.page.ts | 2 +- tests/specs/fund/fund.spec.ts | 31 ++++++- 23 files changed, 395 insertions(+), 110 deletions(-) rename public/assets/images/fund/{receive-stx-ellipses.png => receive-funds-ellipses.png} (100%) create mode 100644 src/app/components/crypto-assets/choose-crypto-asset/choose-asset-container.tsx create mode 100644 src/app/pages/fund/choose-asset-to-fund/choose-asset-to-fund.tsx rename src/app/pages/fund/{ => components}/fund.layout.tsx (52%) create mode 100644 src/app/pages/fund/components/icon-components.tsx create mode 100644 src/app/pages/fund/components/receive-funds-item.tsx delete mode 100644 src/app/pages/fund/components/receive-stx-item.tsx rename src/app/pages/fund/{components => }/fiat-providers-list.tsx (76%) create mode 100644 src/app/store/accounts/blockchain/utils.ts create mode 100644 tests/page-object-models/fund-choose-currency.page.ts diff --git a/public/assets/images/fund/receive-stx-ellipses.png b/public/assets/images/fund/receive-funds-ellipses.png similarity index 100% rename from public/assets/images/fund/receive-stx-ellipses.png rename to public/assets/images/fund/receive-funds-ellipses.png diff --git a/src/app/components/crypto-assets/choose-crypto-asset/choose-asset-container.tsx b/src/app/components/crypto-assets/choose-crypto-asset/choose-asset-container.tsx new file mode 100644 index 00000000000..920d00f30ce --- /dev/null +++ b/src/app/components/crypto-assets/choose-crypto-asset/choose-asset-container.tsx @@ -0,0 +1,25 @@ +import { Flex } from 'leather-styles/jsx'; + +import { HasChildren } from '@app/common/has-children'; +import { whenPageMode } from '@app/common/utils'; + +export function ChooseAssetContainer({ children }: HasChildren) { + return whenPageMode({ + full: ( + + {children} + + ), + popup: ( + + {children} + + ), + }); +} diff --git a/src/app/components/crypto-assets/choose-crypto-asset/choose-crypto-asset.layout.tsx b/src/app/components/crypto-assets/choose-crypto-asset/choose-crypto-asset.layout.tsx index 7ef819b9108..137ce63fe77 100644 --- a/src/app/components/crypto-assets/choose-crypto-asset/choose-crypto-asset.layout.tsx +++ b/src/app/components/crypto-assets/choose-crypto-asset/choose-crypto-asset.layout.tsx @@ -1,6 +1,6 @@ import { Box, Flex, StackProps, styled } from 'leather-styles/jsx'; -export function ChooseCryptoAssetLayout({ children }: StackProps) { +export function ChooseCryptoAssetLayout({ children, title }: StackProps & { title: string }) { return ( - - CHOOSE ASSET -
- TO SEND + + {title}
{children} diff --git a/src/app/pages/fund/choose-asset-to-fund/choose-asset-to-fund.tsx b/src/app/pages/fund/choose-asset-to-fund/choose-asset-to-fund.tsx new file mode 100644 index 00000000000..c7462a2b183 --- /dev/null +++ b/src/app/pages/fund/choose-asset-to-fund/choose-asset-to-fund.tsx @@ -0,0 +1,81 @@ +import { useCallback, useMemo } from 'react'; +import { Outlet, useNavigate } from 'react-router-dom'; + +import { AllTransferableCryptoAssetBalances } from '@shared/models/crypto-asset-balance.model'; +import { RouteUrls } from '@shared/route-urls'; + +import { useStxBalance } from '@app/common/hooks/balance/stx/use-stx-balance'; +import { useRouteHeader } from '@app/common/hooks/use-route-header'; +import { useWalletType } from '@app/common/use-wallet-type'; +import { ChooseAssetContainer } from '@app/components/crypto-assets/choose-crypto-asset/choose-asset-container'; +import { ChooseCryptoAssetLayout } from '@app/components/crypto-assets/choose-crypto-asset/choose-crypto-asset.layout'; +import { CryptoAssetList } from '@app/components/crypto-assets/choose-crypto-asset/crypto-asset-list'; +import { ModalHeader } from '@app/components/modal-header'; +import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/btc-native-segwit-balance.hooks'; +import { createStacksCryptoCurrencyAssetTypeWrapper } from '@app/query/stacks/balance/stacks-ft-balances.utils'; +import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; +import { useCheckLedgerBlockchainAvailable } from '@app/store/accounts/blockchain/utils'; + +function useBtcCryptoCurrencyAssetBalance() { + const currentBtcSigner = useCurrentAccountNativeSegwitSigner(); + if (!currentBtcSigner?.(0).address) throw new Error('No bitcoin address'); + + return useNativeSegwitBalance(currentBtcSigner?.(0).address); +} + +function useStxCryptoCurrencyAssetBalance() { + const { availableBalance: availableStxBalance } = useStxBalance(); + return createStacksCryptoCurrencyAssetTypeWrapper(availableStxBalance.amount); +} + +export function ChooseCryptoAssetToFund() { + const btcCryptoCurrencyAssetBalance = useBtcCryptoCurrencyAssetBalance(); + const stxCryptoCurrencyAssetBalance = useStxCryptoCurrencyAssetBalance(); + + const cryptoCurrencyAssetBalances = useMemo( + () => [btcCryptoCurrencyAssetBalance, stxCryptoCurrencyAssetBalance], + [btcCryptoCurrencyAssetBalance, stxCryptoCurrencyAssetBalance] + ); + + const { whenWallet } = useWalletType(); + const navigate = useNavigate(); + + const checkBlockchainAvailable = useCheckLedgerBlockchainAvailable(); + + const filteredCryptoAssetBalances = useMemo( + () => + cryptoCurrencyAssetBalances.filter(assetBalance => + whenWallet({ + ledger: checkBlockchainAvailable(assetBalance.blockchain), + software: true, + }) + ), + [cryptoCurrencyAssetBalances, checkBlockchainAvailable, whenWallet] + ); + + useRouteHeader( navigate(RouteUrls.Home)} title=" " />); + + const navigateToSendForm = useCallback( + (cryptoAssetBalance: AllTransferableCryptoAssetBalances) => { + const { asset } = cryptoAssetBalance; + + const symbol = asset.symbol === '' ? asset.contractAssetName : asset.symbol; + navigate(RouteUrls.Fund.replace(':currency', symbol.toUpperCase())); + }, + [navigate] + ); + + return ( + <> + + + + + + + + ); +} diff --git a/src/app/pages/fund/components/fiat-providers.utils.ts b/src/app/pages/fund/components/fiat-providers.utils.ts index f317c95a93b..c0800ee36a2 100644 --- a/src/app/pages/fund/components/fiat-providers.utils.ts +++ b/src/app/pages/fund/components/fiat-providers.utils.ts @@ -11,6 +11,7 @@ import TransakIcon from '@assets/images/fund/fiat-providers/transak-icon.png'; import { generateOnRampURL } from '@coinbase/cbpay-js'; import { COINBASE_APP_ID, MOONPAY_API_KEY, TRANSAK_API_KEY } from '@shared/environment'; +import { CryptoCurrencies } from '@shared/models/currencies.model'; import { ActiveFiatProvider } from '@app/query/common/remote-config/remote-config.query'; @@ -41,32 +42,37 @@ export const activeFiatProviderIcons: Record [ActiveFiatProviders.Transak]: TransakIcon, }; -function makeCoinbaseUrl(address: string) { +function makeCoinbaseUrl(address: string, symbol: CryptoCurrencies) { + const code = symbol.toUpperCase(); + const onRampURL = generateOnRampURL({ appId: COINBASE_APP_ID, destinationWallets: [ { address, - assets: ['STX'], + assets: [code], }, ], }); return onRampURL; } -function makeMoonPayUrl(address: string) { - return `https://buy.moonpay.com?apiKey=${MOONPAY_API_KEY}¤cyCode=stx&walletAddress=${address}`; +function makeMoonPayUrl(address: string, symbol: CryptoCurrencies) { + const code = symbol.toLowerCase(); + return `https://buy.moonpay.com?apiKey=${MOONPAY_API_KEY}¤cyCode=${code}&walletAddress=${address}`; } -function makeTransakUrl(address: string) { +function makeTransakUrl(address: string, symbol: CryptoCurrencies) { const screenTitle = 'Buy Stacks'; + const code = symbol.toUpperCase(); - return `https://global.transak.com?apiKey=${TRANSAK_API_KEY}&cryptoCurrencyCode=STX&exchangeScreenTitle=${encodeURI( + return `https://global.transak.com?apiKey=${TRANSAK_API_KEY}&cryptoCurrencyCode=${code}&exchangeScreenTitle=${encodeURI( screenTitle )}&defaultPaymentMethod=credit_debit_card&walletAddress=${address}`; } function makeFiatProviderFaqUrl(address: string, provider: string) { + // TODO: Add FAQ for BTC return `https://hiro.so/wallet-faq/how-do-i-buy-stx-from-an-exchange?provider=${provider}&address=${address}`; } @@ -75,23 +81,25 @@ interface GetProviderNameArgs { hasFastCheckoutProcess: boolean; key: string; name: string; + symbol: CryptoCurrencies; } export function getProviderUrl({ address, hasFastCheckoutProcess, key, name, + symbol, }: GetProviderNameArgs) { if (!hasFastCheckoutProcess) { return makeFiatProviderFaqUrl(address, name); } switch (key) { case ActiveFiatProviders.Coinbase: - return makeCoinbaseUrl(address); + return makeCoinbaseUrl(address, symbol); case ActiveFiatProviders.MoonPay: - return makeMoonPayUrl(address); + return makeMoonPayUrl(address, symbol); case ActiveFiatProviders.Transak: - return makeTransakUrl(address); + return makeTransakUrl(address, symbol); default: return makeFiatProviderFaqUrl(address, name); } diff --git a/src/app/pages/fund/components/fund-account-tile.tsx b/src/app/pages/fund/components/fund-account-tile.tsx index 7c8d7aafe26..f0874d3c13a 100644 --- a/src/app/pages/fund/components/fund-account-tile.tsx +++ b/src/app/pages/fund/components/fund-account-tile.tsx @@ -6,12 +6,12 @@ interface FundAccountTileProps { description: string; icon: string; onClickTile(): void; - receiveStxIcon?: React.JSX.Element; + ReceiveStxIcon?(): React.JSX.Element; testId: string; title?: string; } export function FundAccountTile(props: FundAccountTileProps) { - const { attributes, description, icon, onClickTile, receiveStxIcon, testId, title } = props; + const { attributes, description, icon, onClickTile, ReceiveStxIcon, testId, title } = props; return ( - - {receiveStxIcon} + + {ReceiveStxIcon ? : null} = { + BTC: { + name: 'Bitcoin', + symbol: 'BTC', + }, + STX: { + name: 'Stacks', + symbol: 'STX', + }, +}; + +interface FundLayoutProps extends HasChildren { + symbol: CryptoCurrencies; } -export function FundLayout({ address }: FundLayoutProps) { + +export function FundLayout({ symbol, children }: FundLayoutProps) { + const name = nameMap[symbol].name; + const nameAbbr = nameMap[symbol].symbol; return ( - Choose an exchange to fund your account with Stacks (STX) or deposit from elsewhere. - Exchanges with “Fast checkout” make it easier to purchase STX for direct deposit into your - wallet with a credit card. + Choose an exchange to fund your account with {name} ({nameAbbr}) or deposit from + elsewhere. Exchanges with “Fast checkout” make it easier to purchase {nameAbbr} for direct + deposit into your wallet with a credit card. - + {children}
); } diff --git a/src/app/pages/fund/components/icon-components.tsx b/src/app/pages/fund/components/icon-components.tsx new file mode 100644 index 00000000000..298dab2505c --- /dev/null +++ b/src/app/pages/fund/components/icon-components.tsx @@ -0,0 +1,30 @@ +import BitcoinIcon from '@assets/images/btc-icon.png'; +import ReceiveFundsEllipses from '@assets/images/fund/receive-funds-ellipses.png'; +import StacksIcon from '@assets/images/fund/stacks-icon.png'; +import { Box } from 'leather-styles/jsx'; + +export function StacksIconComponent() { + return ( + <> + + + + + + + + ); +} + +export function BitcoinIconComponent() { + return ( + <> + + + + + + + + ); +} diff --git a/src/app/pages/fund/components/receive-funds-item.tsx b/src/app/pages/fund/components/receive-funds-item.tsx new file mode 100644 index 00000000000..3a5854963b8 --- /dev/null +++ b/src/app/pages/fund/components/receive-funds-item.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import QRCodeIcon from '@assets/images/fund/qr-code-icon.png'; +import { FundPageSelectors } from '@tests/selectors/fund.selectors'; + +import { CryptoCurrencies } from '@shared/models/currencies.model'; + +import { FundAccountTile } from './fund-account-tile'; +import { BitcoinIconComponent, StacksIconComponent } from './icon-components'; + +interface CryptoDescription { + title: string; + IconComponent(): React.JSX.Element; +} + +const cryptoDescriptions: Record = { + STX: { + title: 'Receive STX from a friend or deposit from a separate wallet', + IconComponent: StacksIconComponent, + }, + BTC: { + title: 'Receive BTC from a friend or deposit from a separate wallet', + IconComponent: BitcoinIconComponent, + }, +}; +interface ReceiveStxItemProps { + onReceive(): void; + symbol: CryptoCurrencies; +} +export function ReceiveFundsItem({ onReceive, symbol }: ReceiveStxItemProps) { + return ( + + ); +} diff --git a/src/app/pages/fund/components/receive-stx-item.tsx b/src/app/pages/fund/components/receive-stx-item.tsx deleted file mode 100644 index 2b77bb58e3a..00000000000 --- a/src/app/pages/fund/components/receive-stx-item.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import QRCodeIcon from '@assets/images/fund/qr-code-icon.png'; -import ReceiveStxEllipses from '@assets/images/fund/receive-stx-ellipses.png'; -import StacksIcon from '@assets/images/fund/stacks-icon.png'; -import { FundPageSelectors } from '@tests/selectors/fund.selectors'; -import { Box } from 'leather-styles/jsx'; - -import { FundAccountTile } from './fund-account-tile'; - -const description = 'Receive STX from a friend or deposit from a separate wallet'; -const StxIconWithEllipses = ( - <> - - - - - - - -); -interface ReceiveStxItemProps { - onReceiveStx(): void; -} -export function ReceiveStxItem({ onReceiveStx }: ReceiveStxItemProps) { - return ( - - ); -} diff --git a/src/app/pages/fund/components/fiat-providers-list.tsx b/src/app/pages/fund/fiat-providers-list.tsx similarity index 76% rename from src/app/pages/fund/components/fiat-providers-list.tsx rename to src/app/pages/fund/fiat-providers-list.tsx index 497e8752b55..67c7680a46e 100644 --- a/src/app/pages/fund/components/fiat-providers-list.tsx +++ b/src/app/pages/fund/fiat-providers-list.tsx @@ -1,7 +1,9 @@ +import { useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { Grid } from 'leather-styles/jsx'; +import { CryptoCurrencies } from '@shared/models/currencies.model'; import { RouteUrls } from '@shared/route-urls'; import { useAnalytics } from '@app/common/hooks/analytics/use-analytics'; @@ -12,21 +14,31 @@ import { useHasFiatProviders, } from '@app/query/common/remote-config/remote-config.query'; -import { FiatProviderItem } from './fiat-provider-item'; -import { activeFiatProviderIcons, getProviderUrl } from './fiat-providers.utils'; -import { ReceiveStxItem } from './receive-stx-item'; +import { FiatProviderItem } from './components/fiat-provider-item'; +import { activeFiatProviderIcons, getProviderUrl } from './components/fiat-providers.utils'; +import { ReceiveFundsItem } from './components/receive-funds-item'; interface FiatProvidersProps { address: string; + symbol: CryptoCurrencies; } export function FiatProvidersList(props: FiatProvidersProps) { - const { address } = props; + const { address, symbol } = props; const navigate = useNavigate(); const activeProviders = useActiveFiatProviders(); const hasProviders = useHasFiatProviders(); const analytics = useAnalytics(); const location = useLocation(); + const routeToQr = useMemo(() => { + switch (symbol) { + case 'BTC': + return RouteUrls.ReceiveBtc; + case 'STX': + return RouteUrls.ReceiveStx; + } + }, [symbol]); + const goToProviderExternalWebsite = (provider: string, providerUrl: string) => { void analytics.track('select_buy_option', { provider }); openInNewTab(providerUrl); @@ -53,9 +65,10 @@ export function FiatProvidersList(props: FiatProvidersProps) { width="100%" maxWidth={['100%', '80rem']} > - - navigate(`${RouteUrls.ReceiveStx}`, { + + navigate(routeToQr, { state: { backgroundLocation: location }, }) } @@ -66,6 +79,7 @@ export function FiatProvidersList(props: FiatProvidersProps) { hasFastCheckoutProcess: providerValue.hasFastCheckoutProcess, key: providerKey, name: providerValue.name, + symbol, }); return ( diff --git a/src/app/pages/fund/fund.tsx b/src/app/pages/fund/fund.tsx index 066cd80ca6e..1756f487c8a 100644 --- a/src/app/pages/fund/fund.tsx +++ b/src/app/pages/fund/fund.tsx @@ -1,33 +1,52 @@ -import { useNavigate } from 'react-router-dom'; +import { Outlet, useNavigate, useParams } from 'react-router-dom'; +import { isCryptoCurrency } from '@shared/models/currencies.model'; import { RouteUrls } from '@shared/route-urls'; import { useRouteHeader } from '@app/common/hooks/use-route-header'; import { Header } from '@app/components/header'; import { FullPageLoadingSpinner } from '@app/components/loading-spinner'; import { useCurrentStacksAccountAnchoredBalances } from '@app/query/stacks/balance/stx-balance.hooks'; -import { ModalBackgroundWrapper } from '@app/routes/components/modal-background-wrapper'; -import { useBackgroundLocationRedirect } from '@app/routes/hooks/use-background-location-redirect'; +import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; import { useCurrentStacksAccount } from '@app/store/accounts/blockchain/stacks/stacks-account.hooks'; -import { FundLayout } from './fund.layout'; +import { FundLayout } from './components/fund.layout'; +import { FiatProvidersList } from './fiat-providers-list'; -interface FundPageProps { - children: React.ReactNode; -} -export function FundPage({ children }: FundPageProps) { - useBackgroundLocationRedirect(RouteUrls.Fund); +export function FundPage() { const navigate = useNavigate(); - const currentAccount = useCurrentStacksAccount(); + const currentStxAccount = useCurrentStacksAccount(); + const bitcoinSigner = useCurrentAccountNativeSegwitIndexZeroSigner(); const { data: balances } = useCurrentStacksAccountAnchoredBalances(); + const { currency } = useParams(); + + function getSymbol() { + if (isCryptoCurrency(currency)) { + return currency; + } + return 'STX'; + } + function getAddress() { + switch (symbol) { + case 'BTC': + return bitcoinSigner.address; + case 'STX': + return currentStxAccount?.address; + } + } + + const symbol = getSymbol(); + const address = getAddress(); - useRouteHeader(
navigate(RouteUrls.Home)} title=" " />); + useRouteHeader(
navigate(RouteUrls.FundChooseCurrency)} title=" " />); - if (!currentAccount || !balances) return ; + if (!address || !balances) return ; return ( <> - - {children} + + + + ); } diff --git a/src/app/pages/home/components/account-actions.tsx b/src/app/pages/home/components/account-actions.tsx index 84608e7746e..6d3ac41225c 100644 --- a/src/app/pages/home/components/account-actions.tsx +++ b/src/app/pages/home/components/account-actions.tsx @@ -39,7 +39,7 @@ export function AccountActions(props: FlexProps) { data-testid={HomePageSelectors.FundAccountBtn} icon={} label="Buy" - onClick={() => navigate(RouteUrls.Fund)} + onClick={() => navigate(RouteUrls.FundChooseCurrency)} /> )} diff --git a/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx b/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx index 2ec651e6836..7167b26ea96 100644 --- a/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx +++ b/src/app/pages/send/choose-crypto-asset/choose-crypto-asset.tsx @@ -1,4 +1,3 @@ -import { useCallback } from 'react'; import toast from 'react-hot-toast'; import { useNavigate } from 'react-router-dom'; @@ -14,30 +13,16 @@ import { ChooseCryptoAssetLayout } from '@app/components/crypto-assets/choose-cr import { CryptoAssetList } from '@app/components/crypto-assets/choose-crypto-asset/crypto-asset-list'; import { ModalHeader } from '@app/components/modal-header'; import { useConfigBitcoinSendEnabled } from '@app/query/common/remote-config/remote-config.query'; -import { useHasCurrentBitcoinAccount } from '@app/store/accounts/blockchain/bitcoin/bitcoin.hooks'; -import { useHasStacksLedgerKeychain } from '@app/store/accounts/blockchain/stacks/stacks.hooks'; +import { useCheckLedgerBlockchainAvailable } from '@app/store/accounts/blockchain/utils'; export function ChooseCryptoAsset() { const allTransferableCryptoAssetBalances = useAllTransferableCryptoAssetBalances(); const { whenWallet } = useWalletType(); - const hasBitcoinLedgerKeys = useHasCurrentBitcoinAccount(); - const hasStacksLedgerKeys = useHasStacksLedgerKeychain(); const navigate = useNavigate(); const isBitcoinSendEnabled = useConfigBitcoinSendEnabled(); - const checkBlockchainAvailable = useCallback( - (symbol: string) => { - if (symbol === 'bitcoin') { - return hasBitcoinLedgerKeys; - } - if (symbol === 'stacks') { - return hasStacksLedgerKeys; - } - return false; - }, - [hasBitcoinLedgerKeys, hasStacksLedgerKeys] - ); + const checkBlockchainAvailable = useCheckLedgerBlockchainAvailable(); useRouteHeader(); @@ -61,7 +46,7 @@ export function ChooseCryptoAsset() { } return ( - + navigateToSendForm(cryptoAssetBalance)} cryptoAssetBalances={allTransferableCryptoAssetBalances.filter(asset => diff --git a/src/app/pages/swap/hooks/use-alex-sdk-fiat-price.tsx b/src/app/pages/swap/hooks/use-alex-sdk-fiat-price.tsx index 8869d579507..067538e9df7 100644 --- a/src/app/pages/swap/hooks/use-alex-sdk-fiat-price.tsx +++ b/src/app/pages/swap/hooks/use-alex-sdk-fiat-price.tsx @@ -7,6 +7,7 @@ import { unitToFractionalUnit } from '@app/common/money/unit-conversion'; export function useAlexSdkAmountAsFiat(balance?: Money, price?: Money, value?: string) { const convertAlexSdkCurrencyToUsd = useConvertAlexSdkCurrencyToFiatAmount( + // @ts-expect-error TODO: balance?.symbol should be of a Cryptocurrency type. balance?.symbol ?? '', price ?? createMoney(0, 'USD') ); @@ -22,6 +23,7 @@ export function useAlexSdkAmountAsFiat(balance?: Money, price?: Money, value?: s export function useAlexSdkBalanceAsFiat(balance?: Money, price?: Money) { const convertAlexSdkCurrencyToUsd = useConvertAlexSdkCurrencyToFiatAmount( + // @ts-expect-error TODO: balance?.symbol should be of a Cryptocurrency type. balance?.symbol ?? '', price ?? createMoney(0, 'USD') ); diff --git a/src/app/routes/app-routes.tsx b/src/app/routes/app-routes.tsx index 7ec17bcc076..abefedcce67 100644 --- a/src/app/routes/app-routes.tsx +++ b/src/app/routes/app-routes.tsx @@ -28,12 +28,14 @@ import { RetrieveTaprootToNativeSegwit } from '@app/features/retrieve-taproot-to import { BitcoinContractList } from '@app/pages/bitcoin-contract-list/bitcoin-contract-list'; import { BitcoinContractRequest } from '@app/pages/bitcoin-contract-request/bitcoin-contract-request'; import { ChooseAccount } from '@app/pages/choose-account/choose-account'; +import { ChooseCryptoAssetToFund } from '@app/pages/fund/choose-asset-to-fund/choose-asset-to-fund'; import { FundPage } from '@app/pages/fund/fund'; import { Home } from '@app/pages/home/home'; import { AllowDiagnosticsModal } from '@app/pages/onboarding/allow-diagnostics/allow-diagnostics'; import { BackUpSecretKeyPage } from '@app/pages/onboarding/back-up-secret-key/back-up-secret-key'; import { SignIn } from '@app/pages/onboarding/sign-in/sign-in'; import { WelcomePage } from '@app/pages/onboarding/welcome/welcome'; +import { ReceiveBtcModal } from '@app/pages/receive/receive-btc'; import { ReceiveStxModal } from '@app/pages/receive/receive-stx'; import { RequestError } from '@app/pages/request-error/request-error'; import { RpcSignStacksTransaction } from '@app/pages/rpc-sign-stacks-transaction/rpc-sign-stacks-transaction'; @@ -174,19 +176,28 @@ function useAppRoutes() { - - } /> - {settingsRoutes} - + } > {settingsRoutes} } /> + } /> + + + + + } + > + {settingsRoutes} + } /> {sendCryptoAssetFormRoutes} diff --git a/src/app/store/accounts/blockchain/utils.ts b/src/app/store/accounts/blockchain/utils.ts new file mode 100644 index 00000000000..01b497cbab3 --- /dev/null +++ b/src/app/store/accounts/blockchain/utils.ts @@ -0,0 +1,22 @@ +import { useCallback } from 'react'; + +import { useHasCurrentBitcoinAccount } from './bitcoin/bitcoin.hooks'; +import { useHasStacksLedgerKeychain } from './stacks/stacks.hooks'; + +export function useCheckLedgerBlockchainAvailable() { + const hasBitcoinLedgerKeys = useHasCurrentBitcoinAccount(); + const hasStacksLedgerKeys = useHasStacksLedgerKeychain(); + + return useCallback( + (symbol: string) => { + if (symbol === 'bitcoin') { + return hasBitcoinLedgerKeys; + } + if (symbol === 'stacks') { + return hasStacksLedgerKeys; + } + return false; + }, + [hasBitcoinLedgerKeys, hasStacksLedgerKeys] + ); +} diff --git a/src/shared/models/currencies.model.ts b/src/shared/models/currencies.model.ts index c77df5872ed..115968881a5 100644 --- a/src/shared/models/currencies.model.ts +++ b/src/shared/models/currencies.model.ts @@ -1,6 +1,9 @@ -import { LiteralUnion } from '@shared/utils/type-utils'; +const CRYPTO_CURRENCIES_ARRAY = ['BTC', 'STX'] as const; -export type CryptoCurrencies = LiteralUnion<'BTC' | 'STX', string>; +export type CryptoCurrencies = (typeof CRYPTO_CURRENCIES_ARRAY)[number]; + +export const isCryptoCurrency = (value: unknown): value is CryptoCurrencies => + CRYPTO_CURRENCIES_ARRAY.some(cryptocurrency => cryptocurrency === value); export type FiatCurrencies = 'USD' | string; diff --git a/src/shared/route-urls.ts b/src/shared/route-urls.ts index b8cd76a91ef..2ca8451ca83 100644 --- a/src/shared/route-urls.ts +++ b/src/shared/route-urls.ts @@ -26,7 +26,8 @@ export enum RouteUrls { Home = '/', AddNetwork = '/add-network', ChooseAccount = '/choose-account', - Fund = '/fund', + Fund = '/fund/:currency', + FundChooseCurrency = '/fund-choose-currency', IncreaseStxFee = '/increase-fee/stx', IncreaseBtcFee = '/increase-fee/btc', IncreaseFeeSent = '/increase-fee/sent', diff --git a/tests/fixtures/fixtures.ts b/tests/fixtures/fixtures.ts index 078ee260523..ebd09495608 100644 --- a/tests/fixtures/fixtures.ts +++ b/tests/fixtures/fixtures.ts @@ -1,4 +1,5 @@ import { BrowserContext, test as base, chromium } from '@playwright/test'; +import { FundChooseCurrencyPage } from '@tests/page-object-models/fund-choose-currency.page'; import { GlobalPage } from '@tests/page-object-models/global.page'; import { HomePage } from '@tests/page-object-models/home.page'; import { NetworkPage } from '@tests/page-object-models/network.page'; @@ -16,6 +17,7 @@ interface TestFixtures { sendPage: SendPage; swapPage: SwapPage; networkPage: NetworkPage; + fundChooseCurrencyPage: FundChooseCurrencyPage; } /** @@ -64,4 +66,7 @@ export const test = base.extend({ networkPage: async ({ page }, use) => { await use(new NetworkPage(page)); }, + fundChooseCurrencyPage: async ({ page }, use) => { + await use(new FundChooseCurrencyPage(page)); + }, }); diff --git a/tests/page-object-models/fund-choose-currency.page.ts b/tests/page-object-models/fund-choose-currency.page.ts new file mode 100644 index 00000000000..61f66b5c526 --- /dev/null +++ b/tests/page-object-models/fund-choose-currency.page.ts @@ -0,0 +1,31 @@ +import { Locator, Page } from '@playwright/test'; +import { CryptoAssetSelectors } from '@tests/selectors/crypto-asset.selectors'; + +import { RouteUrls } from '@shared/route-urls'; + +export class FundChooseCurrencyPage { + readonly page: Page; + readonly stxButton: Locator; + readonly btcButton: Locator; + + constructor(page: Page) { + this.page = page; + this.stxButton = page.getByTestId( + CryptoAssetSelectors.CryptoAssetListItem.replace('{symbol}', 'stx') + ); + this.btcButton = page.getByTestId( + CryptoAssetSelectors.CryptoAssetListItem.replace('{symbol}', 'btc') + ); + } + + async goToFundBtcPage() { + await this.page.waitForURL('**' + RouteUrls.FundChooseCurrency); + await this.btcButton.click(); + await this.page.waitForURL('**' + RouteUrls.Fund.replace(':currency', 'BTC')); + } + async goToFundStxPage() { + await this.page.waitForURL('**' + RouteUrls.FundChooseCurrency); + await this.stxButton.click(); + await this.page.waitForURL('**' + RouteUrls.Fund.replace(':currency', 'STX')); + } +} diff --git a/tests/page-object-models/home.page.ts b/tests/page-object-models/home.page.ts index a613ed74d40..e54b5b11b18 100644 --- a/tests/page-object-models/home.page.ts +++ b/tests/page-object-models/home.page.ts @@ -132,7 +132,7 @@ export class HomePage { await this.lockSettingsListItem.click(); } - async goToFundPage() { + async goToFundChooseCurrencyPage() { await this.fundAccountBtn.click(); } } diff --git a/tests/specs/fund/fund.spec.ts b/tests/specs/fund/fund.spec.ts index 301a780a60f..3bf6f1afd40 100644 --- a/tests/specs/fund/fund.spec.ts +++ b/tests/specs/fund/fund.spec.ts @@ -6,10 +6,37 @@ test.describe('Buy tokens test', () => { test.beforeEach(async ({ extensionId, globalPage, onboardingPage, homePage }) => { await globalPage.setupAndUseApiCalls(extensionId); await onboardingPage.signInWithTestAccount(extensionId); - await homePage.goToFundPage(); + await homePage.goToFundChooseCurrencyPage(); }); - test('should redirect to provider URL', async ({ page }) => { + test('should redirect to provider URL while funding STX', async ({ + page, + fundChooseCurrencyPage, + }) => { + await fundChooseCurrencyPage.goToFundStxPage(); + await test.expect(page.getByTestId(FundPageSelectors.FiatProviderName)).not.toHaveCount(0); + + const providerNames = await page.getByTestId(FundPageSelectors.FiatProviderName).all(); + const name = await providerNames[0].innerText(); + + const providerItems = await page.getByTestId(FundPageSelectors.FiatProviderItem).all(); + await providerItems[0].click(); + + await page.waitForTimeout(2000); + const allPages = page.context().pages(); + const recentPage = allPages.pop(); + await recentPage?.waitForLoadState(); + const URL = recentPage?.url(); + test.expect(URL).toContain(name); + }); + + test('should redirect to provider URL while funding BTC', async ({ + page, + fundChooseCurrencyPage, + }) => { + await fundChooseCurrencyPage.goToFundBtcPage(); + await test.expect(page.getByTestId(FundPageSelectors.FiatProviderName)).not.toHaveCount(0); + const providerNames = await page.getByTestId(FundPageSelectors.FiatProviderName).all(); const name = await providerNames[0].innerText();