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

Init "0-click connection" flow for the Ledger Live App #742

Merged
merged 9 commits into from
Oct 2, 2024
2 changes: 1 addition & 1 deletion dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@orangekit/react": "1.0.0-beta.32-dev.0",
"@orangekit/react": "1.0.0-beta.33-dev.3",
"@orangekit/sign-in-with-wallet": "1.0.0-beta.6",
"@reduxjs/toolkit": "^2.2.0",
"@rehooks/local-storage": "^2.4.5",
Expand Down
2 changes: 2 additions & 0 deletions dapp/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ export { default as useReferral } from "./useReferral"
export { default as useMats } from "./useMats"
export { default as useIsEmbed } from "./useIsEmbed"
export { default as useSignMessageAndCreateSession } from "./useSignMessageAndCreateSession"
export { default as useTriggerConnectWalletModal } from "./useTriggerConnectWalletModal"
export { default as useLastUsedBtcAddress } from "./useLastUsedBtcAddress"
3 changes: 2 additions & 1 deletion dapp/src/hooks/orangeKit/useBitcoinProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export function useBitcoinProvider(): UseBitcoinProviderReturn {
const connector = useConnector()

return useMemo(() => {
if (!connector) return undefined
if (!connector || typeof connector.getBitcoinProvider !== "function")
return undefined

return connector.getBitcoinProvider()
}, [connector])
Expand Down
28 changes: 28 additions & 0 deletions dapp/src/hooks/useLastUsedBtcAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useCallback } from "react"
import useLocalStorage from "./useLocalStorage"

export const LAST_USED_BTC_ADDRESS_KEY = "lastUsedBtcAddress"
nkuba marked this conversation as resolved.
Show resolved Hide resolved

export default function useLastUsedBtcAddress() {
const [address, setAddress] = useLocalStorage<string | undefined>(
LAST_USED_BTC_ADDRESS_KEY,
undefined,
)

const setAddressInLocalStorage = useCallback(
(btcAddress: string) => {
setAddress(btcAddress)
},
[setAddress],
)

const removeAddressFromLocalStorage = useCallback(() => {
setAddress(undefined)
}, [setAddress])

return {
address,
setAddressInLocalStorage,
removeAddressFromLocalStorage,
}
}
8 changes: 8 additions & 0 deletions dapp/src/hooks/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { useLocalStorage as useRehooksLocalStorage } from "@rehooks/local-storage"

export const getLocalStorageItem = (key: string): string | undefined => {
const value = localStorage.getItem(key)
if (value === "undefined" || value === "null" || value === null)
return undefined

return value
}

