From e6ea02e57a8326806439b22d2236299f6e6fd93b Mon Sep 17 00:00:00 2001 From: Kieran Allen Date: Thu, 8 Feb 2024 10:04:48 +0000 Subject: [PATCH 1/4] fix: update countdown and refresh rates code --- .../Swap2/Form/FormRates/SectionRate.tsx | 9 ++---- .../exchange/Swap2/Form/FormRates/index.tsx | 20 ++++++------ .../Swap2/Form/Migrations/SwapMigrationUI.tsx | 7 +--- .../exchange/Swap2/Form/Rates/Countdown.tsx | 31 ++++-------------- .../exchange/Swap2/Form/Rates/index.tsx | 10 +++--- .../Swap2/Form/hooks/useRefreshRates.ts | 32 ------------------- .../screens/exchange/Swap2/Form/index.tsx | 8 ----- libs/ledger-live-common/package.json | 1 + .../exchange/swap/hooks/useSwapTransaction.ts | 3 +- .../swap/hooks/v5/useProviderRates.ts | 23 ++++++++++++- .../src/exchange/swap/types.ts | 1 + pnpm-lock.yaml | 13 ++++++++ 12 files changed, 62 insertions(+), 96 deletions(-) delete mode 100644 apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/hooks/useRefreshRates.ts diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/FormRates/SectionRate.tsx b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/FormRates/SectionRate.tsx index 954eded4a94e..a2717986655f 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/FormRates/SectionRate.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/FormRates/SectionRate.tsx @@ -11,8 +11,7 @@ export type SectionRateProps = { ratesState: RatesReducerState; fromCurrency: SwapSelectorStateType["currency"]; toCurrency: SwapSelectorStateType["currency"]; - refreshTime: number; - countdown: boolean; + countdownSecondsToRefresh: number | undefined; }; const SectionRate = ({ @@ -20,8 +19,7 @@ const SectionRate = ({ fromCurrency, toCurrency, ratesState, - refreshTime, - countdown, + countdownSecondsToRefresh, }: SectionRateProps) => { const rates = ratesState.value; @@ -33,8 +31,7 @@ const SectionRate = ({ toCurrency, rates, provider, - refreshTime, - countdown, + countdownSecondsToRefresh, }} /> diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/FormRates/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/FormRates/index.tsx index 504dc3772d9d..9ad9a7b7cc40 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/FormRates/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/FormRates/index.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React from "react"; import styled from "styled-components"; import SectionRate from "./SectionRate"; import { SwapDataType } from "@ledgerhq/live-common/exchange/swap/types"; @@ -10,28 +10,26 @@ const Form = styled.section` `; type SwapFormProvidersProps = { swap: SwapDataType; + countdownSecondsToRefresh: number | undefined; provider?: string; - refreshTime: number; - countdown: boolean; }; -const SwapFormProviders = ({ swap, provider, refreshTime, countdown }: SwapFormProvidersProps) => { +const SwapFormProviders = ({ + swap, + provider, + countdownSecondsToRefresh, +}: SwapFormProvidersProps) => { const { currency: fromCurrency } = swap.from; const { currency: toCurrency } = swap.to; - const updatedRatesState = useMemo(() => { - return swap.rates; - }, [swap.rates]); - return (
); diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Migrations/SwapMigrationUI.tsx b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Migrations/SwapMigrationUI.tsx index d7708722e4c1..2d04ffd91f3c 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Migrations/SwapMigrationUI.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Migrations/SwapMigrationUI.tsx @@ -22,8 +22,6 @@ type SwapMigrationUIProps = { pageState: ReturnType; swapTransaction: SwapTransactionType; provider?: string; - refreshTime: number; - countdown: boolean; // Demo 0 props disabled: boolean; onClick: () => void; @@ -37,8 +35,6 @@ export const SwapMigrationUI = (props: SwapMigrationUIProps) => { pageState, swapTransaction, provider, - refreshTime, - countdown, disabled, onClick, } = props; @@ -49,8 +45,7 @@ export const SwapMigrationUI = (props: SwapMigrationUIProps) => { ) : null; diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Rates/Countdown.tsx b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Rates/Countdown.tsx index 674f1cef07e8..25742ce14284 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Rates/Countdown.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Rates/Countdown.tsx @@ -1,40 +1,21 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { Trans } from "react-i18next"; import styled from "styled-components"; import Box from "~/renderer/components/Box"; import Text from "~/renderer/components/Text"; import AnimatedCountdown from "~/renderer/components/AnimatedCountdown"; import { formatCountdown } from "~/renderer/screens/exchange/Swap2/Form/Rates/utils/formatCountdown"; -import { RatesReducerState } from "@ledgerhq/live-common/exchange/swap/types"; +import { DEFAULT_SWAP_RATES_INTERVAL_MS } from "@ledgerhq/live-common/exchange/swap/const/timeout"; export type Props = { - rates: RatesReducerState["value"]; - refreshTime: number; + countdown: number; }; const CountdownText = styled(Text)` color: ${p => p.theme.colors.neutral.c70}; `; -export default function Countdown({ refreshTime, rates }: Props) { - const getSeconds = (time: number) => Math.round(time / 1000); - const [countdown, setCountdown] = useState(getSeconds(refreshTime)); - const [iconKey, setIconKey] = useState(0); - - useEffect(() => { - setIconKey(key => key + 1); - const startTime = new Date().getTime(); - setCountdown(getSeconds(refreshTime)); - const countdownInterval = window.setInterval(() => { - const now = new Date().getTime(); - const newCountdown = refreshTime + startTime - now; - setCountdown(getSeconds(newCountdown)); - }, 1000); - return () => { - clearInterval(countdownInterval); - }; - }, [rates, refreshTime]); - +export default function Countdown({ countdown }: Props) { return ( <> {countdown >= 0 ? ( @@ -42,8 +23,8 @@ export default function Countdown({ refreshTime, rates }: Props) { - - + + - {countdown && ( + {countdownSecondsToRefresh && ( - + )} diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/hooks/useRefreshRates.ts b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/hooks/useRefreshRates.ts deleted file mode 100644 index f6cb06f7c735..000000000000 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/hooks/useRefreshRates.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useRef, useMemo, useEffect } from "react"; -import { SwapDataType } from "@ledgerhq/live-common/exchange/swap/types"; -import { getRefreshTime } from "~/renderer/screens/exchange/Swap2/utils/getRefreshTime"; - -const useRefreshRates = ( - swap: SwapDataType, - { - pause, - }: { - pause: boolean; - }, -) => { - const refreshInterval = useRef(); - const refreshTime = useMemo(() => getRefreshTime(swap.rates?.value), [swap.rates?.value]); - - useEffect(() => { - clearTimeout(refreshInterval.current); - refreshInterval.current = setTimeout(() => { - if (!pause) { - swap.refetchRates(); - } - }, refreshTime); - return () => { - clearTimeout(refreshInterval.current); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pause, refreshTime, swap.rates?.value]); - - return refreshTime; -}; - -export default useRefreshRates; diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/index.tsx index 490d54219301..8fbb35c9a0a6 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/index.tsx @@ -26,7 +26,6 @@ import ExchangeDrawer from "./ExchangeDrawer/index"; import SwapFormSelectors from "./FormSelectors"; import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; import { accountToWalletAPIAccount } from "@ledgerhq/live-common/wallet-api/converters"; -import useRefreshRates from "./hooks/useRefreshRates"; import LoadingState from "./Rates/LoadingState"; import EmptyState from "./Rates/EmptyState"; import { AccountLike } from "@ledgerhq/types-live"; @@ -128,11 +127,6 @@ const SwapForm = () => { ); const { setDrawer } = React.useContext(context); - const pauseRefreshing = !!swapError || idleState; - const refreshTime = useRefreshRates(swapTransaction.swap, { - pause: pauseRefreshing, - }); - const getExchangeSDKParams = useCallback(() => { const { swap, transaction } = swapTransaction; const { to, from } = swap; @@ -477,8 +471,6 @@ const SwapForm = () => { pageState={pageState} swapTransaction={swapTransaction} provider={provider} - refreshTime={refreshTime} - countdown={!pauseRefreshing} // Demo 0 props disabled={!isSwapReady} onClick={onSubmit} diff --git a/libs/ledger-live-common/package.json b/libs/ledger-live-common/package.json index 3f2e0f45f5fa..d2bdc8db1174 100644 --- a/libs/ledger-live-common/package.json +++ b/libs/ledger-live-common/package.json @@ -244,6 +244,7 @@ "thor-devkit": "^2.0.6", "triple-beam": "^1.3.0", "tronweb": "^5.2.0", + "usehooks-ts": "^2.13.0", "utility-types": "^3.10.0", "varuint-bitcoin": "1.1.2", "winston": "^3.4.0", diff --git a/libs/ledger-live-common/src/exchange/swap/hooks/useSwapTransaction.ts b/libs/ledger-live-common/src/exchange/swap/hooks/useSwapTransaction.ts index 0152039b01b9..843a1c9150d6 100644 --- a/libs/ledger-live-common/src/exchange/swap/hooks/useSwapTransaction.ts +++ b/libs/ledger-live-common/src/exchange/swap/hooks/useSwapTransaction.ts @@ -154,7 +154,7 @@ export const useSwapTransaction = ({ bridge: bridgeTransaction, }); - const { rates, refetchRates, updateSelectedRate } = useProviderRates({ + const { rates, refetchRates, updateSelectedRate, countdown } = useProviderRates({ fromState, toState, onNoRates, @@ -176,6 +176,7 @@ export const useSwapTransaction = ({ value: rates.value.filter(v => v.tradeMethod !== "fixed"), } : rates, + countdown, refetchRates, updateSelectedRate, targetAccounts, diff --git a/libs/ledger-live-common/src/exchange/swap/hooks/v5/useProviderRates.ts b/libs/ledger-live-common/src/exchange/swap/hooks/v5/useProviderRates.ts index 847b69f6304e..3b5817c25c01 100644 --- a/libs/ledger-live-common/src/exchange/swap/hooks/v5/useProviderRates.ts +++ b/libs/ledger-live-common/src/exchange/swap/hooks/v5/useProviderRates.ts @@ -3,7 +3,9 @@ import { OnNoRatesCallback, RatesReducerState, SwapSelectorStateType } from "../ import { useFetchRates } from "./useFetchRates"; import { SetExchangeRateCallback } from "../useSwapTransaction"; import { useFeature } from "../../../../featureFlags"; -import { useCallback } from "react"; +import { useCallback, useEffect } from "react"; +import { useCountdown } from "usehooks-ts"; +import { DEFAULT_SWAP_RATES_INTERVAL_MS } from "../../const/timeout"; type Props = { fromState: SwapSelectorStateType; @@ -16,6 +18,7 @@ export type UseProviderRatesResponse = { rates: RatesReducerState; refetchRates(): void; updateSelectedRate(): void; + countdown: undefined | number; }; export function useProviderRates({ @@ -24,6 +27,10 @@ export function useProviderRates({ onNoRates, setExchangeRate, }: Props): UseProviderRatesResponse { + const [countdown, { startCountdown, resetCountdown, stopCountdown }] = useCountdown({ + countStart: DEFAULT_SWAP_RATES_INTERVAL_MS / 1000, + countStop: 0, + }); const ptxSwapMoonpayProviderFlag = useFeature("ptxSwapMoonpayProvider"); const filterMoonpay = useCallback( rates => { @@ -38,15 +45,24 @@ export function useProviderRates({ toCurrency: toState.currency, fromCurrencyAmount: fromState.amount ?? BigNumber(0), onSuccess(data) { + resetCountdown(); const rates = filterMoonpay(data); if (rates.length === 0) { + stopCountdown(); onNoRates?.({ fromState, toState }); } else { + startCountdown(); setExchangeRate?.(rates[0]); } }, }); + useEffect(() => { + if (countdown <= 0) { + refetch(); + } + }, [countdown, refetch]); + if (!fromState.amount || fromState.amount.lte(0)) { setExchangeRate?.(undefined); return { @@ -57,6 +73,7 @@ export function useProviderRates({ }, refetchRates: () => undefined, updateSelectedRate: () => undefined, + countdown: undefined, }; } @@ -70,6 +87,7 @@ export function useProviderRates({ }, refetchRates: () => undefined, updateSelectedRate: () => undefined, + countdown: undefined, }; } if (error) { @@ -82,6 +100,7 @@ export function useProviderRates({ }, refetchRates: () => undefined, updateSelectedRate: () => undefined, + countdown: undefined, }; } @@ -94,6 +113,7 @@ export function useProviderRates({ }, refetchRates: refetch, updateSelectedRate: () => undefined, + countdown, }; } @@ -105,5 +125,6 @@ export function useProviderRates({ }, refetchRates: () => undefined, updateSelectedRate: () => undefined, + countdown: undefined, }; } diff --git a/libs/ledger-live-common/src/exchange/swap/types.ts b/libs/ledger-live-common/src/exchange/swap/types.ts index 1caa9404b379..32aa9b6800cb 100644 --- a/libs/ledger-live-common/src/exchange/swap/types.ts +++ b/libs/ledger-live-common/src/exchange/swap/types.ts @@ -322,6 +322,7 @@ export type SwapDataType = { refetchRates: () => void; updateSelectedRate: (selected?: ExchangeRate) => void; targetAccounts?: Account[]; + countdown: undefined | number; }; export type SwapTransactionType = UseBridgeTransactionResult & { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf3e18216ccb..f08ebf5b721a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2441,6 +2441,9 @@ importers: tronweb: specifier: ^5.2.0 version: 5.2.0 + usehooks-ts: + specifier: ^2.13.0 + version: 2.13.0(react@18.2.0) utility-types: specifier: ^3.10.0 version: 3.10.0 @@ -56107,6 +56110,16 @@ packages: requiresBuild: true dev: true + /usehooks-ts@2.13.0(react@18.2.0): + resolution: {integrity: sha512-3yZ2dwbc5BTaejBvamckAqv/q29sKFeqNCdi1z9Im4GzsTT5XbdIdbB94KEoh9xSvZy/qRoztjR14pF5voEU5Q==} + engines: {node: '>=16.15.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + dependencies: + lodash.debounce: 4.0.8 + react: 18.2.0 + dev: false + /utf-8-validate@5.0.10: resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==, tarball: https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz} engines: {node: '>=6.14.2'} From ff9589d6a032c066fcd5bee74d7f4eed09fb4164 Mon Sep 17 00:00:00 2001 From: Kieran Allen Date: Thu, 8 Feb 2024 10:11:39 +0000 Subject: [PATCH 2/4] chore: add changeset --- .changeset/brown-experts-fly.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/brown-experts-fly.md diff --git a/.changeset/brown-experts-fly.md b/.changeset/brown-experts-fly.md new file mode 100644 index 000000000000..a4f2853a3a29 --- /dev/null +++ b/.changeset/brown-experts-fly.md @@ -0,0 +1,6 @@ +--- +"@ledgerhq/live-common": minor +"ledger-live-desktop": patch +--- + +Return countdown and refresh rates from the useProviderRates hook From 50f305fca3b13e518bc80d76492be913f0230cb7 Mon Sep 17 00:00:00 2001 From: Kieran Allen Date: Thu, 8 Feb 2024 10:31:44 +0000 Subject: [PATCH 3/4] fix: memoize providers and exchangeRates to stop unnecessary calls to analytics --- .../renderer/screens/exchange/Swap2/Form/Rates/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Rates/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Rates/index.tsx index b31446265142..3e22c1d78f44 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Rates/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/Form/Rates/index.tsx @@ -61,11 +61,12 @@ export default function ProviderRate({ const [defaultPartner, setDefaultPartner] = useState(null); const selectedRate = useSelector(rateSelector); const filteredRates = useMemo(() => filterRates(rates, filter), [rates, filter]); - const providers = [...new Set(rates?.map(rate => rate.provider) ?? [])]; - const exchangeRates = - toCurrency && rates + const providers = useMemo(() => [...new Set(rates?.map(rate => rate.provider) ?? [])], [rates]); + const exchangeRates = useMemo(() => { + return toCurrency && rates ? rates.map(({ toAmount }) => formatCurrencyUnit(getFeesUnit(toCurrency), toAmount)) : []; + }, [toCurrency, rates]); const updateRate = useCallback( (rate: ExchangeRate) => { const value = rate.rate ?? rate.provider; From f0b2e6c345879abee968a44eb62dc9acd5962e3d Mon Sep 17 00:00:00 2001 From: Kieran Allen Date: Thu, 8 Feb 2024 10:34:52 +0000 Subject: [PATCH 4/4] chore: remove unimported files --- .../Swap2/utils/getRefreshTime.test.ts | 58 ------------------- .../exchange/Swap2/utils/getRefreshTime.ts | 19 ------ 2 files changed, 77 deletions(-) delete mode 100644 apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/utils/getRefreshTime.test.ts delete mode 100644 apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/utils/getRefreshTime.ts diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/utils/getRefreshTime.test.ts b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/utils/getRefreshTime.test.ts deleted file mode 100644 index 870ab26e2c4b..000000000000 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/utils/getRefreshTime.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, it, expect, jest } from "@jest/globals"; -import { ExchangeRate } from "@ledgerhq/live-common/exchange/swap/types"; -import { getRefreshTime } from "~/renderer/screens/exchange/Swap2/utils/getRefreshTime"; -const mockDate = new Date("2020-01-01"); -jest.useFakeTimers().setSystemTime(mockDate); -describe("getRefreshTime", () => { - it("returns default refresh time when no rates provided", () => { - expect(getRefreshTime(undefined)).toEqual(20000); - }); - - it("returns the default refresh time when no rates have an expirationTime", () => { - expect( - getRefreshTime([ - { - rateId: "1", - } as ExchangeRate, - ]), - ).toEqual(20000); - }); - - it("returns the a refresh time that is the earliest expirationTime in the list of rates if it's under 60s", () => { - const mockTimeSinceEpoch = mockDate.getTime(); - expect( - getRefreshTime([ - { - rateId: "1", - expirationTime: mockTimeSinceEpoch + 19000, - }, - { - rateId: "2", - expirationTime: mockTimeSinceEpoch + 18000, - }, - { - rateId: "3", - }, - ] as ExchangeRate[]), - ).toEqual(18000); - }); - - it("returns 60s when the earliest expirationTime in the list of rates is over 60s", () => { - const mockTimeSinceEpoch = mockDate.getTime(); - expect( - getRefreshTime([ - { - rateId: "1", - expirationTime: mockTimeSinceEpoch + 21000, - }, - { - rateId: "2", - expirationTime: mockTimeSinceEpoch + 22000, - }, - { - rateId: "3", - }, - ] as ExchangeRate[]), - ).toEqual(20000); - }); -}); diff --git a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/utils/getRefreshTime.ts b/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/utils/getRefreshTime.ts deleted file mode 100644 index 5fd2c036da55..000000000000 --- a/apps/ledger-live-desktop/src/renderer/screens/exchange/Swap2/utils/getRefreshTime.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { DEFAULT_SWAP_RATES_INTERVAL_MS } from "@ledgerhq/live-common/exchange/swap/const/timeout"; -import { ExchangeRate } from "@ledgerhq/live-common/exchange/swap/types"; - -const getMinimumExpirationTime = (rates: ExchangeRate[]): number => { - return rates.reduce((acc, rate) => { - if (!rate.expirationTime) return acc; - return acc ? Math.min(acc, rate.expirationTime) : rate.expirationTime; - }, 0); -}; - -export const getRefreshTime = (rates: ExchangeRate[] | undefined) => { - const minimumExpirationTime = rates ? getMinimumExpirationTime(rates) : null; - if (minimumExpirationTime) { - const timeMs = minimumExpirationTime - Date.now(); - return Math.min(timeMs, DEFAULT_SWAP_RATES_INTERVAL_MS); - } else { - return DEFAULT_SWAP_RATES_INTERVAL_MS; - } -};