diff --git a/dapp/.env b/dapp/.env index 79f9dc9d6..fdb71b35d 100644 --- a/dapp/.env +++ b/dapp/.env @@ -26,4 +26,5 @@ VITE_FEATURE_FLAG_XVERSE_WALLET_ENABLED="false" VITE_FEATURE_FLAG_ACRE_POINTS_ENABLED="true" VITE_FEATURE_FLAG_TVL_ENABLED="true" VITE_FEATURE_GATING_DAPP_ENABLED="true" +VITE_FEATURE_MOBILE_MODE_ENABLED="true" diff --git a/dapp/index.html b/dapp/index.html index 606a95c29..b3e328177 100644 --- a/dapp/index.html +++ b/dapp/index.html @@ -1,13 +1,33 @@ - - - - - ACRE - - -
- - + + + Acre App + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/dapp/manifests/ledger-live/ledger-live-manifest-development.json b/dapp/manifests/ledger-live/ledger-live-manifest-development.json index 649115b55..bffaf7026 100644 --- a/dapp/manifests/ledger-live/ledger-live-manifest-development.json +++ b/dapp/manifests/ledger-live/ledger-live-manifest-development.json @@ -2,9 +2,9 @@ "id": "acre-dev", "name": "Acre [dev]", "url": "http://localhost:5173/?embed=ledger-live", - "homepageUrl": "https:/acre.fi/", + "homepageUrl": "https://acre.fi/", "icon": "http://localhost:5173/acre.svg", - "platforms": "desktop", + "platforms": "all", "apiVersion": "^2.0.0", "manifestVersion": "2", "branch": "stable", @@ -12,10 +12,10 @@ "currencies": ["bitcoin", "bitcoin_testnet"], "content": { "shortDescription": { - "en": "Bitcoin Liquid Staking" + "en": "Bitcoin Rewards Done Right" }, "description": { - "en": "Bitcoin Liquid Staking" + "en": "Bitcoin Rewards Done Right" } }, "permissions": [ @@ -23,10 +23,10 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" ], - "domains": ["http://*"], - "type": "walletApp" + "domains": ["https://"] } diff --git a/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json b/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json index a24319deb..22f74bbc5 100644 --- a/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json +++ b/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json @@ -2,9 +2,9 @@ "id": "acre", "name": "Acre", "url": "https://bitcoin.acre.fi/?embed=ledger-live", - "homepageUrl": "https:/acre.fi/", + "homepageUrl": "https://acre.fi/", "icon": "https://bitcoin.acre.fi/acre.svg", - "platforms": "desktop", + "platforms": "all", "apiVersion": "^2.0.0", "manifestVersion": "2", "branch": "stable", @@ -12,10 +12,10 @@ "currencies": ["bitcoin", "bitcoin_testnet"], "content": { "shortDescription": { - "en": "Bitcoin Liquid Staking" + "en": "Bitcoin Rewards Done Right" }, "description": { - "en": "Bitcoin Liquid Staking" + "en": "Bitcoin Rewards Done Right" } }, "permissions": [ @@ -23,10 +23,10 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" ], - "domains": ["http://*"], - "type": "walletApp" + "domains": ["https://"] } diff --git a/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json b/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json index 06dc73564..a6ea2019c 100644 --- a/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json +++ b/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json @@ -2,9 +2,9 @@ "id": "acre-testnet", "name": "Acre [testnet]", "url": "https://bitcoin.test.acre.fi/?embed=ledger-live", - "homepageUrl": "https:/acre.fi/", + "homepageUrl": "https://acre.fi/", "icon": "https://bitcoin.test.acre.fi/acre.svg", - "platforms": "desktop", + "platforms": "all", "apiVersion": "^2.0.0", "manifestVersion": "2", "branch": "stable", @@ -12,10 +12,10 @@ "currencies": ["bitcoin", "bitcoin_testnet"], "content": { "shortDescription": { - "en": "Bitcoin Liquid Staking" + "en": "Bitcoin Rewards Done Right" }, "description": { - "en": "Bitcoin Liquid Staking" + "en": "Bitcoin Rewards Done Right" } }, "permissions": [ @@ -23,10 +23,10 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" ], - "domains": ["http://*"], - "type": "walletApp" + "domains": ["https://"] } diff --git a/dapp/manifests/ledger-live/ledger-manifest-template.json b/dapp/manifests/ledger-live/ledger-manifest-template.json index 6a4bc80ee..980c407d2 100644 --- a/dapp/manifests/ledger-live/ledger-manifest-template.json +++ b/dapp/manifests/ledger-live/ledger-manifest-template.json @@ -2,9 +2,9 @@ "id": "{{id}}", "name": "{{name}}", "url": "{{{url}}}?embed=ledger-live", - "homepageUrl": "https:/acre.fi/", + "homepageUrl": "https://acre.fi/", "icon": "{{{url}}}acre.svg", - "platforms": "desktop", + "platforms": "all", "apiVersion": "^2.0.0", "manifestVersion": "2", "branch": "stable", @@ -12,10 +12,10 @@ "currencies": ["bitcoin", "bitcoin_testnet"], "content": { "shortDescription": { - "en": "Bitcoin Liquid Staking" + "en": "Bitcoin Rewards Done Right" }, "description": { - "en": "Bitcoin Liquid Staking" + "en": "Bitcoin Rewards Done Right" } }, "permissions": [ @@ -23,10 +23,10 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" ], - "domains": ["http://*"], - "type": "walletApp" + "domains": ["https://"] } diff --git a/dapp/package.json b/dapp/package.json index 97601a1a3..b384d7b43 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -23,7 +23,7 @@ "@emotion/styled": "^11.11.0", "@ledgerhq/wallet-api-acre-module": "0.1.0", "@ledgerhq/wallet-api-client": "1.6.0", - "@orangekit/react": "1.0.0-beta.33", + "@orangekit/react": "1.0.0-beta.34", "@orangekit/sign-in-with-wallet": "1.0.0-beta.6", "@reduxjs/toolkit": "^2.2.0", "@rehooks/local-storage": "^2.4.5", diff --git a/dapp/public/meta-image.png b/dapp/public/meta-image.png new file mode 100644 index 000000000..f2ebebc8e Binary files /dev/null and b/dapp/public/meta-image.png differ diff --git a/dapp/src/components/AcrePointsClaimModal.tsx b/dapp/src/components/AcrePointsClaimModal.tsx index 6efa97ee0..bc3f045af 100644 --- a/dapp/src/components/AcrePointsClaimModal.tsx +++ b/dapp/src/components/AcrePointsClaimModal.tsx @@ -162,7 +162,7 @@ function AcrePointsClaimModalBase({ {children} + {children} ) : ( children ) diff --git a/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx b/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx index 0747fdc8e..8b12b6236 100644 --- a/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx +++ b/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx @@ -129,6 +129,11 @@ export default function ConnectWalletButton({ }, onError: (error: OrangeKitError) => { const errorData = orangeKit.parseOrangeKitConnectionError(error) + + if (errorData === CONNECTION_ERRORS.DEFAULT) { + console.error("Failed to connect", error) + } + setConnectionError(errorData) }, }) diff --git a/dapp/src/components/ConnectWalletModal/ConnectWalletErrorAlert.tsx b/dapp/src/components/ConnectWalletModal/ConnectWalletErrorAlert.tsx index e7439d8e4..2e97b59ea 100644 --- a/dapp/src/components/ConnectWalletModal/ConnectWalletErrorAlert.tsx +++ b/dapp/src/components/ConnectWalletModal/ConnectWalletErrorAlert.tsx @@ -39,7 +39,7 @@ export default function ConnectWalletErrorAlert( - {title} + {title} {description} diff --git a/dapp/src/components/ConnectWalletModal/ConnectWalletStatusLabel.tsx b/dapp/src/components/ConnectWalletModal/ConnectWalletStatusLabel.tsx index 5f89720f8..ec1a0e651 100644 --- a/dapp/src/components/ConnectWalletModal/ConnectWalletStatusLabel.tsx +++ b/dapp/src/components/ConnectWalletModal/ConnectWalletStatusLabel.tsx @@ -46,12 +46,12 @@ export default function ConnectWalletStatusLabel({ return ( - + {icon} {label} {isError && ( - + Rejected by user diff --git a/dapp/src/components/Footer.tsx b/dapp/src/components/Footer.tsx index 7a907a5c5..d80161d7f 100644 --- a/dapp/src/components/Footer.tsx +++ b/dapp/src/components/Footer.tsx @@ -11,6 +11,7 @@ import { } from "@chakra-ui/react" import { EXTERNAL_HREF } from "#/constants" import { AcreSignIcon, ArrowUpRight } from "#/assets/icons" +import { useMobileMode } from "#/hooks" type FooterListItem = Pick @@ -74,6 +75,9 @@ const getItemsList = ( export default function Footer() { const styles = useMultiStyleConfig("Footer") + const isMobileMode = useMobileMode() + + if (isMobileMode) return null return ( diff --git a/dapp/src/components/Header/ConnectWallet.tsx b/dapp/src/components/Header/ConnectWallet.tsx index e3424ed1d..fad9aa380 100644 --- a/dapp/src/components/Header/ConnectWallet.tsx +++ b/dapp/src/components/Header/ConnectWallet.tsx @@ -5,12 +5,15 @@ import { HStack, Icon, IconButton, + Menu, + MenuButton, + MenuItem, + MenuList, StackDivider, - Tooltip, useClipboard, useMultiStyleConfig, } from "@chakra-ui/react" -import { useIsEmbed, useModal, useWallet } from "#/hooks" +import { useIsEmbed, useMobileMode, useModal, useWallet } from "#/hooks" import { CurrencyBalance } from "#/components/shared/CurrencyBalance" import { TextMd } from "#/components/shared/Typography" import { BitcoinIcon } from "#/assets/icons" @@ -22,8 +25,11 @@ import { IconLogout, IconWallet, IconUserCode, + IconChevronDown, + IconChevronUp, } from "@tabler/icons-react" import { useMatch } from "react-router-dom" +import Tooltip from "../shared/Tooltip" function isChangeAccountFeatureSupported(embeddedApp: string | undefined) { return referralProgram.isEmbedApp(embeddedApp) @@ -39,6 +45,7 @@ export default function ConnectWallet() { size: "lg", }) const isDashboardPage = useMatch("/dashboard") + const isMobile = useMobileMode() const handleConnectWallet = (isReconnecting: boolean = false) => { openModal(MODAL_TYPES.CONNECT_WALLET, { isReconnecting }) @@ -62,8 +69,65 @@ export default function ConnectWallet() { ) } + const options = [ + { + id: "Copy", + icon: IconCopy, + label: hasCopied ? "Address copied" : "Copy Address", + onClick: onCopy, + isSupported: true, + closeOnSelect: false, + }, + { + id: "Change account", + icon: IconUserCode, + label: "Change account", + onClick: () => handleConnectWallet(true), + isSupported: isChangeAccountFeatureSupported(embeddedApp), + closeOnSelect: true, + }, + { + id: "Disconnect", + icon: IconLogout, + label: "Disconnect", + onClick: onDisconnect, + closeOnSelect: true, + isSupported: true, + }, + ] - return ( + return isMobile ? ( + + {({ isOpen }) => ( + <> + } + rightIcon={isOpen ? : } + > + {truncateAddress(address)} + + + {options.map( + (option) => + option.isSupported && ( + } + onClick={option.onClick} + > + {option.label} + + ), + )} + + + )} + + ) : ( @@ -100,44 +164,26 @@ export default function ConnectWallet() { spacing={1} divider={} > - - } - px={2} - boxSize={5} - onClick={onCopy} - /> - - - {isChangeAccountFeatureSupported(embeddedApp) && ( - - } - px={2} - boxSize={5} - onClick={() => handleConnectWallet(true)} - /> - + {options.map( + (option) => + option.isSupported && ( + + } + px={2} + boxSize={5} + onClick={option.onClick} + /> + + ), )} - - - } - px={2} - boxSize={5} - onClick={onDisconnect} - /> - diff --git a/dapp/src/components/Header/index.tsx b/dapp/src/components/Header/index.tsx index 8ac5e7ab5..25a48ab7f 100644 --- a/dapp/src/components/Header/index.tsx +++ b/dapp/src/components/Header/index.tsx @@ -13,9 +13,9 @@ export default function Header() { mx="auto" justify="space-between" zIndex="header" - pt={12} + pt={{ base: 4, md: 12 }} pb={{ base: 4, xl: 12 }} - px={{ base: 10, xl: 30 }} + px={{ base: 4, md: "2.5rem", xl: 30 }} > diff --git a/dapp/src/components/Layout.tsx b/dapp/src/components/Layout.tsx index ea34096aa..42bc526a7 100644 --- a/dapp/src/components/Layout.tsx +++ b/dapp/src/components/Layout.tsx @@ -10,7 +10,11 @@ import Sidebar from "./Sidebar" import MobileModeBanner from "./MobileModeBanner" import Footer from "./Footer" -const PADDING = "2.5rem" // 40px +// The padding update should also be done in the Header component and Footer theme as well +const PADDING = { + base: 4, + md: "2.5rem", // 40px +} const PAGE_MAX_WIDTH: Record = { standalone: "63rem", // 1008px "ledger-live": "63rem", // 1008px @@ -18,9 +22,9 @@ const PAGE_MAX_WIDTH: Record = { function Layout() { const isMobileMode = useMobileMode() - const { embeddedApp } = useIsEmbed() + const { isEmbed, embeddedApp } = useIsEmbed() - if (isMobileMode) return + if (!isEmbed && isMobileMode) return const maxWidth = embeddedApp ? PAGE_MAX_WIDTH[embeddedApp] @@ -33,10 +37,11 @@ function Layout() { diff --git a/dapp/src/components/MezoBeehiveModal.tsx b/dapp/src/components/MezoBeehiveModal.tsx index ccc88a5e0..18ca145d8 100644 --- a/dapp/src/components/MezoBeehiveModal.tsx +++ b/dapp/src/components/MezoBeehiveModal.tsx @@ -42,7 +42,7 @@ function MezoBeehiveModalBase() { - + {data && ( diff --git a/dapp/src/components/MobileModeBanner/index.tsx b/dapp/src/components/MobileModeBanner/index.tsx index e8a0028b4..aefba45dd 100644 --- a/dapp/src/components/MobileModeBanner/index.tsx +++ b/dapp/src/components/MobileModeBanner/index.tsx @@ -46,7 +46,7 @@ function MobileModeBanner(props: MobileModeBannerProps) { > - Staking is on + Acre App is on desktop! @@ -92,7 +92,7 @@ function MobileModeBanner(props: MobileModeBannerProps) {

- Staking is live only on desktop for now. We're working with + Acre App live only on desktop for now. We're working with partners to bring it to mobile soon.

diff --git a/dapp/src/components/TransactionModal/ActionFormModal.tsx b/dapp/src/components/TransactionModal/ActionFormModal.tsx index 53ab0e42b..8c50be64e 100644 --- a/dapp/src/components/TransactionModal/ActionFormModal.tsx +++ b/dapp/src/components/TransactionModal/ActionFormModal.tsx @@ -107,7 +107,7 @@ function ActionFormModal({ type }: { type: ActionFlowType }) { <> {!isLoading && } {heading} - + diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/BuildTransactionModal.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/BuildTransactionModal.tsx index c6b6f81af..84c3069b3 100644 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/BuildTransactionModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveUnstakingStep/BuildTransactionModal.tsx @@ -16,10 +16,10 @@ export default function BuildTransactionModal({ return ( <> - Building transaction data... + Preparing withdrawal transaction... - We are building your withdrawal data. + This may take a few minutes. diff --git a/dapp/src/components/TransactionModal/ResumeModal.tsx b/dapp/src/components/TransactionModal/ResumeModal.tsx index 16cb45999..3547673dc 100644 --- a/dapp/src/components/TransactionModal/ResumeModal.tsx +++ b/dapp/src/components/TransactionModal/ResumeModal.tsx @@ -22,7 +22,7 @@ export default function ResumeModal({ closeModal }: BaseModalProps) { return ( <> - + Paused diff --git a/dapp/src/components/TransactionModal/SuccessModal.tsx b/dapp/src/components/TransactionModal/SuccessModal.tsx index e03d49658..7fa7f8d31 100644 --- a/dapp/src/components/TransactionModal/SuccessModal.tsx +++ b/dapp/src/components/TransactionModal/SuccessModal.tsx @@ -32,7 +32,7 @@ export default function SuccessModal({ type }: SuccessModalProps) { return ( <> - + {ACTION_FLOW_TYPES.UNSTAKE === type ? "Withdrawal initiated!" : "Deposit received!"} diff --git a/dapp/src/components/TransactionModal/WalletInteractionModal.tsx b/dapp/src/components/TransactionModal/WalletInteractionModal.tsx index 4eeb6e702..8c3926d1f 100644 --- a/dapp/src/components/TransactionModal/WalletInteractionModal.tsx +++ b/dapp/src/components/TransactionModal/WalletInteractionModal.tsx @@ -10,8 +10,8 @@ import { ProgressProps, } from "@chakra-ui/react" import { AcreSignIcon } from "#/assets/icons" -import { useActionFlowType, useConnector } from "#/hooks" -import { ACTION_FLOW_TYPES } from "#/types" +import { useActionFlowType, useConnector, useIsEmbed } from "#/hooks" +import { ACTION_FLOW_TYPES, DappMode } from "#/types" import { Alert, AlertIcon } from "../shared/Alert" import { TextMd } from "../shared/Typography" @@ -22,11 +22,16 @@ const ICON_STYLES = { type WalletInteractionStep = "opening-wallet" | "awaiting-transaction" +const CONTENT_BY_DAPP_MODE: Record = { + standalone: "wallet", + "ledger-live": "Ledger Device", +} + const DATA: Record< WalletInteractionStep, { header: string - description: (action: string) => string + description: (action: string, mode: DappMode) => string progressProps?: ProgressProps } > = { @@ -37,7 +42,8 @@ const DATA: Record< }, "awaiting-transaction": { header: "Awaiting signature confirmation", - description: () => "Waiting for your wallet to confirm the transaction.", + description: (_, mode: DappMode) => + `Communicating with your ${CONTENT_BY_DAPP_MODE[mode]}...`, progressProps: { transform: "scaleX(-1)" }, }, } @@ -52,11 +58,12 @@ export default function WalletInteractionModal({ const actionType = useActionFlowType() const connector = useConnector() const { header, description, progressProps } = DATA[step] + const { embeddedApp } = useIsEmbed() return ( <> {step === "opening-wallet" && } - + {header} @@ -80,6 +87,7 @@ export default function WalletInteractionModal({ {description( actionType === ACTION_FLOW_TYPES.STAKE ? "deposit" : "withdraw", + embeddedApp ?? "standalone", )} {step === "awaiting-transaction" && ( @@ -87,7 +95,6 @@ export default function WalletInteractionModal({ This may take up to a minute. - Don't close this window. )} diff --git a/dapp/src/components/TransactionModal/index.tsx b/dapp/src/components/TransactionModal/index.tsx index 68add35cc..dd30a73ad 100644 --- a/dapp/src/components/TransactionModal/index.tsx +++ b/dapp/src/components/TransactionModal/index.tsx @@ -4,7 +4,6 @@ import { useAppDispatch, useFetchActivities, useIsSignedMessage, - useSidebar, useTransactionModal, } from "#/hooks" import { ActionFlowType, BaseModalProps } from "#/types" @@ -18,7 +17,6 @@ import { ConnectWalletModalBase } from "../ConnectWalletModal" type TransactionModalProps = { type: ActionFlowType } & BaseModalProps function TransactionModalBase({ type, closeModal }: TransactionModalProps) { - const { onOpen: openSideBar, onClose: closeSidebar } = useSidebar() const dispatch = useAppDispatch() const fetchActivities = useFetchActivities() @@ -34,11 +32,6 @@ function TransactionModalBase({ type, closeModal }: TransactionModalProps) { } }, [dispatch, fetchActivities]) - useEffect(() => { - openSideBar() - return () => closeSidebar() - }, [closeSidebar, openSideBar]) - return ( diff --git a/dapp/src/components/WelcomeModal.tsx b/dapp/src/components/WelcomeModal.tsx index d40d0bc75..b68a6ce75 100644 --- a/dapp/src/components/WelcomeModal.tsx +++ b/dapp/src/components/WelcomeModal.tsx @@ -17,7 +17,7 @@ import { import { H3, TextSm } from "#/components/shared/Typography" import { BaseModalProps, DappMode } from "#/types" import { EmbedApp } from "#/utils/referralProgram" -import { useIsEmbed } from "#/hooks" +import { useIsEmbed, useMobileMode } from "#/hooks" import withBaseModal from "./ModalRoot/withBaseModal" import step1Video from "../assets/videos/welcome-steps/welcome-step-1.mp4" import step2Video from "../assets/videos/welcome-steps/welcome-step-2.mp4" @@ -122,6 +122,7 @@ function WelcomeModalBase({ closeModal }: BaseModalProps) { count: steps.length, }) as UseStepsReturn & { goToNext: () => void } const { embeddedApp } = useIsEmbed() + const isMobileMode = useMobileMode() const isLastStep = activeStep + 1 === steps.length const activeStepData = steps[activeStep] @@ -130,19 +131,19 @@ function WelcomeModalBase({ closeModal }: BaseModalProps) { - + Welcome to Acre, {activeStepData.title} - + {activeStepData.content(embeddedApp)} {steps.map((step) => ( @@ -159,19 +160,21 @@ function WelcomeModalBase({ closeModal }: BaseModalProps) { - + {!isMobileMode && ( + + )} ) } diff --git a/dapp/src/components/shared/CurrencyBalance/index.tsx b/dapp/src/components/shared/CurrencyBalance/index.tsx index 87dede894..bf7f65750 100644 --- a/dapp/src/components/shared/CurrencyBalance/index.tsx +++ b/dapp/src/components/shared/CurrencyBalance/index.tsx @@ -4,7 +4,6 @@ import { useMultiStyleConfig, TextProps, ResponsiveValue, - Tooltip, } from "@chakra-ui/react" import { fixedPointNumberToString, @@ -13,6 +12,7 @@ import { numberToLocaleString, } from "#/utils" import { CurrencyType, AmountType } from "#/types" +import Tooltip from "../Tooltip" export type CurrencyBalanceProps = { currency: CurrencyType diff --git a/dapp/src/components/shared/InfoTooltip.tsx b/dapp/src/components/shared/InfoTooltip.tsx index 2f7731fa6..a9394b460 100644 --- a/dapp/src/components/shared/InfoTooltip.tsx +++ b/dapp/src/components/shared/InfoTooltip.tsx @@ -1,6 +1,7 @@ import React from "react" import { IconInfoCircleFilled } from "@tabler/icons-react" -import { Icon, Tooltip, TooltipProps } from "@chakra-ui/react" +import { Icon, TooltipProps } from "@chakra-ui/react" +import Tooltip from "./Tooltip" // TODO: Define in the new color palette const ICON_COLOR = "#3A3328" diff --git a/dapp/src/components/shared/TokenAmountForm/TokenAmountFormBase.tsx b/dapp/src/components/shared/TokenAmountForm/TokenAmountFormBase.tsx index 959d38ae0..5f3b10b25 100644 --- a/dapp/src/components/shared/TokenAmountForm/TokenAmountFormBase.tsx +++ b/dapp/src/components/shared/TokenAmountForm/TokenAmountFormBase.tsx @@ -42,6 +42,7 @@ export default function TokenAmountFormBase({ withMaxButton={withMaxButton} defaultAmount={defaultAmount} autoFocus + autoComplete="off" /> {children} diff --git a/dapp/src/components/shared/TokenAmountForm/index.tsx b/dapp/src/components/shared/TokenAmountForm/index.tsx index b3b157fec..a1360ed8e 100644 --- a/dapp/src/components/shared/TokenAmountForm/index.tsx +++ b/dapp/src/components/shared/TokenAmountForm/index.tsx @@ -36,7 +36,7 @@ const TokenAmountForm = withFormik( handleSubmit: (values, { props }) => { props.onSubmitForm(values) }, - validateOnBlur: true, + validateOnBlur: false, validateOnChange: false, }, )(TokenAmountFormBase) diff --git a/dapp/src/components/shared/Tooltip.tsx b/dapp/src/components/shared/Tooltip.tsx new file mode 100644 index 000000000..924e0e950 --- /dev/null +++ b/dapp/src/components/shared/Tooltip.tsx @@ -0,0 +1,19 @@ +import React, { useState } from "react" +import { Box, Tooltip as ChakraTooltip, TooltipProps } from "@chakra-ui/react" + +export default function Tooltip(props: TooltipProps) { + const { children, ...restProps } = props + const [isOpen, setIsOpen] = useState(false) + + return ( + + setIsOpen(true)} + onMouseLeave={() => setIsOpen(false)} + onClick={() => setIsOpen(true)} + > + {children} + + + ) +} diff --git a/dapp/src/constants/featureFlags.ts b/dapp/src/constants/featureFlags.ts index 2d2062a9e..ddc1ef2ee 100644 --- a/dapp/src/constants/featureFlags.ts +++ b/dapp/src/constants/featureFlags.ts @@ -18,6 +18,9 @@ const TVL_ENABLED = import.meta.env.VITE_FEATURE_FLAG_TVL_ENABLED === "true" const GATING_DAPP_ENABLED = import.meta.env.VITE_FEATURE_GATING_DAPP_ENABLED === "true" +const MOBILE_MODE_ENABLED = + import.meta.env.VITE_FEATURE_MOBILE_MODE_ENABLED === "true" + const featureFlags = { GAMIFICATION_ENABLED, OKX_WALLET_ENABLED, @@ -26,6 +29,7 @@ const featureFlags = { ACRE_POINTS_ENABLED, TVL_ENABLED, GATING_DAPP_ENABLED, + MOBILE_MODE_ENABLED, } export default featureFlags diff --git a/dapp/src/constants/screen.ts b/dapp/src/constants/screen.ts index 70538bd61..c57950789 100644 --- a/dapp/src/constants/screen.ts +++ b/dapp/src/constants/screen.ts @@ -1,3 +1,3 @@ -const MAX_MOBILE_SCREEN_WIDTH = "1023px" +const MAX_MOBILE_SCREEN_WIDTH = "600px" export default { MAX_MOBILE_SCREEN_WIDTH } diff --git a/dapp/src/hooks/index.ts b/dapp/src/hooks/index.ts index 0f3213d35..db36d2455 100644 --- a/dapp/src/hooks/index.ts +++ b/dapp/src/hooks/index.ts @@ -27,7 +27,6 @@ export { default as useMobileMode } from "./useMobileMode" export { default as useBitcoinRecoveryAddress } from "./useBitcoinRecoveryAddress" export { default as useIsFetchedWalletData } from "./useIsFetchedWalletData" export { default as useLocalStorage } from "./useLocalStorage" -export { default as useDetectReferral } from "./useDetectReferral" export { default as useReferral } from "./useReferral" export { default as useMats } from "./useMats" export { default as useIsEmbed } from "./useIsEmbed" diff --git a/dapp/src/hooks/useDetectReferral.ts b/dapp/src/hooks/useDetectReferral.ts deleted file mode 100644 index e32a457b6..000000000 --- a/dapp/src/hooks/useDetectReferral.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useEffect } from "react" -import useReferral from "./useReferral" - -export default function useDetectReferral() { - const { detectReferral } = useReferral() - - useEffect(() => { - detectReferral() - }, [detectReferral]) -} diff --git a/dapp/src/hooks/useInitApp.ts b/dapp/src/hooks/useInitApp.ts index e87e8955a..caf792595 100644 --- a/dapp/src/hooks/useInitApp.ts +++ b/dapp/src/hooks/useInitApp.ts @@ -4,7 +4,6 @@ import { useAccountsChangedOKX } from "./orangeKit/useAccountsChangedOKX" import { useInitDataFromSdk, useInitializeAcreSdk } from "./sdk" import { useSentry } from "./sentry" import useDetectEmbed from "./useDetectEmbed" -import useDetectReferral from "./useDetectReferral" import { useDisconnectWallet } from "./useDisconnectWallet" import { useFetchBTCPriceUSD } from "./useFetchBTCPriceUSD" @@ -13,7 +12,6 @@ export function useInitApp() { // useDetectThemeMode() useSentry() useDetectEmbed() - useDetectReferral() useInitializeAcreSdk() useInitDataFromSdk() useFetchBTCPriceUSD() diff --git a/dapp/src/hooks/useLocalStorage.ts b/dapp/src/hooks/useLocalStorage.ts index d3d03e990..73272cecd 100644 --- a/dapp/src/hooks/useLocalStorage.ts +++ b/dapp/src/hooks/useLocalStorage.ts @@ -1,4 +1,7 @@ -import { useLocalStorage as useRehooksLocalStorage } from "@rehooks/local-storage" +import { + useLocalStorage as useRehooksLocalStorage, + writeStorage, +} from "@rehooks/local-storage" export const parseLocalStorageValue = (value: string | null | undefined) => { if ( @@ -12,6 +15,8 @@ export const parseLocalStorageValue = (value: string | null | undefined) => { return value } +export { writeStorage } + export const getLocalStorageItem = (key: string): string | undefined => { const value = localStorage.getItem(key) return parseLocalStorageValue(value) diff --git a/dapp/src/hooks/useMobileMode.ts b/dapp/src/hooks/useMobileMode.ts index 25a491c1d..07e7de6a1 100644 --- a/dapp/src/hooks/useMobileMode.ts +++ b/dapp/src/hooks/useMobileMode.ts @@ -2,13 +2,11 @@ import { screen } from "#/constants" import { userAgent as userAgentUtils } from "#/utils" import { useMediaQuery } from "@chakra-ui/react" import { useMemo } from "react" -import useIsEmbed from "./useIsEmbed" const { MAX_MOBILE_SCREEN_WIDTH } = screen const { getDeviceDetect } = userAgentUtils const useMobileMode = () => { - const { isEmbed } = useIsEmbed() const [isMobileScreen] = useMediaQuery( `(max-width: ${MAX_MOBILE_SCREEN_WIDTH})`, ) @@ -30,7 +28,7 @@ const useMobileMode = () => { return isMobileDevice() }, [isMobileScreen]) - return !isEmbed && isMobile + return isMobile } export default useMobileMode diff --git a/dapp/src/hooks/useReferral.ts b/dapp/src/hooks/useReferral.ts index d919850e9..4e6f7566c 100644 --- a/dapp/src/hooks/useReferral.ts +++ b/dapp/src/hooks/useReferral.ts @@ -1,52 +1,23 @@ import { env } from "#/constants" -import { referralProgram } from "#/utils" - import { useCallback, useMemo } from "react" -import { MODAL_TYPES } from "#/types" -import useIsEmbed from "./useIsEmbed" -import useLocalStorage from "./useLocalStorage" -import { useModal } from "./useModal" +import useLocalStorage, { writeStorage } from "./useLocalStorage" type UseReferralReturn = { referral: number | null - detectReferral: () => void resetReferral: () => void } +const LOCAL_STORAGE_KEY = "acre.referral" + +export const writeReferral = (value: string) => { + writeStorage(LOCAL_STORAGE_KEY, value) +} + export default function useReferral(): UseReferralReturn { const [referral, setReferral] = useLocalStorage( - "acre.referral", + LOCAL_STORAGE_KEY, env.REFERRAL, ) - const { openModal } = useModal() - const { isEmbed, embeddedApp } = useIsEmbed() - - const detectReferral = useCallback(() => { - const param = referralProgram.getReferralFromURL() - - if (isEmbed && embeddedApp) { - setReferral(referralProgram.getReferralByEmbeddedApp(embeddedApp)) - return - } - - if (param === null) { - setReferral(env.REFERRAL) - return - } - - const convertedReferral = Number(param) - - if (referralProgram.isValidReferral(convertedReferral)) { - setReferral(convertedReferral) - } else { - console.error("Incorrect referral") - setReferral(null) - openModal(MODAL_TYPES.UNEXPECTED_ERROR, { - withCloseButton: false, - closeOnEsc: false, - }) - } - }, [isEmbed, embeddedApp, openModal, setReferral]) const resetReferral = useCallback(() => { setReferral(env.REFERRAL) @@ -54,10 +25,9 @@ export default function useReferral(): UseReferralReturn { return useMemo( () => ({ - detectReferral, resetReferral, referral, }), - [detectReferral, resetReferral, referral], + [resetReferral, referral], ) } diff --git a/dapp/src/hooks/useWallet.ts b/dapp/src/hooks/useWallet.ts index 2786a4472..504b0a453 100644 --- a/dapp/src/hooks/useWallet.ts +++ b/dapp/src/hooks/useWallet.ts @@ -8,6 +8,7 @@ import { useDisconnect, } from "wagmi" import { orangeKit } from "#/utils" +import sentry from "#/sentry" import { OnErrorCallback, OrangeKitConnector, @@ -75,6 +76,7 @@ export function useWallet(): UseWalletReturn { dispatch(setAddress(bitcoinAddress)) setAddressInLocalStorage(bitcoinAddress) + sentry.setUser(bitcoinAddress) }, }, }) @@ -86,6 +88,7 @@ export function useWallet(): UseWalletReturn { dispatch(setAddress(undefined)) removeAddressFromLocalStorage() resetWalletState() + sentry.setUser(undefined) }, }, }) @@ -124,6 +127,7 @@ export function useWallet(): UseWalletReturn { dispatch(setAddress(bitcoinAddress)) setAddressInLocalStorage(bitcoinAddress) + sentry.setUser(bitcoinAddress) }, }, queryClient, diff --git a/dapp/src/pages/DashboardPage/AcreTVLProgress.tsx b/dapp/src/pages/DashboardPage/AcreTVLProgress.tsx index d9b8078ab..51c645153 100644 --- a/dapp/src/pages/DashboardPage/AcreTVLProgress.tsx +++ b/dapp/src/pages/DashboardPage/AcreTVLProgress.tsx @@ -33,7 +33,7 @@ export function AcreTVLProgress(props: AcreTVLProgressProps) { return ( - + @@ -59,7 +59,7 @@ export function AcreTVLProgress(props: AcreTVLProgressProps) { 2} />
-
+
) } diff --git a/dapp/src/pages/DashboardPage/BeehiveCard.tsx b/dapp/src/pages/DashboardPage/BeehiveCard.tsx index bba103729..9168d0b5b 100644 --- a/dapp/src/pages/DashboardPage/BeehiveCard.tsx +++ b/dapp/src/pages/DashboardPage/BeehiveCard.tsx @@ -42,14 +42,16 @@ export default function BeehiveCard(props: CardProps) { - - - - Total collected mats from Mezo - - - - + + + Total collected mats from Mezo + + + {data && (
{numberToLocaleString(data.totalMats)} diff --git a/dapp/src/pages/DashboardPage/PositionDetails.tsx b/dapp/src/pages/DashboardPage/PositionDetails.tsx index c05c6163a..f5b2e30b4 100644 --- a/dapp/src/pages/DashboardPage/PositionDetails.tsx +++ b/dapp/src/pages/DashboardPage/PositionDetails.tsx @@ -6,6 +6,7 @@ import { useTransactionModal, useStatistics, useWallet, + useMobileMode, } from "#/hooks" import { ACTION_FLOW_TYPES } from "#/types" import { @@ -27,7 +28,7 @@ const isWithdrawalFlowEnabled = featureFlags.WITHDRAWALS_ENABLED const buttonStyles: ButtonProps = { size: "lg", flex: 1, - w: { base: "auto", lg: 40 }, + w: 40, fontWeight: "bold", lineHeight: 6, px: 7, @@ -41,17 +42,21 @@ export default function PositionDetails() { const openDepositModal = useTransactionModal(ACTION_FLOW_TYPES.STAKE) const openWithdrawModal = useTransactionModal(ACTION_FLOW_TYPES.UNSTAKE) const activitiesCount = useAllActivitiesCount() + const isMobileMode = useMobileMode() const { tvl } = useStatistics() const { isConnected } = useWallet() + const isDisabledForMobileMode = + isMobileMode && !featureFlags.MOBILE_MODE_ENABLED + return ( {/* TODO: Component should be moved to `CardHeader` */} - Your deposit + Your balance {/* TODO: Uncomment when position will be implemented */} {/* {positionPercentage && ( - + + {isConnected && activitiesCount > 0 && ( - + diff --git a/dapp/src/pages/DashboardPage/TransactionHistory/TransactionTable.tsx b/dapp/src/pages/DashboardPage/TransactionHistory/TransactionTable.tsx index a29242694..b6ed02c49 100644 --- a/dapp/src/pages/DashboardPage/TransactionHistory/TransactionTable.tsx +++ b/dapp/src/pages/DashboardPage/TransactionHistory/TransactionTable.tsx @@ -13,7 +13,7 @@ import { displayBlockTimestamp, getActivityTimestamp } from "#/utils" import { Activity } from "#/types" import BlockExplorerLink from "#/components/shared/BlockExplorerLink" import { IconArrowUpRight } from "@tabler/icons-react" -import { useActivities } from "#/hooks" +import { useActivities, useMobileMode } from "#/hooks" import { semanticTokens } from "#/theme/utils" import EstimatedDuration from "./EstimatedDuration" @@ -21,9 +21,10 @@ const BLOCK_EXPLORER_CELL_MIN_WIDTH = 16 export default function TransactionTable() { const activities = useActivities() + const isMobileMode = useMobileMode() return ( - + {(pageData: Activity[]) => pageData.map((activity) => ( diff --git a/dapp/src/pages/DashboardPage/index.tsx b/dapp/src/pages/DashboardPage/index.tsx index 6f28092ec..e0fb11bc2 100644 --- a/dapp/src/pages/DashboardPage/index.tsx +++ b/dapp/src/pages/DashboardPage/index.tsx @@ -9,28 +9,44 @@ import AcrePointsTemplateCard from "./AcrePointsTemplateCard" import BeehiveCard from "./BeehiveCard" import { AcreTVLProgress } from "./AcreTVLProgress" -const TEMPLATE_AREAS = ` - ${featureFlags.TVL_ENABLED ? '"tvl tvl"' : ""} - "dashboard acre-points" - "dashboard beehive" - "dashboard useful-links" -` - -const GRID_TEMPLATE_ROWS = `${featureFlags.TVL_ENABLED ? "auto" : ""} auto auto 1fr` - export default function DashboardPage() { useTriggerConnectWalletModal() return ( {featureFlags.TVL_ENABLED && } @@ -40,12 +56,12 @@ export default function DashboardPage() { {/* */} {featureFlags.ACRE_POINTS_ENABLED ? ( - + ) : ( - + )} - + ) } diff --git a/dapp/src/router/index.tsx b/dapp/src/router/index.tsx index 9d6a42b0f..351a8504e 100644 --- a/dapp/src/router/index.tsx +++ b/dapp/src/router/index.tsx @@ -12,6 +12,8 @@ import AccessPage from "#/pages/AccessPage" import WelcomePage from "#/pages/WelcomePage" import welcomePageLoader from "#/pages/WelcomePage/loader" import accessPageLoader from "#/pages/AccessPage/loader" +import { writeReferral } from "#/hooks/useReferral" +import { env } from "#/constants" import { routerPath } from "./path" const mainLayoutLoader: LoaderFunction = ({ request }) => { @@ -32,6 +34,24 @@ export const router = createBrowserRouter([ { path: "/", element: , + loader: ({ request }) => { + // TODO: display the error page/modal when the referral is invalid. + const referralCodeFromUrl = referralProgram.getReferralFromURL() + + const referralCode = referralProgram.isValidReferral(referralCodeFromUrl) + ? referralCodeFromUrl! + : env.REFERRAL + + writeReferral(referralCode.toString()) + + const embedApp = referralProgram.getEmbeddedApp(request.url) + if (referralProgram.isEmbedApp(embedApp)) { + writeReferral( + referralProgram.getReferralByEmbeddedApp(embedApp).toString(), + ) + } + return null + }, children: [ { index: true, diff --git a/dapp/src/sentry.test.ts b/dapp/src/sentry.test.ts new file mode 100644 index 000000000..7543d8d1d --- /dev/null +++ b/dapp/src/sentry.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it, vi } from "vitest" +import * as Sentry from "@sentry/react" +import sentry from "./sentry" + +describe("sentry", () => { + describe("setUser", () => { + vi.mock("@sentry/react") + + const testCases = [ + { bitcoinAddress: undefined, expectedResult: null }, + { bitcoinAddress: "", expectedResult: null }, + { bitcoinAddress: " ", expectedResult: null }, + { + bitcoinAddress: "17VZNX1SN5NtKa8UQFxwQbFeFc3iqRYhem", + expectedResult: { id: "1f520a9757" }, + }, + { + bitcoinAddress: "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", + expectedResult: { id: "6cd42dab02" }, + }, + { + bitcoinAddress: "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4", + expectedResult: { id: "6cd42dab02" }, + }, + ] + + describe.each(testCases)( + "when address is $bitcoinAddress", + ({ bitcoinAddress, expectedResult }) => { + it("should set expected user in Sentry", () => { + sentry.setUser(bitcoinAddress) + + expect(Sentry.setUser).toHaveBeenCalledWith(expectedResult) + }) + }, + ) + }) +}) diff --git a/dapp/src/sentry.ts b/dapp/src/sentry.ts new file mode 100644 index 000000000..f3707ede4 --- /dev/null +++ b/dapp/src/sentry.ts @@ -0,0 +1,50 @@ +import * as Sentry from "@sentry/react" +import { sha256, toUtf8Bytes } from "ethers" + +const initialize = (dsn: string) => { + Sentry.init({ + dsn, + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.captureConsoleIntegration({ levels: ["error"] }), + Sentry.extraErrorDataIntegration(), + Sentry.httpClientIntegration(), + ], + // Attach stacktrace to errors logged by `console.error`. This is useful for + // the `captureConsoleIntegration` integration. + attachStacktrace: true, + // Performance Monitoring + tracesSampleRate: 0.1, + }) +} + +/** + * Sets the user in Sentry with an ID from hashed Bitcoin address. + * The Bitcoin address is first converted to lowercase and then hashed using SHA-256. + * The resulting hash is then converted to a hexadecimal string and the first 10 + * characters are set as the user ID. + * + * @param bitcoinAddress - The Bitcoin address of the user. If undefined, the user + * is set to null in Sentry. + */ +const setUser = (bitcoinAddress: string | undefined) => { + if (!bitcoinAddress) { + Sentry.setUser(null) + return + } + + const hashedBitcoinAddress = sha256(toUtf8Bytes(bitcoinAddress.toLowerCase())) + // Remove the 0x prefix and take the first 10 characters. + const id = hashedBitcoinAddress.slice(2, 12) + + Sentry.setUser({ id }) +} + +const captureException = (exception: unknown) => + Sentry.captureException(exception) + +export default { + initialize, + setUser, + captureException, +} diff --git a/dapp/src/sentry/index.ts b/dapp/src/sentry/index.ts deleted file mode 100644 index 0bb08159d..000000000 --- a/dapp/src/sentry/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import * as Sentry from "@sentry/react" - -const initialize = (dsn: string) => { - Sentry.init({ - dsn, - integrations: [ - Sentry.browserTracingIntegration(), - Sentry.captureConsoleIntegration({ levels: ["error"] }), - ], - // Attach stacktrace to errors logged by `console.error`. This is useful for - // the `captureConsoleIntegration` integration. - attachStacktrace: true, - // Performance Monitoring - tracesSampleRate: 0.1, - }) -} - -const captureException = (exception: unknown) => - Sentry.captureException(exception) - -export default { - initialize, - captureException, -} diff --git a/dapp/src/theme/AcreTVLProgress.ts b/dapp/src/theme/AcreTVLProgress.ts index 622b34263..16d5524bc 100644 --- a/dapp/src/theme/AcreTVLProgress.ts +++ b/dapp/src/theme/AcreTVLProgress.ts @@ -27,7 +27,9 @@ const containerStyles = defineStyle({ const wrapperStyles = defineStyle({ px: 5, py: 6, - gap: "6.875rem", // 110px + display: "flex", + flexDirection: { base: "column", sm: "row" }, + gap: { base: 5, md: "6.875rem" }, // 110px }) const contentWrapperStyles = defineStyle({ gridAutoFlow: "column", diff --git a/dapp/src/theme/Footer.ts b/dapp/src/theme/Footer.ts index 8cf4f3229..3aced57fe 100644 --- a/dapp/src/theme/Footer.ts +++ b/dapp/src/theme/Footer.ts @@ -19,7 +19,7 @@ const wrapperStyles = defineStyle({ gap: { base: 4, xl: 10 }, maxW: "120rem", // 1920px py: 3, - px: { base: 10, xl: 30 }, + px: { base: 4, md: "2.5rem", xl: 30 }, mx: "auto", }) diff --git a/dapp/src/theme/Modal.ts b/dapp/src/theme/Modal.ts index 7e6f1fc66..bab2047b2 100644 --- a/dapp/src/theme/Modal.ts +++ b/dapp/src/theme/Modal.ts @@ -2,23 +2,24 @@ import { modalAnatomy as parts } from "@chakra-ui/anatomy" import { createMultiStyleConfigHelpers, defineStyle } from "@chakra-ui/react" const baseStyleContainer = defineStyle({ - px: 8, + px: { base: 3, sm: 8 }, }) const baseStyleDialog = defineStyle({ - marginTop: "var(--chakra-space-modal_shift)", + marginTop: { base: 12, sm: "var(--chakra-space-modal_shift)" }, marginBottom: 8, boxShadow: "none", borderRadius: "xl", + p: { base: 5, sm: 0 }, bg: "gold.100", border: "none", }) const baseCloseButton = defineStyle({ - top: -7, - right: -7, - boxSize: 7, - rounded: "100%", + top: { base: 3, sm: -7 }, + right: { base: 3, sm: -7 }, + boxSize: { sm: 7 }, + rounded: { sm: "100%" }, bg: "opacity.white.5", _hover: { @@ -37,8 +38,8 @@ const baseStyleHeader = defineStyle({ fontSize: "xl", lineHeight: "xl", fontWeight: "bold", - pt: 10, - px: 10, + pt: { sm: 10 }, + px: { sm: 10 }, pb: 8, }) @@ -50,15 +51,15 @@ const baseStyleBody = defineStyle({ alignItems: "center", gap: 6, pt: 0, - px: 8, - pb: 10, + px: { base: 0, sm: 10 }, + pb: { base: 0, sm: 10 }, }) const baseStyleFooter = defineStyle({ flexDirection: "column", gap: 6, - px: 8, - pb: 10, + px: { base: 0, sm: 8 }, + pb: { base: 0, sm: 10 }, }) const multiStyleConfig = createMultiStyleConfigHelpers(parts.keys) diff --git a/dapp/src/utils/activities.ts b/dapp/src/utils/activities.ts index 7349a985b..a6d1716da 100644 --- a/dapp/src/utils/activities.ts +++ b/dapp/src/utils/activities.ts @@ -21,12 +21,31 @@ export function getEstimatedDuration( amount: bigint, type: ActivityType, ): string { + // Withdrawal duration is related to the tBTC redemption process, which takes + // approximately 5 - 7 hours. We use the average value of 6 hours. if (isWithdrawType(type)) return "6 hours" - if (amount < MIN_LIMIT_VALUE_DURATION) return "1 hour" - - if (amount >= MIN_LIMIT_VALUE_DURATION && amount < MAX_LIMIT_VALUE_DURATION) - return "2 hours" - + // Deposit duration is related to the tBTC minting process, which varies based + // on the amount of BTC deposited. + // Each threshold requires a different number of Bitcoin transaction confirmations: + // <0.1 BTC: 1 Bitcoin block confirmation (~10 minutes), + // >=0.1 BTC and <1 BTC: 3 Bitcoin block confirmations (~30 minutes), + // >=1 BTC: 6 Bitcoin block confirmations (~60 minutes). + // The duration of the transaction minting process depends on the Bitcoin network + // congestion, and the fee paid by the user. + // + // After the required number of Bitcoin block confirmations, the tBTC optimistic + // minting process starts. The optimistic minting process takes approximately + // 1 hour to complete. + // After optimistic minting is completed, the Acre bots will finalize the deposit + // in no more than 10 minutes. + // + // We round the estimated duration up to the nearest hour. + // + // For <0.1 BTC estimated duration is around 1 hour 20 minutes. + if (amount < MIN_LIMIT_VALUE_DURATION) return "2 hours" + // For <1 BTC estimated duration is around 1 hours 40 minutes. + if (amount < MAX_LIMIT_VALUE_DURATION) return "2 hours" + // For >=1 BTC estimated duration is around 2 hours 10 minutes. return "3 hours" } diff --git a/dapp/src/utils/orangekit/index.ts b/dapp/src/utils/orangekit/index.ts index 18f355df4..af5317b7c 100644 --- a/dapp/src/utils/orangekit/index.ts +++ b/dapp/src/utils/orangekit/index.ts @@ -107,6 +107,33 @@ async function verifySignInWithWalletMessage( return result.data } +/** + * Finds the extended public key (xpub) of the user's account from URL. Users + * can be redirected to the exact app in the Ledger Live application. One of the + * parameters passed via URL is `accountId` - the ID of the user's account in + * Ledger Live. + * @see https://developers.ledger.com/docs/ledger-live/exchange/earn/liveapp#url-parameters-for-direct-navigation + * + * @param {string} url Request url + * @returns The extended public key (xpub) of the user's account if the search + * parameter `accountId` exists in the URL. Otherwise `undefined`. + */ +function findXpubFromUrl(url: string): string | undefined { + const parsedUrl = new URL(url) + + const accountId = parsedUrl.searchParams.get("accountId") + + if (!accountId) return undefined + + // The fourth value separated by `:` is extended public key. See the + // account ID template: `js:2:bitcoin_testnet::`. + const xpubFromAccountId = accountId.split(":")[3] + + if (!xpubFromAccountId) return undefined + + return xpubFromAccountId +} + export default { getWalletInfo, isWalletInstalled, @@ -119,4 +146,5 @@ export default { isWalletConnectionRejectedError, verifySignInWithWalletMessage, getOrangeKitLedgerLiveConnector, + findXpubFromUrl, } diff --git a/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts b/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts index 7dab9c5ad..316e628a4 100644 --- a/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts +++ b/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts @@ -58,9 +58,12 @@ function numberToValidHexString(value: number): string { return `0x${hex}` } -export type AcreLedgerLiveBitcoinProviderOptions = { - tryConnectToAddress: string | undefined -} +export type AcreLedgerLiveBitcoinProviderOptions = + | { + tryConnectToAddress?: string + tryConnectToAccountByXpub?: never + } + | { tryConnectToAddress?: never; tryConnectToAccountByXpub?: string } /** * Ledger Live Wallet API Bitcoin Provider. @@ -90,6 +93,7 @@ export default class AcreLedgerLiveBitcoinProvider network: BitcoinNetwork, options: AcreLedgerLiveBitcoinProviderOptions = { tryConnectToAddress: undefined, + tryConnectToAccountByXpub: undefined, }, ) { const windowMessageTransport = new WindowMessageTransport() @@ -115,6 +119,7 @@ export default class AcreLedgerLiveBitcoinProvider walletApiClient: WalletAPIClient, options: AcreLedgerLiveBitcoinProviderOptions = { tryConnectToAddress: undefined, + tryConnectToAccountByXpub: undefined, }, ) { this.#network = network @@ -140,12 +145,24 @@ export default class AcreLedgerLiveBitcoinProvider currencyIds, }) - if (this.#options.tryConnectToAddress) { + if ( + this.#options.tryConnectToAddress || + this.#options.tryConnectToAccountByXpub + ) { for (let i = 0; i < accounts.length; i += 1) { const acc = accounts[i] + if ( + this.#options.tryConnectToAccountByXpub && + // eslint-disable-next-line no-await-in-loop + (await this.#walletApiClient.bitcoin.getXPub(acc.id)) === + this.#options.tryConnectToAccountByXpub + ) { + this.#account = acc + break + } + // eslint-disable-next-line no-await-in-loop const address = await this.#getAddress(acc.id) - if (address === this.#options.tryConnectToAddress) { this.#account = acc break diff --git a/dapp/src/utils/referralProgram.ts b/dapp/src/utils/referralProgram.ts index 36f48b9a3..82a3bfbc5 100644 --- a/dapp/src/utils/referralProgram.ts +++ b/dapp/src/utils/referralProgram.ts @@ -9,9 +9,23 @@ const EMBEDDED_APP_TO_REFERRAL: Record = { "ledger-live": 2083, } -const isValidReferral = (value: number) => { - const isInteger = Number.isInteger(value) - return isInteger && value >= 0 && value <= MAX_UINT16 +const isValidReferral = (value: unknown) => { + let valueAsNumber: number | undefined + + if (typeof value === "string") { + // Only digits w/o leading zeros. + const isNumber = /^(?:[1-9][0-9]*|0)$/.test(value) + valueAsNumber = isNumber ? Number(value) : undefined + } else if (typeof value === "number") { + valueAsNumber = value + } + + return ( + !!valueAsNumber && + Number.isInteger(valueAsNumber) && + valueAsNumber >= 0 && + valueAsNumber <= MAX_UINT16 + ) } const getReferralFromURL = () => diff --git a/dapp/src/utils/tests/activities.test.ts b/dapp/src/utils/tests/activities.test.ts new file mode 100644 index 000000000..fcafa84a7 --- /dev/null +++ b/dapp/src/utils/tests/activities.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest" +import { getEstimatedDuration } from "../activities" + +describe("Utils functions for activities", () => { + describe("getEstimatedDuration", () => { + describe("withdraw", () => { + describe.each([ + // 0.01 BTC + { value: 0.01, expectedResult: "6 hours" }, + // 0.1 BTC + { value: 0.1, expectedResult: "6 hours" }, + // 1 BTC + { value: 1, expectedResult: "6 hours" }, + // 10 BTC + { value: 10, expectedResult: "6 hours" }, + ])("when it is $value BTC", ({ value, expectedResult }) => { + it(`should return ${expectedResult}`, () => { + expect(getEstimatedDuration(BigInt(value * 1e8), "withdraw")).toEqual( + expectedResult, + ) + }) + }) + }) + + describe("deposit", () => { + describe.each([ + // 0.0001 BTC + { value: 0.0001, expectedResult: "2 hours" }, + // 0.001 BTC + { value: 0.001, expectedResult: "2 hours" }, + // 0.01 BTC + { value: 0.01, expectedResult: "2 hours" }, + // 0.09 BTC + { value: 0.09, expectedResult: "2 hours" }, + // 0.1 BTC + { value: 0.1, expectedResult: "2 hours" }, + // 0.9 BTC + { value: 0.9, expectedResult: "2 hours" }, + // 1 BTC + { value: 1, expectedResult: "3 hours" }, + // 10 BTC + { value: 10, expectedResult: "3 hours" }, + ])("when it is $value BTC", ({ value, expectedResult }) => { + it(`should return ${expectedResult}`, () => { + expect(getEstimatedDuration(BigInt(value * 1e8), "deposit")).toEqual( + expectedResult, + ) + }) + }) + }) + }) +}) diff --git a/dapp/src/vite-env.d.ts b/dapp/src/vite-env.d.ts index 53f7175b8..564bf6a80 100644 --- a/dapp/src/vite-env.d.ts +++ b/dapp/src/vite-env.d.ts @@ -17,6 +17,7 @@ interface ImportMetaEnv { readonly VITE_SUBGRAPH_API_KEY: string readonly VITE_LATEST_COMMIT_HASH: string readonly VITE_ACRE_API_ENDPOINT: string + readonly VITE_FEATURE_MOBILE_MODE_ENABLED: string } interface ImportMeta { diff --git a/dapp/src/wagmiConfig.ts b/dapp/src/wagmiConfig.ts index 19d1cf4e4..972a38fda 100644 --- a/dapp/src/wagmiConfig.ts +++ b/dapp/src/wagmiConfig.ts @@ -5,6 +5,7 @@ import { env } from "./constants" import { getLastUsedBtcAddress } from "./hooks/useLastUsedBtcAddress" import referralProgram, { EmbedApp } from "./utils/referralProgram" import { orangeKit } from "./utils" +import { AcreLedgerLiveBitcoinProviderOptions } from "./utils/orangekit/ledger-live/bitcoin-provider" const isTestnet = env.USE_TESTNET const CHAIN_ID = isTestnet ? sepolia.id : mainnet.id @@ -34,12 +35,19 @@ async function getWagmiConfig() { let createEmbedConnectorFn const embeddedApp = referralProgram.getEmbeddedApp() if (referralProgram.isEmbedApp(embeddedApp)) { + const lastUsedBtcAddress = getLastUsedBtcAddress() + const xpub = orangeKit.findXpubFromUrl(window.location.href) + const ledgerLiveConnectorOptions: AcreLedgerLiveBitcoinProviderOptions = + xpub + ? { tryConnectToAccountByXpub: xpub } + : { + tryConnectToAddress: lastUsedBtcAddress, + } + const orangeKitLedgerLiveConnector = orangeKit.getOrangeKitLedgerLiveConnector({ ...connectorConfig, - options: { - tryConnectToAddress: getLastUsedBtcAddress(), - }, + options: ledgerLiveConnectorOptions, }) const embedConnectorsMap: Record< diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf2c53ac6..53c684f24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,8 +39,8 @@ importers: specifier: 1.6.0 version: 1.6.0 '@orangekit/react': - specifier: 1.0.0-beta.33 - version: 1.0.0-beta.33(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.0(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) + specifier: 1.0.0-beta.34 + version: 1.0.0-beta.34(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.0(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8) '@orangekit/sign-in-with-wallet': specifier: 1.0.0-beta.6 version: 1.0.0-beta.6(bech32@2.0.0)(ethers@6.13.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) @@ -172,8 +172,8 @@ importers: specifier: 2.5.0-dev.3 version: 2.5.0-dev.3(@keep-network/keep-core@1.8.1-dev.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@orangekit/sdk': - specifier: 1.0.0-beta.18 - version: 1.0.0-beta.18(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + specifier: 1.0.0-beta.19 + version: 1.0.0-beta.19(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) ethers: specifier: 6.10.0 version: 6.10.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -2692,11 +2692,11 @@ packages: '@orangekit/contracts@1.0.0-beta.3': resolution: {integrity: sha512-xP1Oz/JzuHypg5DcsHayINhFSL5M/tCmRP/stmNAvjeebXhrKuALKAHWn98H0Mo3QfYYrz8UcltQPeH6+68n6A==} - '@orangekit/react@1.0.0-beta.33': - resolution: {integrity: sha512-b5VMFB2tZad86FNdtylwsKaXlXlrSjY9cIMsaJMUh7EEWEoVvtndbCsHrISKbspm7y+S5nNmGlE3WcQvR8RWWw==} + '@orangekit/react@1.0.0-beta.34': + resolution: {integrity: sha512-/PTfYcu/BU4ssltIuy+AKqbaGzue+MEJHwd7+4aZhXhwJ+/xrYo3mJ1tSwj6K8lAJCl/N25vq7zNJgVgjLK1WQ==} - '@orangekit/sdk@1.0.0-beta.18': - resolution: {integrity: sha512-031JkI0F8gdGzj/7sKbJ52k0Xj/E4uwp/O0SWFDCXyZC0vwtjlw3ItAaZz+nIj820b6dSM6pjL+P7um3m9AK3Q==} + '@orangekit/sdk@1.0.0-beta.19': + resolution: {integrity: sha512-sIqzu3QTb0WkB4Ir4zHX4tP7hf+uZBXX60q/ww6UzwPjoqGL3dOdui59lTZSNygdMbydLE3y9FSNZPNkQvm+gA==} '@orangekit/sign-in-with-wallet-parser@1.0.0-beta.6': resolution: {integrity: sha512-Yi6ohSJV4/Ovrq5c7jD+kPE8pZxLhWtFbZjKRwUW8JL60P/tcyT5o0etul0reqcY2iBlIo5aoC2Hh0noRGl86w==} @@ -3113,6 +3113,10 @@ packages: peerDependencies: ethers: 5.4.0 + '@safe-global/safe-core-sdk-types@4.1.1': + resolution: {integrity: sha512-5NIWG7OjV+C5iquux0yPcu8SHUzg1qJXJ/jAQcPwXGTC7ZVsFawnR43/l2Vg9zEwf0RF0xTm3W8DXkaBYORiEQ==} + deprecated: 'WARNING: This project has been renamed to @safe-global/types-kit. Please, migrate from @safe-global/safe-core-sdk-types@5.1.0 to @safe-global/types-kit@1.0.0.' + '@safe-global/safe-core-sdk-types@5.0.1': resolution: {integrity: sha512-xIlHZ9kaAIwEhR0OY0i2scdcQyrc0tDJ+eZZ04lhvg81cgYLY1Z5wfJQqazR2plPT1Hz0A9C79jYdUVvzoF/tw==} @@ -14831,9 +14835,9 @@ snapshots: transitivePeerDependencies: - ethers - '@orangekit/react@1.0.0-beta.33(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.0(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)': + '@orangekit/react@1.0.0-beta.34(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.0(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)': dependencies: - '@orangekit/sdk': 1.0.0-beta.18(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@orangekit/sdk': 1.0.0-beta.19(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) '@rainbow-me/rainbowkit': 2.0.2(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(viem@2.8.16(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.5.12(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.0(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(rollup@4.18.0)(typescript@5.4.5)(utf-8-validate@5.0.10)(viem@2.8.16(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) ethers: 6.12.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) react: 18.3.1 @@ -14870,13 +14874,14 @@ snapshots: - utf-8-validate - zod - '@orangekit/sdk@1.0.0-beta.18(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': + '@orangekit/sdk@1.0.0-beta.19(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@gelatonetwork/relay-sdk': 5.5.6(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@noble/curves': 1.4.0 '@noble/hashes': 1.4.0 '@orangekit/contracts': 1.0.0-beta.3(ethers@6.12.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@safe-global/protocol-kit': 3.1.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@safe-global/safe-core-sdk-types': 4.1.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10) bitcoinjs-lib: 6.1.5 ethers: 6.12.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: @@ -15492,6 +15497,18 @@ snapshots: dependencies: ethers: 6.12.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@safe-global/safe-core-sdk-types@4.1.1(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)': + dependencies: + '@safe-global/safe-deployments': 1.37.0 + ethers: 6.10.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + web3-core: 1.10.4(encoding@0.1.13) + web3-utils: 1.10.4 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + '@safe-global/safe-core-sdk-types@5.0.1(typescript@5.4.5)(zod@3.23.8)': dependencies: abitype: 1.0.2(typescript@5.4.5)(zod@3.23.8) diff --git a/sdk/package.json b/sdk/package.json index 2a79b6f5d..9a67628dc 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -27,7 +27,7 @@ "dependencies": { "@acre-btc/contracts": "workspace:*", "@keep-network/tbtc-v2.ts": "2.5.0-dev.3", - "@orangekit/sdk": "1.0.0-beta.18", + "@orangekit/sdk": "1.0.0-beta.19", "ethers": "6.10.0", "ethers-v5": "npm:ethers@^5.5.2" } diff --git a/sdk/src/acre.ts b/sdk/src/acre.ts index d1a84eabd..652dc5a25 100644 --- a/sdk/src/acre.ts +++ b/sdk/src/acre.ts @@ -85,7 +85,9 @@ class Acre { const orangeKit = await OrangeKitSdk.init( Number(ethereumChainId), ethereumRpcUrl, - new GelatoTransactionSender(gelatoApiKey), + new GelatoTransactionSender(gelatoApiKey, { + backoffRetrier: { retries: 7, backoffStepMs: 3000 }, + }), ) const contracts = await getEthereumContracts(