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: add support for partial wallet management #320

Merged
merged 1 commit into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ListItemButton } from './ListItemButton.js'
import { ListItemText } from './ListItemText.js'
import { SVMListItemButton } from './SVMListItemButton.js'
import { UTXOListItemButton } from './UTXOListItemButton.js'
import { WalletMenuContentEmpty } from './WalletMenuContentEmpty.js'

interface WalletMenuContentProps {
onClose: () => void
Expand Down Expand Up @@ -197,6 +198,9 @@ export const WalletMenuContent: React.FC<WalletMenuContentProps> = ({
</ListItemButton>
)
})}
{/* TODO: show all connected wallets with 'Connected' badge
and have this empty screen only when there is no installed wallets at all */}
{!installedWallets.length ? <WalletMenuContentEmpty /> : null}
</List>
</Fade>
</Collapse>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Wallet } from '@mui/icons-material'
import { Box, Typography } from '@mui/material'
import { useTranslation } from 'react-i18next'

export const WalletMenuContentEmpty: React.FC = () => {
const { t } = useTranslation()
return (
<Box
sx={{
display: 'flex',
flex: 1,
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
paddingY: 8,
}}
>
<Typography fontSize={48}>
<Wallet fontSize="inherit" />
</Typography>
<Typography fontSize={18} fontWeight={700}>
{t('title.availableWalletsNotFound')}
</Typography>
<Typography
fontSize={14}
color="text.secondary"
textAlign="center"
mt={2}
>
{t('message.availableWalletsNotFound')}
</Typography>
</Box>
)
}
46 changes: 28 additions & 18 deletions packages/wallet-management/src/hooks/useCombinedWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,24 +141,34 @@ export const useCombinedWallets = () => {
)
}

const installedUTXOConnectors = bigmiConnectors.filter((connector) => {
const isInstalled = isWalletInstalled(connector.id)
const isConnected = bigmiAccount.connector?.id === connector.id
return isInstalled && !isConnected
})

const installedEVMConnectors = evmConnectors.filter((connector) => {
const isInstalled = isWalletInstalled(connector.id)
const isConnected = wagmiAccount.connector?.id === connector.id
return isInstalled && !isConnected
})

const installedSVMWallets = solanaWallets.filter((wallet) => {
const isInstalled =
wallet.adapter.readyState === WalletReadyState.Installed
const isConnected = wallet.adapter.connected
return isInstalled && !isConnected
})
const includeEcosystem = (chainType: ChainType) =>
!walletConfig.enabledChainTypes ||
walletConfig.enabledChainTypes.includes(chainType)

const installedUTXOConnectors = includeEcosystem(ChainType.UTXO)
? bigmiConnectors.filter((connector) => {
const isInstalled = isWalletInstalled(connector.id)
const isConnected = bigmiAccount.connector?.id === connector.id
return isInstalled && !isConnected
})
: []

const installedEVMConnectors = includeEcosystem(ChainType.EVM)
? evmConnectors.filter((connector) => {
const isInstalled = isWalletInstalled(connector.id)
const isConnected = wagmiAccount.connector?.id === connector.id
return isInstalled && !isConnected
})
: []

const installedSVMWallets = includeEcosystem(ChainType.SVM)
? solanaWallets.filter((wallet) => {
const isInstalled =
wallet.adapter.readyState === WalletReadyState.Installed
const isConnected = wallet.adapter.connected
return isInstalled && !isConnected
})
: []

