diff --git a/.changeset/pre.json b/.changeset/pre.json index 0f413a0..2927742 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "beta", "initialVersions": { "with-cra": "0.1.0", diff --git a/.gitignore b/.gitignore index 230e671..0e76bec 100644 --- a/.gitignore +++ b/.gitignore @@ -47,9 +47,11 @@ dist-ssr PROJECT_CONTEXT.md /.cursor +/.claude /scripts/* .envrc .biome/ +/CLAUDE.md diff --git a/packages/core/src/config/createConfig.ts b/packages/core/src/config/createConfig.ts index 647b1b5..659f314 100644 --- a/packages/core/src/config/createConfig.ts +++ b/packages/core/src/config/createConfig.ts @@ -2,6 +2,7 @@ import type { Chain, Config, Connector, + ConnectorGroup, CreateConfigParameters, RawStorage, Transport, @@ -36,11 +37,15 @@ function generateTransportsFromChains(chains: readonly Chain[]): Record 0 && 'groupName' in input[0] && 'wallets' in input[0]; +} + export function createConfig(parameters: CreateConfigParameters): Config { const { appName = 'My Luno App', chains = [], - connectors, + connectors: connectorsInput, transports = {}, storage = defaultLunoStorage, autoConnect = true, @@ -52,6 +57,14 @@ export function createConfig(parameters: CreateConfigParameters): Config { subscan, } = parameters; + const connectorGroups = isConnectorGroupArray(connectorsInput) + ? connectorsInput.filter((g) => g.wallets.length > 0) + : undefined; + + const connectors = isConnectorGroupArray(connectorsInput) + ? connectorsInput.flatMap((g) => g.wallets) + : connectorsInput; + if (!connectors || connectors.length === 0) { throw new Error('No connectors provided. Wallet connection features will be unavailable.'); } @@ -82,6 +95,9 @@ export function createConfig(parameters: CreateConfigParameters): Config { appName, chains: Object.freeze([...chains]) as readonly Chain[], connectors: Object.freeze([...connectors]) as readonly Connector[], + connectorGroups: connectorGroups + ? (Object.freeze([...connectorGroups]) as readonly ConnectorGroup[]) + : undefined, transports: Object.freeze({ ...finalTransports }) as Readonly>, storage, autoConnect, diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index fd102c9..dbd33e5 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -1,7 +1,7 @@ import type { ApiOptions } from 'dedot'; import type { AnyShape } from 'dedot/shape'; import type { Chain } from './chain'; -import type { Connector } from './connector'; +import type { Connector, ConnectorGroup } from './connector'; export interface RawStorage { getItem(key: string): string | null | Promise; @@ -26,7 +26,7 @@ type LunoApiOptions = Partial> & { export interface CreateConfigParameters extends LunoApiOptions { appName?: Optional; chains?: Optional; - connectors: Connector[]; + connectors: Connector[] | ConnectorGroup[]; transports?: Optional>; storage?: Optional; @@ -42,6 +42,7 @@ export interface Config extends LunoApiOptions { readonly appName: string; readonly chains: readonly Chain[]; readonly connectors: readonly Connector[]; + readonly connectorGroups?: readonly ConnectorGroup[]; readonly transports: Readonly>; readonly storage: LunoStorage; readonly autoConnect: boolean; diff --git a/packages/core/src/types/connector.ts b/packages/core/src/types/connector.ts index 760400b..62fe65f 100644 --- a/packages/core/src/types/connector.ts +++ b/packages/core/src/types/connector.ts @@ -47,3 +47,8 @@ export interface WalletConnectConnectorOptions { links?: Optional; supportedChains?: Optional; } + +export interface ConnectorGroup { + groupName: string; + wallets: Connector[]; +} diff --git a/packages/ui/src/components/ConnectModal/ConnectOptions.tsx b/packages/ui/src/components/ConnectModal/ConnectOptions.tsx index b25f4ea..f60e708 100644 --- a/packages/ui/src/components/ConnectModal/ConnectOptions.tsx +++ b/packages/ui/src/components/ConnectModal/ConnectOptions.tsx @@ -1,4 +1,4 @@ -import { useConnectors } from '@luno-kit/react'; +import { useConfig, useConnectors } from '@luno-kit/react'; import type { Connector } from '@luno-kit/react/types'; import { isMobileDevice } from '@luno-kit/react/utils'; import React, { useMemo } from 'react'; @@ -6,6 +6,7 @@ import { cs } from '../../utils'; interface Props { onConnect: (connector: Connector) => Promise; + showInstalledGroup: boolean; } const popularConnectorIds = [ @@ -19,33 +20,57 @@ const popularConnectorIds = [ const moreConnectorIds = ['ledger', 'polkagate', 'fearless-wallet', 'mimir', 'enkrypt', 'OneKey']; -export const ConnectOptions = React.memo(({ onConnect }: Props) => { +export const ConnectOptions = React.memo(({ onConnect, showInstalledGroup }: Props) => { const connectors = useConnectors(); + const config = useConfig(); const installedConnectors = connectors.filter((c) => c.isInstalled()); const popularConnectors = connectors.filter( - (c) => popularConnectorIds.includes(c.id) && !c.isInstalled() + (c: Connector) => popularConnectorIds.includes(c.id) && !c.isInstalled() ); const moreConnectors = connectors.filter( - (c) => moreConnectorIds.includes(c.id) && !c.isInstalled() + (c: Connector) => !popularConnectorIds.includes(c.id) && !c.isInstalled() ); const connectorGroup: { title: string; group: Connector[] }[] = useMemo(() => { + if (!config?.connectorGroups) { + return [ + { title: 'Installed', group: installedConnectors }, + { title: 'Popular', group: popularConnectors }, + { title: 'More', group: moreConnectors }, + ]; + } + + const hasUserDefinedInstalled = config.connectorGroups.some((g) => g.groupName === 'Installed'); + + if (hasUserDefinedInstalled || !showInstalledGroup) { + return config.connectorGroups + .filter((g) => g.wallets.length > 0) + .map((g) => ({ title: g.groupName, group: g.wallets })) + .sort((a, b) => (a.title === 'Installed' ? -1 : b.title === 'Installed' ? 1 : 0)); + } + + const allWallets = config.connectorGroups.flatMap((g) => g.wallets); + const installed = allWallets.filter((c) => c.isInstalled()); + + const customGroups = config.connectorGroups + .map((g) => ({ + title: g.groupName, + group: g.wallets.filter((c) => !c.isInstalled()), + })) + .filter((g) => g.group.length > 0); + return [ - { - title: 'Installed', - group: installedConnectors, - }, - { - title: 'Popular', - group: popularConnectors, - }, - { - title: 'More', - group: moreConnectors, - }, + ...(installed.length > 0 ? [{ title: 'Installed', group: installed }] : []), + ...customGroups, ]; - }, [installedConnectors, popularConnectors, moreConnectors]); + }, [ + installedConnectors, + popularConnectors, + moreConnectors, + config?.connectorGroups, + showInstalledGroup, + ]); if (isMobileDevice()) { const filteredConnectors = connectors diff --git a/packages/ui/src/components/ConnectModal/index.tsx b/packages/ui/src/components/ConnectModal/index.tsx index ca9fc53..8184cc1 100644 --- a/packages/ui/src/components/ConnectModal/index.tsx +++ b/packages/ui/src/components/ConnectModal/index.tsx @@ -22,11 +22,13 @@ export interface ConnectModalProps { size?: Optional; appInfo?: Optional>; container?: Optional; + showInstalledGroup?: Optional; } export const ConnectModal: React.FC = ({ appInfo, container, + showInstalledGroup = true, size = 'wide', }) => { const { isOpen, close } = useConnectModal(); @@ -86,7 +88,9 @@ export const ConnectModal: React.FC = ({ const viewComponents = useMemo(() => { return { - [ConnectModalView.connectOptions]: , + [ConnectModalView.connectOptions]: ( + + ), [ConnectModalView.walletView]: ( = ({ connectError, connectErrorMsg, appInfo, + showInstalledGroup, ]); useEffect(() => { diff --git a/packages/ui/src/providers/LunoKitProvider.tsx b/packages/ui/src/providers/LunoKitProvider.tsx index 3116bf5..fec6ded 100644 --- a/packages/ui/src/providers/LunoKitProvider.tsx +++ b/packages/ui/src/providers/LunoKitProvider.tsx @@ -32,6 +32,7 @@ export interface LunoKitProviderProps { config: LunoCoreConfig & { modalSize?: Optional; modalContainer?: Optional; + showInstalledGroup?: boolean; }; theme?: Optional; appInfo?: Optional>; @@ -52,6 +53,7 @@ export const LunoKitProvider: React.FC = ({ appInfo={appInfo} modalSize={config.modalSize} modalContainer={config.modalContainer} + showInstalledGroup={config.showInstalledGroup} /> @@ -63,16 +65,23 @@ interface RenderModalsProps { modalSize?: Optional; appInfo?: Optional>; modalContainer?: Optional; + showInstalledGroup?: Optional; } const RenderModals: React.FC = ({ modalSize, appInfo, modalContainer, + showInstalledGroup, }: RenderModalsProps) => { return ( <> - +