Skip to content

Commit 9e28754

Browse files
authored
Add "All wallets" in connect ui (#2560)
1 parent 095d227 commit 9e28754

28 files changed

+455
-1183
lines changed

.changeset/calm-plums-invite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Show "All wallets" in Connect UI

packages/thirdweb/scripts/wallets/generate.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ export type MinimalWalletInfo = {
141141
/**
142142
* @internal
143143
*/
144-
export const ALL_MINIMAL_WALLET_INFOS = <const>${JSON.stringify(walletInfos, null, 2)} satisfies MinimalWalletInfo[];
144+
const ALL_MINIMAL_WALLET_INFOS = <const>${JSON.stringify(walletInfos, null, 2)} satisfies MinimalWalletInfo[];
145+
146+
export default ALL_MINIMAL_WALLET_INFOS;
145147
`,
146148
{
147149
parser: "babel-ts",

packages/thirdweb/src/react/core/providers/wallet-connection.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export const WalletConnectionContext = /* @__PURE__ */ createContext<{
2020
projectId?: string;
2121
};
2222
accountAbstraction?: SmartWalletOptions;
23+
showAllWallets?: boolean;
2324
} | null>(null);

packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectWallet.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ export function ConnectButton(props: ConnectButtonProps) {
113113
chains: props.chains,
114114
walletConnect: props.walletConnect,
115115
accountAbstraction: props.accountAbstraction,
116+
recommendedWallets: props.recommendedWallets,
117+
showAllWallets: props.showAllWallets,
116118
}}
117119
>
118120
<WalletUIStatesProvider theme="dark">

packages/thirdweb/src/react/web/ui/ConnectWallet/ConnectWalletProps.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,4 +604,16 @@ export type ConnectButtonProps = {
604604
* />
605605
*/
606606
accountAbstraction?: SmartWalletOptions;
607+
608+
/**
609+
* Wallets to show as recommended in the `ConnectButton`'s Modal
610+
*/
611+
recommendedWallets?: Wallet[];
612+
613+
/**
614+
* By default, ConnectButton modal shows a "All Wallets" button that shows a list of 350+ wallets.
615+
*
616+
* You can disable this button by setting `showAllWallets` prop to `false`
617+
*/
618+
showAllWallets?: boolean;
607619
};
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { useContext, useMemo, useRef, useState } from "react";
2+
import type { Wallet } from "../../../../../wallets/interfaces/wallet.js";
3+
import { Spacer } from "../../components/Spacer.js";
4+
import { Container, ModalHeader } from "../../components/basic.js";
5+
import { Input } from "../../components/formElements.js";
6+
import { useDebouncedValue } from "../../hooks/useDebouncedValue.js";
7+
import { Spinner } from "../../components/Spinner.js";
8+
import { iconSize, spacing } from "../../design-system/index.js";
9+
import { useCustomTheme } from "../../design-system/CustomThemeProvider.js";
10+
import styled from "@emotion/styled";
11+
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
12+
import { WalletEntryButton } from "../WalletEntryButton.js";
13+
import { createWallet } from "../../../../../wallets/create-wallet.js";
14+
import { useShowMore } from "../../hooks/useShowMore.js";
15+
import { sortWallets } from "../../../utils/sortWallets.js";
16+
import { useWalletConnectionCtx } from "../../../../core/hooks/others/useWalletConnectionCtx.js";
17+
import { ModalConfigCtx } from "../../../providers/wallet-ui-states-provider.js";
18+
import Fuse from "fuse.js";
19+
import walletInfos from "../../../../../wallets/__generated__/wallet-infos.js";
20+
import { Text } from "../../components/text.js";
21+
import { CrossCircledIcon } from "@radix-ui/react-icons";
22+
23+
/**
24+
*
25+
* @internal
26+
*/
27+
function AllWalletsUI(props: {
28+
onBack: () => void;
29+
onSelect: (wallet: Wallet) => void;
30+
}) {
31+
const { recommendedWallets, wallets: specifiedWallets } =
32+
useWalletConnectionCtx();
33+
const { modalSize } = useContext(ModalConfigCtx);
34+
35+
const fuseInstance = useMemo(() => {
36+
return new Fuse(walletInfos, {
37+
threshold: 0.4,
38+
keys: [
39+
{
40+
name: "name",
41+
weight: 1,
42+
},
43+
],
44+
});
45+
}, []);
46+
47+
const listContainer = useRef<HTMLDivElement | null>(null);
48+
const [searchTerm, setSearchTerm] = useState("");
49+
const deferredSearchTerm = useDebouncedValue(searchTerm, 300);
50+
51+
const walletInfosWithSearch = deferredSearchTerm
52+
? fuseInstance.search(deferredSearchTerm).map((result) => result.item)
53+
: walletInfos;
54+
55+
const installedWalletsFirst = sortWallets(
56+
walletInfosWithSearch,
57+
recommendedWallets,
58+
);
59+
60+
// show specified wallets first
61+
const sortedWallets = useMemo(() => {
62+
return installedWalletsFirst.sort((a, b) => {
63+
const aIsSpecified = specifiedWallets.find((w) => w.id === a.id);
64+
const bIsSpecified = specifiedWallets.find((w) => w.id === b.id);
65+
if (aIsSpecified && !bIsSpecified) {
66+
return -1;
67+
}
68+
if (!aIsSpecified && bIsSpecified) {
69+
return 1;
70+
}
71+
return 0;
72+
});
73+
}, [installedWalletsFirst, specifiedWallets]);
74+
75+
const { itemsToShow, lastItemRef } = useShowMore<HTMLLIElement>(10, 10);
76+
77+
const walletInfosToShow = sortedWallets.slice(0, itemsToShow);
78+
79+
return (
80+
<Container fullHeight flex="column" animate="fadein">
81+
<Container p="lg">
82+
<ModalHeader title="Select Wallet" onBack={props.onBack} />
83+
</Container>
84+
85+
<Spacer y="xs" />
86+
87+
<Container px="lg">
88+
{/* Search */}
89+
<div
90+
style={{
91+
display: "flex",
92+
alignItems: "center",
93+
position: "relative",
94+
}}
95+
>
96+
<StyledMagnifyingGlassIcon width={iconSize.md} height={iconSize.md} />
97+
98+
<Input
99+
style={{
100+
padding: `${spacing.sm} ${spacing.md} ${spacing.sm} ${spacing.xxl}`,
101+
}}
102+
tabIndex={-1}
103+
variant="outline"
104+
placeholder={"Search Wallet"}
105+
value={searchTerm}
106+
onChange={(e) => {
107+
listContainer.current?.parentElement?.scroll({
108+
top: 0,
109+
});
110+
setSearchTerm(e.target.value);
111+
}}
112+
/>
113+
{/* Searching Spinner */}
114+
{deferredSearchTerm !== searchTerm && (
115+
<div
116+
style={{
117+
position: "absolute",
118+
right: spacing.md,
119+
}}
120+
>
121+
<Spinner size="md" color="accentText" />
122+
</div>
123+
)}
124+
</div>
125+
</Container>
126+
127+
{walletInfosToShow.length > 0 && (
128+
<>
129+
<Spacer y="md" />
130+
<Container animate="fadein" expand scrollY>
131+
<div
132+
ref={listContainer}
133+
style={{
134+
maxHeight: modalSize === "compact" ? "400px" : undefined,
135+
paddingInline: spacing.md,
136+
}}
137+
>
138+
{walletInfosToShow.map((walletInfo, i) => {
139+
const isLast = i === walletInfosToShow.length - 1;
140+
141+
return (
142+
<li
143+
ref={isLast ? lastItemRef : undefined}
144+
key={walletInfo.id}
145+
style={{
146+
listStyle: "none",
147+
}}
148+
>
149+
<WalletEntryButton
150+
walletId={walletInfo.id}
151+
selectWallet={() => {
152+
const wallet = createWallet(walletInfo.id);
153+
props.onSelect(wallet);
154+
}}
155+
/>
156+
</li>
157+
);
158+
})}
159+
</div>
160+
161+
<Spacer y="xl" />
162+
</Container>
163+
</>
164+
)}
165+
166+
{walletInfosToShow.length === 0 && (
167+
<Container
168+
flex="column"
169+
gap="md"
170+
center="both"
171+
color="secondaryText"
172+
animate="fadein"
173+
expand
174+
style={{
175+
minHeight: "250px",
176+
}}
177+
>
178+
<CrossCircledIcon width={iconSize.xl} height={iconSize.xl} />
179+
<Text> No Results </Text>
180+
</Container>
181+
)}
182+
</Container>
183+
);
184+
}
185+
186+
const StyledMagnifyingGlassIcon = /* @__PURE__ */ styled(MagnifyingGlassIcon)(
187+
() => {
188+
const theme = useCustomTheme();
189+
return {
190+
color: theme.colors.secondaryText,
191+
position: "absolute",
192+
left: spacing.sm,
193+
};
194+
},
195+
);
196+
197+
export default AllWalletsUI;

packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/ConnectEmbed.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,18 @@ export type ConnectEmbedProps = {
310310
* />
311311
*/
312312
accountAbstraction?: SmartWalletOptions;
313+
314+
/**
315+
* Wallets to show as recommended in the `ConnectEmbed` UI
316+
*/
317+
recommendedWallets?: Wallet[];
318+
319+
/**
320+
* By default, `ConnectEmbed` shows a "All Wallets" button that shows a list of 350+ wallets.
321+
*
322+
* You can disable this button by setting `showAllWallets` prop to `false`
323+
*/
324+
showAllWallets?: boolean;
313325
};
314326

315327
/**
@@ -457,6 +469,8 @@ export function ConnectEmbed(props: ConnectEmbedProps) {
457469
chains: props.chains,
458470
walletConnect: props.walletConnect,
459471
accountAbstraction: props.accountAbstraction,
472+
recommendedWallets: props.recommendedWallets,
473+
showAllWallets: props.showAllWallets,
460474
}}
461475
>
462476
<WalletUIStatesProvider {...walletUIStatesProps}>

packages/thirdweb/src/react/web/ui/ConnectWallet/Modal/ConnectModalContent.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
import {
2-
ModalConfigCtx,
3-
// SetModalConfigCtx,
4-
// SetModalConfigCtx,
5-
} from "../../../providers/wallet-ui-states-provider.js";
6-
import { useCallback, useContext } from "react";
1+
import { ModalConfigCtx } from "../../../providers/wallet-ui-states-provider.js";
2+
import { Suspense, lazy, useCallback, useContext } from "react";
73
import { reservedScreens, onModalUnmount } from "../constants.js";
8-
// import { HeadlessConnectUI } from "../../../wallets/headlessConnectUI.js";
94
import { ScreenSetupContext, type ScreenSetup } from "./screen.js";
105
import { StartScreen } from "../screens/StartScreen.js";
116
import { WalletSelector } from "../WalletSelector.js";
@@ -18,6 +13,9 @@ import { useConnect } from "../../../../core/hooks/wallets/wallet-hooks.js";
1813
import { useWalletConnectionCtx } from "../../../../core/hooks/others/useWalletConnectionCtx.js";
1914
import { AnyWalletConnectUI } from "./AnyWalletConnectUI.js";
2015
import { SmartConnectUI } from "./SmartWalletConnectUI.js";
16+
import { LoadingScreen } from "../../../wallets/shared/LoadingScreen.js";
17+
18+
const AllWalletsUI = /* @__PURE__ */ lazy(() => import("./AllWalletsUI.js"));
2119

2220
/**
2321
* @internal
@@ -111,11 +109,20 @@ export const ConnectModalContent = (props: {
111109
setScreen(reservedScreens.getStarted);
112110
}}
113111
selectWallet={setScreen}
112+
onShowAll={() => {
113+
setScreen(reservedScreens.showAll);
114+
}}
114115
done={handleConnected}
115116
goBack={wallets.length > 1 ? handleBack : undefined}
116117
/>
117118
);
118119

120+
const showAll = (
121+
<Suspense fallback={<LoadingScreen />}>
122+
<AllWalletsUI onBack={handleBack} onSelect={setScreen} />
123+
</Suspense>
124+
);
125+
119126
const getStarted = <StartScreen />;
120127

121128
const goBack = wallets.length > 1 ? handleBack : undefined;
@@ -127,7 +134,6 @@ export const ConnectModalContent = (props: {
127134
key={wallet.id}
128135
accountAbstraction={accountAbstraction}
129136
done={(smartWallet) => {
130-
console.log("connected smart wallet");
131137
handleConnected(smartWallet);
132138
}}
133139
personalWallet={wallet}
@@ -167,6 +173,7 @@ export const ConnectModalContent = (props: {
167173
{/* {screen === reservedScreens.signIn && signatureScreen} */}
168174
{screen === reservedScreens.main && <>{getStarted}</>}
169175
{screen === reservedScreens.getStarted && getStarted}
176+
{screen === reservedScreens.showAll && showAll}
170177
{typeof screen !== "string" && getWalletUI(screen)}
171178
</>
172179
}
@@ -176,6 +183,7 @@ export const ConnectModalContent = (props: {
176183
{/* {screen === reservedScreens.signIn && signatureScreen} */}
177184
{screen === reservedScreens.main && walletList}
178185
{screen === reservedScreens.getStarted && getStarted}
186+
{screen === reservedScreens.showAll && showAll}
179187
{typeof screen !== "string" && getWalletUI(screen)}
180188
</ConnectModalCompactLayout>
181189
)}

packages/thirdweb/src/react/web/ui/ConnectWallet/NetworkSelector.tsx

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type { ChainMetadata, Chain } from "../../../../chains/types.js";
3333
import { convertApiChainToChain } from "../../../../chains/utils.js";
3434
import { useWalletConnectionCtx } from "../../../core/hooks/others/useWalletConnectionCtx.js";
3535
import { useDebouncedValue } from "../hooks/useDebouncedValue.js";
36+
import { useShowMore } from "../hooks/useShowMore.js";
3637

3738
type NetworkSelectorChainProps = {
3839
/**
@@ -801,33 +802,3 @@ const StyledMagnifyingGlassIcon = /* @__PURE__ */ styled(MagnifyingGlassIcon)(
801802
};
802803
},
803804
);
804-
805-
function useShowMore<T extends HTMLElement>(
806-
initialItemsToShow: number,
807-
itemsToAdd: number,
808-
) {
809-
// start with showing first 10 items, when the last item is in view, show 10 more
810-
const [itemsToShow, setItemsToShow] = useState(initialItemsToShow);
811-
const lastItemRef = useCallback(
812-
(node: T) => {
813-
if (!node) {
814-
return;
815-
}
816-
817-
const observer = new IntersectionObserver(
818-
(entries) => {
819-
if (entries[0] && entries[0].isIntersecting) {
820-
setItemsToShow((prev) => prev + itemsToAdd); // show 10 more items
821-
}
822-
},
823-
{ threshold: 1 },
824-
);
825-
826-
observer.observe(node);
827-
// when the node is removed from the DOM, observer will be disconnected automatically by the browser
828-
},
829-
[itemsToAdd],
830-
);
831-
832-
return { itemsToShow, lastItemRef };
833-
}

0 commit comments

Comments
 (0)