const installedCombinedWallets = combineWalletLists(
installedUTXOConnectors,
Expand Down
6 changes: 4 additions & 2 deletions packages/wallet-management/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
"connectWallet": "Connect wallet",
"connecting": "Connecting",
"selectEcosystem": "Select ecosystem",
"waitingForWallet": "Waiting for {{walletName}}"
"waitingForWallet": "Waiting for {{walletName}}",
"availableWalletsNotFound": "Available wallets not found"
},
"message": {
"connecting": "Click connect in your wallet popup. Don't see your wallet? Check your other browser windows.",
"multipleEcosystems": "{{walletName}} supports multiple chain ecosystems. Select which chain ecosystem you'd like to connect."
"multipleEcosystems": "{{walletName}} supports multiple chain ecosystems. Select which chain ecosystem you'd like to connect.",
"availableWalletsNotFound": "No compatible wallet extensions detected. Please install a supported wallet and refresh the page. If already installed, ensure it's enabled and compatible, or contact support."
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { ChainType } from '@lifi/sdk'
import type { WalletConfig } from '../../types/walletConfig.js'
import type { LanguageKey } from '../I18nProvider/types.js'

export interface WalletManagementConfig extends WalletConfig {
locale?: LanguageKey
enabledChainTypes?: ChainType[]
}

export interface WalletManagementProviderProps {
Expand Down
2 changes: 2 additions & 0 deletions packages/wallet-management/src/utils/isWalletInstalled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export const isWalletInstalled = (id: string): boolean => {
)
case 'app.phantom.bitcoin':
return anyWindow.phantom?.bitcoin?.isPhantom
case 'com.okex.wallet.bitcoin':
return anyWindow.okxwallet?.bitcoin?.isOkxWallet
case 'XverseProviders.BitcoinProvider':
return anyWindow.XverseProviders?.BitcoinProvider
case 'unisat':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Collapse } from '@mui/material'
import type * as React from 'react'
import { useConfigActions } from '../../../store/widgetConfig/useConfigActions'
import { useConfigWalletManagement } from '../../../store/widgetConfig/useConfigValues'
Expand All @@ -10,18 +11,33 @@ import {
import { Switch } from '../../Switch'

export const WalletManagementControl = () => {
const { isExternalWalletManagement, replacementWalletConfig } =
useConfigWalletManagement()
const {
isExternalWalletManagement,
isPartialWalletManagement,
replacementWalletConfig,
} = useConfigWalletManagement()
const { setWalletConfig } = useConfigActions()
const handleSwitchChange: (

const handleExternalWalletManagement = (
_: React.ChangeEvent<HTMLInputElement>,
checked: boolean
) => void = (_, checked) => {
) => {
const walletConfig = checked ? replacementWalletConfig : undefined

setWalletConfig(walletConfig)
}

const handlePartialWalletManagement = (
_: React.ChangeEvent<HTMLInputElement>,
checked: boolean
) => {
const walletConfig = checked
? { ...replacementWalletConfig, usePartialWalletManagement: true }
: replacementWalletConfig

setWalletConfig(walletConfig)
}

return (
<Card>
<CardRowContainer>
Expand All @@ -30,10 +46,22 @@ export const WalletManagementControl = () => {
</CardTitleContainer>
<Switch
checked={isExternalWalletManagement}
onChange={handleSwitchChange}
onChange={handleExternalWalletManagement}
aria-label="Enable external wallet management"
/>
</CardRowContainer>
<Collapse in={isExternalWalletManagement}>
<CardRowContainer>
<CardTitleContainer>
<CardValue>Use partial wallet management</CardValue>
</CardTitleContainer>
<Switch
checked={isPartialWalletManagement}
onChange={handlePartialWalletManagement}
aria-label="Use partial wallet management"
/>
</CardRowContainer>
</Collapse>
</Card>
)
}
3 changes: 3 additions & 0 deletions packages/widget-playground/src/defaultWidgetConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const widgetBaseConfig: WidgetConfig = {
// disabledUI: ['toAddress', 'fromAmount', 'toToken', 'fromToken'],
// requiredUI: ['toAddress'],
// slippage: 0.003,
// walletConfig: {
// usePartialWalletManagement: true,
// },
sdkConfig: {
apiUrl: 'https://li.quest/v1',
rpcUrls: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import {
darkTheme,
getDefaultConfig,
lightTheme,
useConnectModal,
} from '@rainbow-me/rainbowkit'
import '@rainbow-me/rainbowkit/styles.css'
import { type FC, type PropsWithChildren, useMemo } from 'react'
import { type FC, type PropsWithChildren, useEffect, useMemo } from 'react'
import type { Chain } from 'viem'
import { WagmiProvider } from 'wagmi'
import { mainnet } from 'wagmi/chains'
import { useThemeMode } from '../../hooks/useThemeMode'
import { useWidgetConfigStore } from '../../store/widgetConfig/WidgetConfigProvider'
import { useConfigActions } from '../../store/widgetConfig/useConfigActions'
import { useEnvVariables } from '../EnvVariablesProvider'
import { theme } from '../PlaygroundThemeProvider/theme'

Expand Down Expand Up @@ -72,8 +75,36 @@ export const EVMProvider: FC<PropsWithChildren> = ({ children }) => {
reconnectOnMount={Boolean(chains?.length)}
>
<RainbowKitProvider theme={RainbowKitModes[themeMode] as RainbowKitTheme}>
<WidgetWalletConfigUpdater />
{children}
</RainbowKitProvider>
</WagmiProvider>
)
}
export const WidgetWalletConfigUpdater = () => {
const { openConnectModal } = useConnectModal()
const { setWalletConfig } = useConfigActions()
const walletConfig = useWidgetConfigStore(
(store) => store.config?.walletConfig
)

useEffect(() => {
// Due to provider constraints, we're unable to directly assign an onConnect function
// that opens the external wallet management directly from the widget playground settings component.
// To work around this limitation, we employ a temporary "hack" by initially setting an empty function.
// This allows us to later replace it with the intended functionality.
const onConnectStringified = walletConfig?.onConnect
?.toString()
.replaceAll(' ', '')
if (onConnectStringified === '()=>{}') {
setWalletConfig({
...walletConfig,
onConnect: () => {
openConnectModal?.()
},
})
}
}, [openConnectModal, setWalletConfig, walletConfig])

return null
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { FC, PropsWithChildren } from 'react'
import { EVMProvider } from './EVMProvider'
import { SVMProvider } from './SVMProvider'

interface ExternalWalletProviderProps extends PropsWithChildren {
isExternalProvider?: boolean
Expand All @@ -9,11 +8,5 @@ export const ExternalWalletProvider: FC<ExternalWalletProviderProps> = ({
children,
isExternalProvider,
}) => {
return isExternalProvider ? (
<EVMProvider>
<SVMProvider>{children}</SVMProvider>
</EVMProvider>
) : (
<>{children}</>
)
return isExternalProvider ? <EVMProvider>{children}</EVMProvider> : children
}
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ export const createWidgetConfigStore = (
if (state.config?.walletConfig) {
const walletConfig = state.defaultConfig?.walletConfig
? state.defaultConfig?.walletConfig
: { async onConnect() {} }
: { onConnect: () => {} }
state.setWalletConfig(walletConfig)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,11 @@ export const useConfigWalletManagement = () => {

const replacementWalletConfig = defaultWalletConfig
? defaultWalletConfig
: { async onConnect() {} }
: { onConnect: () => {} }

return {
isExternalWalletManagement: !!walletConfig,
isPartialWalletManagement: !!walletConfig?.usePartialWalletManagement,
replacementWalletConfig,
}
}
13 changes: 9 additions & 4 deletions packages/widget/src/components/ChainSelect/useChainSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { EVMChain } from '@lifi/sdk'
import { useChains } from '../../hooks/useChains.js'
import { useSwapOnly } from '../../hooks/useSwapOnly.js'
import { useToAddressReset } from '../../hooks/useToAddressReset.js'
import { useHasExternalWalletProvider } from '../../providers/WalletProvider/useHasExternalWalletProvider.js'
import { useExternalWalletProvider } from '../../providers/WalletProvider/useExternalWalletProvider.js'
import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'
import { useChainOrder } from '../../stores/chains/useChainOrder.js'
import type { FormType } from '../../stores/form/types.js'
Expand All @@ -16,11 +16,16 @@ export const useChainSelect = (formType: FormType) => {
const chainKey = FormKeyHelper.getChainKey(formType)
const { onChange } = useFieldController({ name: chainKey })
const { setFieldValue, getFieldValues } = useFieldActions()
const { hasExternalProvider, availableChainTypes } =
useHasExternalWalletProvider()
const { useExternalWalletProvidersOnly, externalChainTypes } =
useExternalWalletProvider()
const { chains, isLoading, getChainById } = useChains(
formType,
formType === 'from' && hasExternalProvider ? availableChainTypes : undefined
// If the integrator uses external wallet management and has not opted in for partial wallet management,
// restrict the displayed chains to those compatible with external wallet management.
// This ensures users only see chains for which they can sign transactions.
formType === 'from' && useExternalWalletProvidersOnly
? externalChainTypes
: undefined
)

const [chainOrder, setChainOrder] = useChainOrder(formType)
Expand Down
7 changes: 4 additions & 3 deletions packages/widget/src/components/Header/CloseDrawerButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { CloseRounded } from '@mui/icons-material'
import { IconButton, Tooltip } from '@mui/material'
import { useTranslation } from 'react-i18next'
import { useDrawer } from '../../AppDrawerContext.js'
import { useHasExternalWalletProvider } from '../../providers/WalletProvider/useHasExternalWalletProvider.js'
import { useExternalWalletProvider } from '../../providers/WalletProvider/useExternalWalletProvider.js'
import { useWidgetConfig } from '../../providers/WidgetProvider/WidgetProvider.js'

interface CloseDrawerButtonProps {
Expand All @@ -13,10 +13,11 @@ export const CloseDrawerButton = ({ header }: CloseDrawerButtonProps) => {
const { t } = useTranslation()
const { subvariant } = useWidgetConfig()
const { closeDrawer } = useDrawer()
const { hasExternalProvider } = useHasExternalWalletProvider()
const { useExternalWalletProvidersOnly } = useExternalWalletProvider()

const showInNavigationHeader =
header === 'navigation' && (hasExternalProvider || subvariant === 'split')
header === 'navigation' &&
(useExternalWalletProvidersOnly || subvariant === 'split')

const showInWalletHeader = header === 'wallet' && subvariant !== 'split'

Expand Down
Loading