export default function useLocalStorage<T>(key: string, defaultValue: T) {
return useRehooksLocalStorage(key, defaultValue)
}
8 changes: 5 additions & 3 deletions dapp/src/hooks/useResetWalletState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQueryClient } from "@tanstack/react-query"
import { QueryFilters, useQueryClient } from "@tanstack/react-query"
import { useCallback } from "react"
import { resetState } from "#/store/wallet"
import { queryKeysFactory } from "#/constants"
Expand All @@ -12,8 +12,10 @@ export default function useResetWalletState() {
const dispatch = useAppDispatch()

const resetQueries = useCallback(async () => {
queryClient.removeQueries({ queryKey: userKeys.all })
await queryClient.resetQueries({ queryKey: userKeys.all })
const filters: QueryFilters = { queryKey: userKeys.all, exact: true }

queryClient.removeQueries(filters)
await queryClient.resetQueries(filters)
}, [queryClient])

return useCallback(() => {
Expand Down
31 changes: 29 additions & 2 deletions dapp/src/hooks/useSignMessageAndCreateSession.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { OrangeKitConnector } from "#/types"
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { acreApi, orangeKit } from "#/utils"
import { acreApi, getExpirationDate, orangeKit } from "#/utils"
import { generateNonce } from "@orangekit/sign-in-with-wallet"
import { useCallback } from "react"
import { useSignMessage } from "wagmi"
import { ACRE_SESSION_EXPIRATION_TIME } from "#/constants"
import useLocalStorage from "./useLocalStorage"

const initialSession = { address: "", sessionId: 0 }

function useSignMessageAndCreateSession() {
const {
Expand All @@ -12,6 +16,16 @@ function useSignMessageAndCreateSession() {
reset: resetMessageStatus,
} = useSignMessage()

// TODO: Temporary solution to mock the session mechanism for Ledger Live App
// integration. To fully support the session mechanism exposed by Acre API
// backend we need sign message with "zero" address not fresh address. This is
// will be supported in future versions of Ledger Bitcoin App.
const [session, setSession] = useLocalStorage<{
address: string
sessionId: number
}>("acre.session", initialSession)
const { address: sessionAddress, sessionId } = session

const signMessageAndCreateSession = useCallback(
async (connectedConnector: OrangeKitConnector, btcAddress: string) => {
// const session = await acreApi.getSession()
Expand All @@ -35,6 +49,14 @@ function useSignMessageAndCreateSession() {
// if (!("nonce" in session)) {
// throw new Error("Session nonce not available")
// }
if (
new Date(sessionId).getTime() > Date.now() &&
btcAddress === sessionAddress
) {
// The session is valid no need to sign message.
return
}

const nonce = generateNonce()
const message = orangeKit.createSignInWithWalletMessage(btcAddress, nonce)

Expand All @@ -44,13 +66,18 @@ function useSignMessageAndCreateSession() {
connector: orangeKit.typeConversionToConnector(connectedConnector),
})

const newSessionId = getExpirationDate(
ACRE_SESSION_EXPIRATION_TIME,
).getTime()
setSession({ address: btcAddress, sessionId: newSessionId })

// const publicKey = await connectedConnector
// .getBitcoinProvider()
// .getPublicKey()

// await acreApi.createSession(message, signedMessage, publicKey)
},
[signMessageAsync],
[signMessageAsync, sessionAddress, sessionId, setSession],
)

return {
Expand Down
22 changes: 22 additions & 0 deletions dapp/src/hooks/useTriggerConnectWalletModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useRef } from "react"
import { MODAL_TYPES } from "#/types"
import { useIsSignedMessage } from "./store/useIsSignedMessage"
import { useModal } from "./useModal"
import useIsEmbed from "./useIsEmbed"

export default function useTriggerConnectWalletModal() {
const isSignedMessage = useIsSignedMessage()
const { isEmbed } = useIsEmbed()
const { openModal, closeModal } = useModal()
const isMounted = useRef(false)

useEffect(() => {
if (!isMounted.current && isEmbed && !isSignedMessage) {
isMounted.current = true
openModal(MODAL_TYPES.CONNECT_WALLET, {
withCloseButton: false,
closeOnEsc: false,
})
}
}, [closeModal, isEmbed, isSignedMessage, openModal])
}
10 changes: 8 additions & 2 deletions dapp/src/hooks/useWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useConnector } from "./orangeKit/useConnector"
import { useBitcoinProvider } from "./orangeKit/useBitcoinProvider"
import useBitcoinBalance from "./orangeKit/useBitcoinBalance"
import useResetWalletState from "./useResetWalletState"
import useLastUsedBtcAddress from "./useLastUsedBtcAddress"

const { typeConversionToConnector, typeConversionToOrangeKitConnector } =
orangeKit
Expand Down Expand Up @@ -39,6 +40,8 @@ export function useWallet(): UseWalletReturn {
const provider = useBitcoinProvider()
const { data: balance } = useBitcoinBalance()
const resetWalletState = useResetWalletState()
const { setAddressInLocalStorage, removeAddressFromLocalStorage } =
useLastUsedBtcAddress()

const [address, setAddress] = useState<string | undefined>(undefined)

Expand Down Expand Up @@ -82,22 +85,25 @@ export function useWallet(): UseWalletReturn {

const onDisconnect = useCallback(() => {
disconnect()
setAddress(undefined)
resetWalletState()
}, [disconnect, resetWalletState])
removeAddressFromLocalStorage()
}, [disconnect, removeAddressFromLocalStorage, resetWalletState])

useEffect(() => {
const fetchBitcoinAddress = async () => {
if (connector) {
const btcAddress = await connector.getBitcoinAddress()

setAddress(btcAddress)
setAddressInLocalStorage(btcAddress)
} else {
setAddress(undefined)
}
}

logPromiseFailure(fetchBitcoinAddress())
}, [connector, provider])
}, [connector, provider, setAddressInLocalStorage])

return useMemo(
() => ({
Expand Down
4 changes: 3 additions & 1 deletion dapp/src/pages/DashboardPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react"
import { useMobileMode } from "#/hooks"
import { useMobileMode, useTriggerConnectWalletModal } from "#/hooks"
import MobileModeBanner from "#/components/MobileModeBanner"
import { featureFlags } from "#/constants"
import DashboardCard from "./DashboardCard"
Expand All @@ -13,6 +13,8 @@ import UsefulLinks from "./UsefulLinks"
export default function DashboardPage() {
const isMobileMode = useMobileMode()

useTriggerConnectWalletModal()

return isMobileMode ? (
<MobileModeBanner forceOpen />
) : (
Expand Down
10 changes: 8 additions & 2 deletions dapp/src/wagmiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
import { env } from "./constants"
import { router } from "./utils"
import { SEARCH_PARAMS_NAMES } from "./router/path"
import { LAST_USED_BTC_ADDRESS_KEY } from "./hooks/useLastUsedBtcAddress"
import { getLocalStorageItem } from "./hooks/useLocalStorage"

const isTestnet = env.USE_TESTNET
const CHAIN_ID = isTestnet ? sepolia.id : mainnet.id
Expand All @@ -28,8 +30,12 @@ const transports = chains.reduce(
const orangeKitUnisatConnector = getOrangeKitUnisatConnector(connectorConfig)
const orangeKitOKXConnector = getOrangeKitOKXConnector(connectorConfig)
const orangeKitXverseConnector = getOrangeKitXverseConnector(connectorConfig)
const orangeKitLedgerLiveConnector =
getOrangeKitLedgerLiveConnector(connectorConfig)
const orangeKitLedgerLiveConnector = getOrangeKitLedgerLiveConnector({
...connectorConfig,
options: {
tryConnectToAddress: getLocalStorageItem(LAST_USED_BTC_ADDRESS_KEY),
},
})

const embedConnectors = [orangeKitLedgerLiveConnector()]
const defaultConnectors = [
Expand Down
Loading
Loading