diff --git a/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx b/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx new file mode 100644 index 00000000..5d5e26ea --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/bitcoin-contract-request.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react'; + +import { useBitcoinContracts } from '@app/common/hooks/use-bitcoin-contracts'; +import { BitcoinContractOfferDetails } from '@app/common/hooks/use-bitcoin-contracts'; +import { useOnMount } from '@app/common/hooks/use-on-mount'; +import { initialSearchParams } from '@app/common/initial-search-params'; +import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks'; + +import { BitcoinContractOfferDetailsSimple } from './components/bitcoin-contract-offer/bitcoin-contract-offer-details'; +import { BitcoinContractRequestActions } from './components/bitcoin-contract-request-actions'; +import { BitcoinContractRequestHeader } from './components/bitcoin-contract-request-header'; +import { BitcoinContractRequestLayout } from './components/bitcoin-contract-request-layout'; +import { BitcoinContractRequestWarningLabel } from './components/bitcoin-contract-request-warning-label'; + +export function BitcoinContractRequest() { + const getNativeSegwitSigner = useCurrentAccountNativeSegwitSigner(); + + const { handleOffer, handleAccept, handleReject } = useBitcoinContracts(); + + const [bitcoinContractJSON, setBitcoinContractJSON] = useState(); + const [bitcoinContractOfferDetails, setBitcoinContractOfferDetails] = + useState(); + const [bitcoinAddress, setBitcoinAddress] = useState(); + + const [isLoading, setLoading] = useState(true); + + const handleAcceptClick = async () => { + if (!bitcoinContractJSON || !bitcoinContractOfferDetails) return; + + await handleAccept(bitcoinContractJSON, bitcoinContractOfferDetails.counterpartyWalletDetails); + }; + + const handleRejectClick = async () => { + if (!bitcoinContractOfferDetails) return; + + await handleReject(bitcoinContractOfferDetails.simplifiedBitcoinContract.bitcoinContractId); + }; + + useOnMount(() => { + const bitcoinContractOfferJSON = initialSearchParams.get('bitcoinContractOffer'); + const counterpartyWalletURL = initialSearchParams.get('counterpartyWalletURL'); + const counterpartyWalletName = initialSearchParams.get('counterpartyWalletName'); + const counterpartyWalletIcon = initialSearchParams.get('counterpartyWalletIcon'); + + if ( + !getNativeSegwitSigner || + !bitcoinContractOfferJSON || + !counterpartyWalletURL || + !counterpartyWalletName || + !counterpartyWalletIcon + ) + return; + + const currentBitcoinContractOfferDetails = handleOffer( + bitcoinContractOfferJSON, + counterpartyWalletURL, + counterpartyWalletName, + counterpartyWalletIcon + ); + + const currentAddress = getNativeSegwitSigner(0).address; + + setBitcoinContractJSON(bitcoinContractOfferJSON); + setBitcoinContractOfferDetails(currentBitcoinContractOfferDetails); + setBitcoinAddress(currentAddress); + setLoading(false); + }); + + return ( + <> + {!isLoading && bitcoinAddress && bitcoinContractOfferDetails && ( + + + + + + + )} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-expiration-date.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-expiration-date.tsx new file mode 100644 index 00000000..a4c212af --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-expiration-date.tsx @@ -0,0 +1,24 @@ +import { Flex } from '@stacks/ui'; + +import { SpaceBetween } from '@app/components/layout/space-between'; +import { Text } from '@app/components/typography'; + +interface BitcoinContractExpirationDateProps { + expirationDate: string; +} +export function BitcoinContractExpirationDate({ + expirationDate, +}: BitcoinContractExpirationDateProps) { + return ( + + + + Expiration Date + + + {expirationDate} + + + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-lock-amount.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-lock-amount.tsx new file mode 100644 index 00000000..7b49804a --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-lock-amount.tsx @@ -0,0 +1,76 @@ +import { FiArrowUpRight, FiCopy } from 'react-icons/fi'; + +import { Box, Stack, Text, color, useClipboard } from '@stacks/ui'; + +import { BtcIcon } from '@app/components/icons/btc-icon'; +import { Flag } from '@app/components/layout/flag'; +import { SpaceBetween } from '@app/components/layout/space-between'; +import { Tooltip } from '@app/components/tooltip'; + +interface BitcoinContractLockAmountProps { + hoverLabel?: string; + image?: JSX.Element; + subtitle?: string; + subValue?: string; + subValueAction?(): void; + title?: string; + value: string; +} +export function BitcoinContractLockAmount({ + hoverLabel, + image, + subtitle, + subValue, + subValueAction, + title, + value, +}: BitcoinContractLockAmountProps) { + const { onCopy, hasCopied } = useClipboard(hoverLabel ?? ''); + + return ( + } my="loose" spacing="base"> + + + {title ? title : 'BTC'} + + + {value} + + + + {subtitle ? ( + + + + {subtitle} + + {hoverLabel ? : null} + + + ) : null} + {subValue ? ( + + + {subValue} + + {subValueAction ? : null} + + ) : null} + + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-details.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-details.tsx new file mode 100644 index 00000000..c3421d46 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-details.tsx @@ -0,0 +1,25 @@ +import { SimplifiedBitcoinContract } from '@app/common/hooks/use-bitcoin-contracts'; + +import { BitcoinContractExpirationDate } from './bitcoin-contract-expiration-date'; +import { BitcoinContractOfferInput } from './bitcoin-contract-offer-input'; + +interface BitcoinContractOfferDetailsSimpleProps { + bitcoinAddress: string; + bitcoinContractOffer: SimplifiedBitcoinContract; +} +export function BitcoinContractOfferDetailsSimple({ + bitcoinAddress, + bitcoinContractOffer, +}: BitcoinContractOfferDetailsSimpleProps) { + return ( + <> + + + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-input.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-input.tsx new file mode 100644 index 00000000..0cb225b6 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-offer/bitcoin-contract-offer-input.tsx @@ -0,0 +1,48 @@ +import { Box, Text } from '@stacks/ui'; +import { truncateMiddle } from '@stacks/ui-utils'; + +import { createMoneyFromDecimal } from '@shared/models/money.model'; + +import { SimplifiedBitcoinContract } from '@app/common/hooks/use-bitcoin-contracts'; +import { formatMoney, i18nFormatCurrency } from '@app/common/money/format-money'; +import { satToBtc } from '@app/common/money/unit-conversion'; +import { useCalculateBitcoinFiatValue } from '@app/query/common/market-data/market-data.hooks'; + +import { BitcoinContractLockAmount } from './bitcoin-contract-lock-amount'; + +interface BitcoinContractOfferInputProps { + addressNativeSegwit: string; + bitcoinContractOffer: SimplifiedBitcoinContract; +} +export function BitcoinContractOfferInput({ + addressNativeSegwit, + bitcoinContractOffer, +}: BitcoinContractOfferInputProps) { + const calculateFiatValue = useCalculateBitcoinFiatValue(); + + const bitcoinValue = satToBtc(bitcoinContractOffer.bitcoinContractCollateralAmount); + const money = createMoneyFromDecimal(bitcoinValue, 'BTC'); + const fiatValue = calculateFiatValue(money); + const formattedBitcoinValue = formatMoney(money); + const formattedFiatValue = i18nFormatCurrency(fiatValue); + + return ( + + Amount + +
+
+ ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-actions.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-actions.tsx new file mode 100644 index 00000000..1143ff49 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-actions.tsx @@ -0,0 +1,55 @@ +import { Box, Button, Stack, color } from '@stacks/ui'; + +import { useBtcAssetBalance } from '@app/common/hooks/balance/btc/use-btc-balance'; +import { PrimaryButton } from '@app/components/primary-button'; + +interface BitcoinContractRequestActionsProps { + isLoading: boolean; + bitcoinAddress: string; + requiredAmount: number; + onRejectBitcoinContractOffer(): Promise; + onAcceptBitcoinContractOffer(): Promise; +} +export function BitcoinContractRequestActions({ + isLoading, + bitcoinAddress, + requiredAmount, + onRejectBitcoinContractOffer, + onAcceptBitcoinContractOffer, +}: BitcoinContractRequestActionsProps) { + const { btcAvailableAssetBalance } = useBtcAssetBalance(bitcoinAddress); + const canAccept = btcAvailableAssetBalance.balance.amount.isGreaterThan(requiredAmount); + + return ( + + + + + Accept + + + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-header.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-header.tsx new file mode 100644 index 00000000..709f42af --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-header.tsx @@ -0,0 +1,37 @@ +import { memo } from 'react'; + +import { Flex } from '@stacks/ui'; + +import { Flag } from '@app/components/layout/flag'; +import { Caption, Title } from '@app/components/typography'; + +interface BitcoinContractRequestHeaderBaseProps { + counterpartyWalletIcon: string; + counterpartyWalletName: string; +} + +function BitcoinContractRequestHeaderBase({ + counterpartyWalletName, + counterpartyWalletIcon, +}: BitcoinContractRequestHeaderBaseProps) { + const caption = `${counterpartyWalletName} is requesting you accept this offer`; + + return ( + + + Lock Bitcoin + + {caption && ( + } + pl="tight" + > + {caption} + + )} + + ); +} + +export const BitcoinContractRequestHeader = memo(BitcoinContractRequestHeaderBase); diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-layout.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-layout.tsx new file mode 100644 index 00000000..99df93e3 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-layout.tsx @@ -0,0 +1,20 @@ +import { Stack } from '@stacks/ui'; + +interface BitcoinContractRequestLayoutProps { + children: React.ReactNode; +} +export function BitcoinContractRequestLayout({ children }: BitcoinContractRequestLayoutProps) { + return ( + + {children} + + ); +} diff --git a/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-warning-label.tsx b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-warning-label.tsx new file mode 100644 index 00000000..d209ea52 --- /dev/null +++ b/src/app/pages/bitcoin-contract-request/components/bitcoin-contract-request-warning-label.tsx @@ -0,0 +1,13 @@ +import { WarningLabel } from '@app/components/warning-label'; + +export function BitcoinContractRequestWarningLabel(props: { appName?: string }) { + const { appName } = props; + const title = `Do not proceed unless you trust ${appName ?? 'Unknown'}!`; + + return ( + + By signing the contract YOU AGREE TO LOCK YOUR BITCOIN with {appName} into a contract where it + will remain until a triggering event will release it. + + ); +}