Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ledger implementation rework #1763

Merged
merged 15 commits into from
Dec 30, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ export const AddLedgerSelectNetwork = () => {
<ConnectLedgerSubstrateGeneric
className="min-h-[11rem]"
onReadyChanged={setIsLedgerReady}
appName={chain?.ledgerAppName}
legacyAppName={chain?.ledgerAppName}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { log } from "extension-shared"
import { FC, useCallback, useEffect, useRef, useState } from "react"
import { useTranslation } from "react-i18next"

import { Spacer } from "@talisman/components/Spacer"
import {
LedgerConnectionStatus,
LedgerConnectionStatusProps,
} from "@ui/domains/Account/LedgerConnectionStatus"
import { getCustomTalismanLedgerError } from "@ui/hooks/ledger/errors"

type ConnectLedgerBaseProps = {
appName: string
isReadyCheck: () => Promise<unknown>
onReadyChanged: (ready: boolean) => void
className?: string
}

export const ConnectLedgerBase: FC<ConnectLedgerBaseProps> = ({
appName,
isReadyCheck,
onReadyChanged,
className,
}) => {
const { t } = useTranslation("admin")

// flag to prevents double connect attempt in dev mode
const refIsBusy = useRef(false)

const [connectionStatus, setConnectionStatus] = useState<LedgerConnectionStatusProps>({
status: "connecting",
message: t("Connecting to Ledger..."),
})

const connect = useCallback(async () => {
if (refIsBusy.current) return
refIsBusy.current = true

try {
onReadyChanged?.(false)
setConnectionStatus({
status: "connecting",
message: t("Connecting to Ledger..."),
})

await isReadyCheck()

setConnectionStatus({
status: "ready",
message: t("Successfully connected to Ledger."),
})
onReadyChanged?.(true)
} catch (err) {
const error = getCustomTalismanLedgerError(err)
log.error("ConnectLedgerSubstrateGeneric", { error })
setConnectionStatus({
status: "error",
message: error.message,
onRetryClick: connect,
})
} finally {
refIsBusy.current = false
}
}, [isReadyCheck, onReadyChanged, t])

useEffect(() => {
connect()
}, [connect, isReadyCheck, onReadyChanged])

return (
<div className={className}>
<div className="text-body-secondary m-0">
{t("Connect and unlock your Ledger, then open the {{appName}} app on your Ledger.", {
appName,
})}
</div>
<Spacer small />
{!!connectionStatus && <LedgerConnectionStatus {...connectionStatus} />}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,27 @@
import { useEffect } from "react"
import { Trans, useTranslation } from "react-i18next"
import { getEthLedgerDerivationPath } from "extension-core"
import { FC, useCallback } from "react"

import { Spacer } from "@talisman/components/Spacer"
import { LedgerConnectionStatus } from "@ui/domains/Account/LedgerConnectionStatus"
import { useLedgerEthereum } from "@ui/hooks/ledger/useLedgerEthereum"

export const ConnectLedgerEthereum = ({
onReadyChanged,
className,
}: {
onReadyChanged?: (ready: boolean) => void
className?: string
}) => {
const { t } = useTranslation("admin")
const ledger = useLedgerEthereum(true)
import { ConnectLedgerBase } from "./ConnectLedgerBase"

useEffect(() => {
onReadyChanged?.(ledger.isReady)
export const ConnectLedgerEthereum: FC<{
onReadyChanged: (ready: boolean) => void
className?: string
}> = ({ onReadyChanged, className }) => {
const { getAddress } = useLedgerEthereum()

return () => {
onReadyChanged?.(false)
}
}, [ledger.isReady, onReadyChanged])
const isReadyCheck = useCallback(() => {
const derivationPath = getEthLedgerDerivationPath("LedgerLive")
return getAddress(derivationPath)
}, [getAddress])

return (
<div className={className}>
<div className="text-body-secondary m-0">
<Trans t={t}>
Connect and unlock your Ledger, then open the <span className="text-body">Ethereum</span>{" "}
app on your Ledger.
</Trans>
</div>
<Spacer small />
<LedgerConnectionStatus {...ledger} />
</div>
<ConnectLedgerBase
appName="Ethereum"
className={className}
isReadyCheck={isReadyCheck}
onReadyChanged={onReadyChanged}
/>
)
}
Original file line number Diff line number Diff line change
@@ -1,43 +1,30 @@
import { FC, useEffect } from "react"
import { useTranslation } from "react-i18next"
import { FC, useCallback } from "react"

import { Spacer } from "@talisman/components/Spacer"
import { LedgerConnectionStatus } from "@ui/domains/Account/LedgerConnectionStatus"
import { getPolkadotLedgerDerivationPath } from "@ui/hooks/ledger/common"
import { useLedgerSubstrateAppByName } from "@ui/hooks/ledger/useLedgerSubstrateApp"
import { useLedgerSubstrateGeneric } from "@ui/hooks/ledger/useLedgerSubstrateGeneric"

type ConnectLedgerSubstrateGenericProps = {
onReadyChanged?: (ready: boolean) => void
className?: string
appName?: string | null
}
import { ConnectLedgerBase } from "./ConnectLedgerBase"

export const ConnectLedgerSubstrateGeneric: FC<ConnectLedgerSubstrateGenericProps> = ({
onReadyChanged,
className,
appName,
}) => {
const app = useLedgerSubstrateAppByName(appName)
const ledger = useLedgerSubstrateGeneric({ persist: true, app })
const { t } = useTranslation("admin")

useEffect(() => {
onReadyChanged?.(ledger.isReady)
export const ConnectLedgerSubstrateGeneric: FC<{
onReadyChanged: (ready: boolean) => void
className?: string
legacyAppName?: string | null
}> = ({ onReadyChanged, className, legacyAppName }) => {
const legacyApp = useLedgerSubstrateAppByName(legacyAppName)
const { getAddress } = useLedgerSubstrateGeneric({ legacyApp })

return () => {
onReadyChanged?.(false)
}
}, [ledger.isReady, onReadyChanged])
const isReadyCheck = useCallback(() => {
const derivationPath = getPolkadotLedgerDerivationPath({ legacyApp })
return getAddress(derivationPath)
}, [getAddress, legacyApp])

return (
<div className={className}>
<div className="text-body-secondary m-0">
{t("Connect and unlock your Ledger, then open the {{appName}} app on your Ledger.", {
appName: app ? "Polkadot Migration" : "Polkadot",
})}
</div>
<Spacer small />
<LedgerConnectionStatus {...ledger} />
</div>
<ConnectLedgerBase
appName={legacyAppName ? "Polkadot Migration" : "Polkadot"}
className={className}
isReadyCheck={isReadyCheck}
onReadyChanged={onReadyChanged}
/>
)
}
Original file line number Diff line number Diff line change
@@ -1,56 +1,26 @@
import { FC, useEffect } from "react"
import { Trans, useTranslation } from "react-i18next"
import { FC, useCallback } from "react"

import { Spacer } from "@talisman/components/Spacer"
import { LedgerConnectionStatus } from "@ui/domains/Account/LedgerConnectionStatus"
import { useLedgerSubstrateAppByChain } from "@ui/hooks/ledger/useLedgerSubstrateApp"
import { useLedgerSubstrateLegacy } from "@ui/hooks/ledger/useLedgerSubstrateLegacy"
import { useChain, useToken } from "@ui/state"
import { useChain } from "@ui/state"

type ConnectLedgerSubstrateLegacyProps = {
import { ConnectLedgerBase } from "./ConnectLedgerBase"

export const ConnectLedgerSubstrateLegacy: FC<{
chainId: string
onReadyChanged?: (ready: boolean) => void
onReadyChanged: (ready: boolean) => void
className?: string
}

export const ConnectLedgerSubstrateLegacy: FC<ConnectLedgerSubstrateLegacyProps> = ({
chainId,
onReadyChanged,
className,
}) => {
}> = ({ chainId, onReadyChanged, className }) => {
const chain = useChain(chainId)
const token = useToken(chain?.nativeToken?.id)
const ledger = useLedgerSubstrateLegacy(chain?.genesisHash, true)
const app = useLedgerSubstrateAppByChain(chain)
const { t } = useTranslation("admin")

useEffect(() => {
onReadyChanged?.(ledger.isReady)

return () => {
onReadyChanged?.(false)
}
}, [ledger.isReady, onReadyChanged])
const { app, getAddress } = useLedgerSubstrateLegacy(chain?.genesisHash)

if (!app) return null
const isReadyCheck = useCallback(() => getAddress(0, 0), [getAddress])

return (
<div className={className}>
<div className="text-body-secondary m-0">
<Trans
t={t}
components={{
AppName: (
<span className="text-body">
{app.name + (token?.symbol ? ` (${token.symbol})` : "")}
</span>
),
}}
defaults="Connect and unlock your Ledger, then open the <AppName /> app on your Ledger."
/>
</div>
<Spacer small />
<LedgerConnectionStatus {...ledger} />
</div>
<ConnectLedgerBase
appName={app?.name ?? "Unknown App"}
className={className}
isReadyCheck={isReadyCheck}
onReadyChanged={onReadyChanged}
/>
)
}
48 changes: 16 additions & 32 deletions apps/extension/src/ui/domains/Account/LedgerConnectionStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { CheckCircleIcon, LoaderIcon, XCircleIcon } from "@talismn/icons"
import { classNames } from "@talismn/util"
import { FC, ReactNode, useEffect, useState } from "react"
import { useTranslation } from "react-i18next"

import { LedgerStatus } from "@ui/hooks/ledger/common"

export type LedgerConnectionStatusProps = {
status: LedgerStatus
message: string
requiresManualRetry?: boolean
hideOnSuccess?: boolean
className?: string
refresh: () => void
onRetryClick?: () => void
}

const wrapStrong = (text: string) => {
Expand All @@ -31,45 +29,22 @@ const wrapStrong = (text: string) => {
})
}

const Container: FC<{ className?: string; onClick?: () => void; children?: ReactNode }> = ({
className,
onClick,
children,
}) => {
if (onClick)
return (
<button type="button" onClick={onClick} className={className}>
{children}
</button>
)
else return <div className={className}>{children}</div>
}

export const LedgerConnectionStatus = ({
status,
message,
requiresManualRetry,
hideOnSuccess = false,
className,
refresh,
onRetryClick,
}: LedgerConnectionStatusProps) => {
const [hide, setHide] = useState<boolean>(false)

useEffect(() => {
if (status === "ready" && hideOnSuccess) setTimeout(() => setHide(true), 1000)
}, [status, hideOnSuccess])
const { t } = useTranslation()

if (!status || status === "unknown") return null

return (
<Container
<div
className={classNames(
"text-body-secondary bg-grey-850 flex h-28 w-full items-center gap-4 rounded-sm p-8",
hide && "invisible",
requiresManualRetry && "hover:bg-grey-800",
className,
)}
onClick={requiresManualRetry ? refresh : undefined}
>
{status === "ready" && (
<CheckCircleIcon className="text-alert-success min-w-[1em] shrink-0 text-[2rem]" />
Expand All @@ -83,7 +58,16 @@ export const LedgerConnectionStatus = ({
{status === "connecting" && (
<LoaderIcon className="animate-spin-slow min-w-[1em] shrink-0 text-[2rem] text-white" />
)}
<div className="text-left leading-[2rem]">{wrapStrong(message)}</div>
</Container>
<div className="grow text-left leading-[2rem]">{wrapStrong(message)}</div>
{!!onRetryClick && (
<button
type="button"
onClick={onRetryClick}
className="bg-grey-800 hover:bg-grey-750 text-body border-body-disabled hover:border-body-inactive h-20 rounded border px-8"
>
{t("Retry")}
</button>
)}
</div>
)
}
Loading
Loading