diff --git a/dapp/src/DApp.tsx b/dapp/src/DApp.tsx index 085b6fbe8..d1d38a8bd 100644 --- a/dapp/src/DApp.tsx +++ b/dapp/src/DApp.tsx @@ -2,7 +2,11 @@ import React from "react" import { ChakraProvider } from "@chakra-ui/react" import { useDetectThemeMode } from "./hooks" import theme from "./theme" -import { LedgerWalletAPIProvider, WalletContextProvider } from "./contexts" +import { + LedgerWalletAPIProvider, + StakingFlowProvider, + WalletContextProvider, +} from "./contexts" import Header from "./components/Header" import Overview from "./components/Overview" @@ -23,9 +27,11 @@ function DAppProviders() { return ( - - - + + + + + ) diff --git a/dapp/src/components/Header/index.tsx b/dapp/src/components/Header/index.tsx index 838ed37b2..d78d97ef8 100644 --- a/dapp/src/components/Header/index.tsx +++ b/dapp/src/components/Header/index.tsx @@ -2,9 +2,11 @@ import React from "react" import { Flex } from "@chakra-ui/react" import ConnectWallet from "./ConnectWallet" +export const HEADER_HEIGHT = 24 + export default function Header() { return ( - + ) diff --git a/dapp/src/components/ModalOverlay/index.tsx b/dapp/src/components/ModalOverlay/index.tsx new file mode 100644 index 000000000..9ca30d1f1 --- /dev/null +++ b/dapp/src/components/ModalOverlay/index.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useState } from "react" +import { Box, useColorModeValue } from "@chakra-ui/react" +import { useStakingFlowContext } from "../../hooks" + +const DELAY = 300 + +export default function ModalOverlay({ marginTop }: { marginTop?: number }) { + const { modalType } = useStakingFlowContext() + const [showOverlay, setShowOverlay] = useState(!modalType) + + useEffect(() => { + const timeout = setTimeout( + () => { + setShowOverlay(!!modalType) + }, + // When the modal opens, we should show the sidebar without delay. + // However, when the modal disappears, the overlay should disappear with some delay. + modalType ? 0 : DELAY, + ) + return () => clearTimeout(timeout) + }, [modalType]) + + return ( + + ) +} diff --git a/dapp/src/components/Modals/BaseModal.tsx b/dapp/src/components/Modals/BaseModal.tsx new file mode 100644 index 000000000..1f98f4345 --- /dev/null +++ b/dapp/src/components/Modals/BaseModal.tsx @@ -0,0 +1,23 @@ +import React from "react" +import { Modal, ModalCloseButton, ModalContent } from "@chakra-ui/react" +import { useStakingFlowContext } from "../../hooks" +import { HEADER_HEIGHT } from "../Header" + +export default function BaseModal({ children }: { children: React.ReactNode }) { + const { closeModal } = useStakingFlowContext() + + return ( + + + + {children} + + + ) +} diff --git a/dapp/src/components/Modals/ConnectWalletModal.tsx b/dapp/src/components/Modals/ConnectWalletModal.tsx new file mode 100644 index 000000000..5ca953960 --- /dev/null +++ b/dapp/src/components/Modals/ConnectWalletModal.tsx @@ -0,0 +1,44 @@ +import React from "react" +import { + Button, + Image, + ModalBody, + ModalFooter, + ModalHeader, + VStack, + Text, +} from "@chakra-ui/react" +import { useRequestBitcoinAccount } from "../../hooks" +import BaseModal from "./BaseModal" +import ConnectBTCAccount from "../../static/images/ConnectBTCAccount.png" + +export default function ConnectWalletModal() { + const { requestAccount } = useRequestBitcoinAccount() + + return ( + + + Bitcoin account not installed + + + + + + Bitcoin account is required to make transactions for depositing and + staking your BTC. + + + + + + + + ) +} diff --git a/dapp/src/components/Modals/StakingOverviewModal.tsx b/dapp/src/components/Modals/StakingOverviewModal.tsx new file mode 100644 index 000000000..e7cbcddb2 --- /dev/null +++ b/dapp/src/components/Modals/StakingOverviewModal.tsx @@ -0,0 +1,67 @@ +import React from "react" +import { + Button, + Flex, + ModalBody, + ModalFooter, + ModalHeader, + Step, + StepDescription, + StepIndicator, + StepNumber, + StepSeparator, + StepTitle, + Stepper, +} from "@chakra-ui/react" +import { useStakingFlowContext } from "../../hooks" +import BaseModal from "./BaseModal" + +const STEPS = [ + { + title: "Sign message", + description: + "You will sign a gas-free Ethereum message to indicate the address where you'd like to get your stBTC liquid staking token.", + }, + { + title: "Deposit BTC", + description: + "You will make a Bitcoin transaction to deposit and stake your BTC.", + }, +] + +export default function StakingOverviewModal() { + const { closeModal } = useStakingFlowContext() + + return ( + + Staking overview + + + {STEPS.map((step) => ( + + + + + + {step.title} + {step.description} + + + + ))} + + + + + + + ) +} diff --git a/dapp/src/components/Overview/PositionDetails.tsx b/dapp/src/components/Overview/PositionDetails.tsx index 972db661f..cfe51119c 100644 --- a/dapp/src/components/Overview/PositionDetails.tsx +++ b/dapp/src/components/Overview/PositionDetails.tsx @@ -10,34 +10,41 @@ import { } from "@chakra-ui/react" import { BITCOIN, USD } from "../../constants" import { Info } from "../../static/icons" +import Staking from "../Staking" +import { useStakingFlowContext } from "../../hooks" export default function PositionDetails() { + const { setModalType } = useStakingFlowContext() + return ( - - - Your positions - - - 34.75 - {BITCOIN.symbol} - - + <> + + + Your positions + - 1.245.148,1 - {USD.symbol} + 34.75 + {BITCOIN.symbol} - {/* TODO: Add correct text for tooltip */} - - - + + + 1.245.148,1 + {USD.symbol} + + {/* TODO: Add correct text for tooltip */} + + + + + + {/* TODO: Handle click actions */} + + + - - {/* TODO: Handle click actions */} - - - - + + ) } diff --git a/dapp/src/components/Staking/index.tsx b/dapp/src/components/Staking/index.tsx new file mode 100644 index 000000000..b4e56103b --- /dev/null +++ b/dapp/src/components/Staking/index.tsx @@ -0,0 +1,28 @@ +import React from "react" +import ConnectWalletModal from "../Modals/ConnectWalletModal" +import StakingOverviewModal from "../Modals/StakingOverviewModal" +import { useStakingFlowContext, useWalletContext } from "../../hooks" +import ModalOverlay from "../ModalOverlay" +import { HEADER_HEIGHT } from "../Header" + +function Modal() { + const { modalType } = useStakingFlowContext() + const { btcAccount } = useWalletContext() + + if (!modalType) return null + + if (!btcAccount) return + + if (modalType === "overview") return +} + +export default function Staking() { + return ( + <> + + {/* The user has several modals in a flow. + Let's use our own modal overlay to prevent the background flickering effect. */} + + + ) +} diff --git a/dapp/src/contexts/StakingFlowContext.tsx b/dapp/src/contexts/StakingFlowContext.tsx new file mode 100644 index 000000000..a58446787 --- /dev/null +++ b/dapp/src/contexts/StakingFlowContext.tsx @@ -0,0 +1,42 @@ +import React, { createContext, useCallback, useMemo, useState } from "react" +import { ModalType } from "../types" + +type StakingFlowContextValue = { + modalType: ModalType | undefined + closeModal: () => void + setModalType: React.Dispatch> +} + +export const StakingFlowContext = createContext({ + modalType: undefined, + setModalType: () => {}, + closeModal: () => {}, +}) + +export function StakingFlowProvider({ + children, +}: { + children: React.ReactNode +}): React.ReactElement { + const [modalType, setModalType] = useState(undefined) + + const closeModal = useCallback(() => { + setModalType(undefined) + }, []) + + const contextValue: StakingFlowContextValue = + useMemo( + () => ({ + modalType, + closeModal, + setModalType, + }), + [modalType, closeModal], + ) + + return ( + + {children} + + ) +} diff --git a/dapp/src/contexts/WalletContext.tsx b/dapp/src/contexts/WalletContext.tsx index f327ce0bd..63fb3faba 100644 --- a/dapp/src/contexts/WalletContext.tsx +++ b/dapp/src/contexts/WalletContext.tsx @@ -8,9 +8,12 @@ type WalletContextValue = { setEthAccount: React.Dispatch> } -export const WalletContext = createContext( - undefined, -) +export const WalletContext = createContext({ + ethAccount: undefined, + btcAccount: undefined, + setEthAccount: () => {}, + setBtcAccount: () => {}, +}) export function WalletContextProvider({ children, @@ -27,7 +30,7 @@ export function WalletContextProvider({ ethAccount, setEthAccount, }), - [btcAccount, setBtcAccount, ethAccount, setEthAccount], + [btcAccount, ethAccount], ) return ( diff --git a/dapp/src/contexts/index.tsx b/dapp/src/contexts/index.tsx index 3ea3058ed..9271773c0 100644 --- a/dapp/src/contexts/index.tsx +++ b/dapp/src/contexts/index.tsx @@ -1,2 +1,3 @@ export * from "./WalletContext" export * from "./LedgerWalletAPIProvider" +export * from "./StakingFlowContext" diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 038388476..b12025abb 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -2,3 +2,4 @@ export * from "./useDetectThemeMode" export * from "./useRequestBitcoinAccount" export * from "./useRequestEthereumAccount" export * from "./useWalletContext" +export * from "./useStakingFlowContext" diff --git a/dapp/src/hooks/useRequestBitcoinAccount.ts b/dapp/src/hooks/useRequestBitcoinAccount.ts index 971a3f9ef..b9cce07c2 100644 --- a/dapp/src/hooks/useRequestBitcoinAccount.ts +++ b/dapp/src/hooks/useRequestBitcoinAccount.ts @@ -5,12 +5,12 @@ import { UseRequestAccountReturn } from "../types" import { WalletContext } from "../contexts" export function useRequestBitcoinAccount(): UseRequestAccountReturn { - const walletContext = useContext(WalletContext) + const { setBtcAccount } = useContext(WalletContext) const { account, requestAccount } = useRequestAccount() useEffect(() => { - walletContext?.setBtcAccount(account || undefined) - }, [account, walletContext]) + setBtcAccount(account || undefined) + }, [account, setBtcAccount]) const requestBitcoinAccount = useCallback(async () => { await requestAccount({ currencyIds: [CURRENCY_ID_BITCOIN] }) diff --git a/dapp/src/hooks/useRequestEthereumAccount.ts b/dapp/src/hooks/useRequestEthereumAccount.ts index ebeb1f268..c780aea20 100644 --- a/dapp/src/hooks/useRequestEthereumAccount.ts +++ b/dapp/src/hooks/useRequestEthereumAccount.ts @@ -5,12 +5,12 @@ import { UseRequestAccountReturn } from "../types" import { WalletContext } from "../contexts" export function useRequestEthereumAccount(): UseRequestAccountReturn { - const walletContext = useContext(WalletContext) + const { setEthAccount } = useContext(WalletContext) const { account, requestAccount } = useRequestAccount() useEffect(() => { - walletContext?.setEthAccount(account || undefined) - }, [account, walletContext]) + setEthAccount(account || undefined) + }, [account, setEthAccount]) const requestEthereumAccount = useCallback(async () => { await requestAccount({ currencyIds: [CURRENCY_ID_ETHEREUM] }) diff --git a/dapp/src/hooks/useStakingFlowContext.ts b/dapp/src/hooks/useStakingFlowContext.ts new file mode 100644 index 000000000..870e15f71 --- /dev/null +++ b/dapp/src/hooks/useStakingFlowContext.ts @@ -0,0 +1,14 @@ +import { useContext } from "react" +import { StakingFlowContext } from "../contexts" + +export function useStakingFlowContext() { + const context = useContext(StakingFlowContext) + + if (!context) { + throw new Error( + "StakingFlowContext used outside of StakingFlowContext component", + ) + } + + return context +} diff --git a/dapp/src/static/images/ConnectBTCAccount.png b/dapp/src/static/images/ConnectBTCAccount.png new file mode 100644 index 000000000..deff3c0cc Binary files /dev/null and b/dapp/src/static/images/ConnectBTCAccount.png differ diff --git a/dapp/src/types/index.ts b/dapp/src/types/index.ts index 1e77e81e7..32b259c7f 100644 --- a/dapp/src/types/index.ts +++ b/dapp/src/types/index.ts @@ -1,2 +1,3 @@ export * from "./ledger-live-app" export * from "./currency" +export * from "./staking" diff --git a/dapp/src/types/staking.ts b/dapp/src/types/staking.ts new file mode 100644 index 000000000..38c5d8098 --- /dev/null +++ b/dapp/src/types/staking.ts @@ -0,0 +1 @@ +export type ModalType = "overview" | "stake"