Skip to content

Commit

Permalink
feat: ledger implementation rework (#1763)
Browse files Browse the repository at this point in the history
* wip: ledger rework

* wip: ledger rework

* feat: update ledger legacy hooks and components to new model

* feat: update ledger ethereum to new model

* refactor: cleanup and remove suspenses

* refactor: useSignLedgerBase

* refactor: isSigning

* refactor: useLedgerXxx

* refactor: errors

* feat: error if app needs to be upgraded to generic

* refactor: connect ledger component

* chore: cleanup LedgerConnectionStatus

* chore: cleanup

* chore: remove commented code
  • Loading branch information
0xKheops authored Dec 30, 2024
1 parent bdb1bb6 commit b2eda68
Show file tree
Hide file tree
Showing 24 changed files with 1,367 additions and 1,617 deletions.
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

0 comments on commit b2eda68

Please sign in to comment.