Skip to content

Commit 506764d

Browse files
feat: support showAllWallets prop in React Native Connect UI (#4933)
1 parent a644912 commit 506764d

File tree

79 files changed

+1063
-218
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+1063
-218
lines changed

.changeset/cuddly-pandas-impress.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+
Support show all wallets option in React Native Connect UI

packages/thirdweb/scripts/wallets/generate.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,25 +127,33 @@ const allSupportedWallets = walletConnectSupportedWallets
127127
);
128128

129129
const walletInfos = allSupportedWallets.map((wallet) => {
130-
return { id: wallet.id, name: wallet.name };
130+
return {
131+
id: wallet.id,
132+
name: wallet.name,
133+
hasMobileSupport: !!wallet.mobile.universal || !!wallet.mobile.native,
134+
};
131135
});
132136

133137
const customWalletInfos = [
134138
{
135139
id: "smart",
136140
name: "Smart Wallet",
141+
hasMobileSupport: true,
137142
},
138143
{
139144
id: "inApp",
140145
name: "In-App Wallet",
146+
hasMobileSupport: true,
141147
},
142148
{
143149
id: "embedded",
144150
name: "In-App Wallet",
151+
hasMobileSupport: true,
145152
},
146153
{
147154
id: "walletConnect",
148155
name: "WalletConnect",
156+
hasMobileSupport: false,
149157
},
150158
];
151159

@@ -202,6 +210,7 @@ await writeFile(
202210
export type MinimalWalletInfo = {
203211
id: string;
204212
name: string;
213+
hasMobileSupport: boolean;
205214
};
206215
207216
/**
@@ -320,7 +329,13 @@ const walletImports = allSupportedWallets
320329
)
321330
.join("\n");
322331

323-
const customWalletImports = ["smart", "inApp", "walletConnect", "embedded", "adapter"]
332+
const customWalletImports = [
333+
"smart",
334+
"inApp",
335+
"walletConnect",
336+
"embedded",
337+
"adapter",
338+
]
324339
.map(
325340
(walletId) =>
326341
`case "${walletId}": {

packages/thirdweb/src/react/native/ui/components/input.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import { ThemedSpinner } from "./spinner.js";
1414

1515
type ThemedInputProps = {
1616
theme: Theme;
17+
leftView?: React.ReactNode;
1718
rightView?: React.ReactNode;
1819
} & TextInputProps;
1920

2021
export function ThemedInput(props: ThemedInputProps) {
21-
const { theme, rightView } = props;
22+
const { theme, leftView, rightView } = props;
2223
const [isFocused, setIsFocused] = useState(false);
2324
return (
2425
<View
@@ -31,9 +32,14 @@ export function ThemedInput(props: ThemedInputProps) {
3132
},
3233
]}
3334
>
35+
{leftView && leftView}
3436
<TextInput
3537
placeholderTextColor={theme.colors.secondaryText}
36-
style={[styles.input, { color: theme.colors.primaryText }]}
38+
style={[
39+
styles.input,
40+
{ color: theme.colors.primaryText },
41+
leftView ? { paddingLeft: 0 } : {},
42+
]}
3743
onFocus={() => setIsFocused(true)}
3844
onBlur={() => setIsFocused(false)}
3945
{...props}

packages/thirdweb/src/react/native/ui/connect/ConnectModal.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { ThemedText } from "../components/text.js";
2929
import { ThemedView } from "../components/view.js";
3030
import { TW_ICON, WALLET_ICON } from "../icons/svgs.js";
3131
import { ErrorView } from "./ErrorView.js";
32-
import { ExternalWalletsList } from "./ExternalWalletsList.js";
32+
import { AllWalletsList, ExternalWalletsList } from "./ExternalWalletsList.js";
3333
import { InAppWalletUI, OtpLogin, PasskeyView } from "./InAppWalletUI.js";
3434
import WalletLoadingThumbnail from "./WalletLoadingThumbnail.js";
3535

@@ -40,6 +40,7 @@ export type ModalState =
4040
| { screen: "otp"; auth: MultiStepAuthProviderType; wallet: Wallet<"inApp"> }
4141
| { screen: "passkey"; wallet: Wallet<"inApp"> }
4242
| { screen: "external_wallets" }
43+
| { screen: "all_wallets" }
4344
| { screen: "auth" };
4445

4546
/**
@@ -217,6 +218,34 @@ export function ConnectModal(
217218
client={client}
218219
connector={connector}
219220
containerType={containerType}
221+
showAllWalletsButton={props.showAllWallets !== false}
222+
onShowAllWallets={() => setModalState({ screen: "all_wallets" })}
223+
/>
224+
</>
225+
);
226+
break;
227+
}
228+
case "all_wallets": {
229+
content = (
230+
<>
231+
<Header
232+
theme={theme}
233+
onClose={props.onClose}
234+
containerType={containerType}
235+
onBack={() =>
236+
inAppWallet
237+
? setModalState({ screen: "external_wallets" })
238+
: setModalState({ screen: "base" })
239+
}
240+
title={props.connectModal?.title || "Select Wallet"}
241+
/>
242+
<Spacer size="lg" />
243+
<AllWalletsList
244+
theme={theme}
245+
externalWallets={externalWallets}
246+
client={client}
247+
connector={connector}
248+
containerType={containerType}
220249
/>
221250
</>
222251
);
@@ -402,6 +431,10 @@ export function ConnectModal(
402431
client={client}
403432
connector={connector}
404433
containerType={containerType}
434+
showAllWalletsButton={props.showAllWallets !== false}
435+
onShowAllWallets={() =>
436+
setModalState({ screen: "all_wallets" })
437+
}
405438
/>
406439
</View>
407440
</>

packages/thirdweb/src/react/native/ui/connect/ExternalWalletsList.tsx

Lines changed: 162 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import Fuse from "fuse.js";
2+
import { useMemo, useState } from "react";
13
import {
4+
FlatList,
25
Image,
36
Linking,
47
ScrollView,
@@ -8,13 +11,20 @@ import {
811
} from "react-native";
912
import type { Chain } from "../../../../chains/types.js";
1013
import type { ThirdwebClient } from "../../../../client/client.js";
14+
import walletInfos from "../../../../wallets/__generated__/wallet-infos.js";
1115
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
16+
import { createWallet } from "../../../../wallets/native/create-wallet.js";
17+
import type { WalletId } from "../../../../wallets/wallet-types.js";
1218
import type { Theme } from "../../../core/design-system/index.js";
1319
import { useWalletImage, useWalletInfo } from "../../../core/utils/wallet.js";
1420
import { spacing } from "../../design-system/index.js";
1521
import type { ContainerType } from "../components/Header.js";
22+
import { RNImage } from "../components/RNImage.js";
1623
import { Skeleton } from "../components/Skeleton.js";
24+
import { ThemedInput } from "../components/input.js";
25+
import { Spacer } from "../components/spacer.js";
1726
import { ThemedText } from "../components/text.js";
27+
import { SEARCH_ICON } from "../icons/svgs.js";
1828

1929
type ExternalWalletsUiProps = {
2030
theme: Theme;
@@ -27,9 +37,13 @@ type ExternalWalletsUiProps = {
2737
};
2838

2939
export function ExternalWalletsList(
30-
props: ExternalWalletsUiProps & { externalWallets: Wallet[] },
40+
props: ExternalWalletsUiProps & {
41+
externalWallets: Wallet[];
42+
showAllWalletsButton: boolean;
43+
onShowAllWallets: () => void;
44+
},
3145
) {
32-
const { connector, client, theme } = props;
46+
const { connector, client, theme, externalWallets, onShowAllWallets } = props;
3347
const connectWallet = (wallet: Wallet) => {
3448
connector({
3549
wallet,
@@ -42,32 +56,125 @@ export function ExternalWalletsList(
4256
},
4357
});
4458
};
45-
4659
return (
4760
<View style={styles.container}>
4861
<ScrollView
4962
style={{
5063
flex: 1,
5164
paddingHorizontal: props.containerType === "modal" ? spacing.lg : 0,
52-
paddingBottom: spacing.md,
5365
}}
5466
>
55-
<View style={{ flexDirection: "column", gap: spacing.md }}>
56-
{props.externalWallets.map((wallet) => (
67+
<View
68+
style={{
69+
flexDirection: "column",
70+
gap: spacing.md,
71+
paddingBottom: spacing.md,
72+
}}
73+
>
74+
{externalWallets.map((wallet) => (
5775
<ExternalWalletRow
5876
key={wallet.id}
5977
wallet={wallet}
6078
connectWallet={connectWallet}
6179
theme={theme}
6280
/>
6381
))}
82+
{props.showAllWalletsButton && (
83+
<ShowAllWalletsRow theme={theme} onPress={onShowAllWallets} />
84+
)}
6485
</View>
6586
</ScrollView>
6687
<NewToWallets theme={props.theme} containerType={props.containerType} />
6788
</View>
6889
);
6990
}
7091

92+
export function AllWalletsList(
93+
props: ExternalWalletsUiProps & { externalWallets: Wallet[] },
94+
) {
95+
const { connector, client, theme, externalWallets } = props;
96+
const [searchQuery, setSearchQuery] = useState("");
97+
98+
const walletsToShow = useMemo(() => {
99+
const filteredWallets = walletInfos
100+
.filter(
101+
(info) => !externalWallets.find((wallet) => wallet.id === info.id),
102+
)
103+
.filter((info) => info.hasMobileSupport);
104+
105+
const fuse = new Fuse(filteredWallets, {
106+
keys: ["name"],
107+
threshold: 0.3,
108+
});
109+
110+
return searchQuery
111+
? fuse.search(searchQuery).map((result) => result.item.id)
112+
: filteredWallets.map((info) => info.id);
113+
}, [externalWallets, searchQuery]);
114+
115+
const connectWallet = (wallet: Wallet) => {
116+
connector({
117+
wallet,
118+
connectFn: async (chain) => {
119+
await wallet.connect({
120+
client,
121+
chain,
122+
});
123+
return wallet;
124+
},
125+
});
126+
};
127+
128+
return (
129+
<View style={styles.container}>
130+
<View style={styles.searchContainer}>
131+
<ThemedInput
132+
theme={theme}
133+
leftView={
134+
<View
135+
style={{
136+
padding: spacing.sm,
137+
width: 48,
138+
flexDirection: "row",
139+
justifyContent: "center",
140+
}}
141+
>
142+
<RNImage
143+
data={SEARCH_ICON}
144+
size={24}
145+
theme={theme}
146+
color={theme.colors.secondaryIconColor}
147+
/>
148+
</View>
149+
}
150+
placeholder="Search Wallet"
151+
value={searchQuery}
152+
onChangeText={setSearchQuery}
153+
/>
154+
</View>
155+
<Spacer size="md" />
156+
<FlatList
157+
style={{
158+
flex: 1,
159+
paddingHorizontal: props.containerType === "modal" ? spacing.lg : 0,
160+
paddingBottom: spacing.md,
161+
}}
162+
data={walletsToShow}
163+
renderItem={({ item: walletId }) => (
164+
<ExternalWalletRow
165+
key={walletId}
166+
wallet={createWallet(walletId as WalletId)}
167+
connectWallet={connectWallet}
168+
theme={theme}
169+
/>
170+
)}
171+
keyExtractor={(walletId) => walletId}
172+
ItemSeparatorComponent={() => <View style={{ height: spacing.md }} />}
173+
/>
174+
</View>
175+
);
176+
}
177+
71178
function ExternalWalletRow(props: {
72179
theme: Theme;
73180
wallet: Wallet;
@@ -99,6 +206,48 @@ function ExternalWalletRow(props: {
99206
);
100207
}
101208

209+
function ShowAllWalletsRow(props: {
210+
theme: Theme;
211+
onPress: () => void;
212+
}) {
213+
const { theme, onPress } = props;
214+
return (
215+
<TouchableOpacity style={styles.row} onPress={onPress}>
216+
<View
217+
style={{
218+
width: 52,
219+
height: 52,
220+
flexDirection: "row",
221+
flexWrap: "wrap",
222+
justifyContent: "space-between",
223+
alignContent: "space-between",
224+
backgroundColor: theme.colors.secondaryButtonBg,
225+
borderColor: theme.colors.borderColor,
226+
borderWidth: 1,
227+
padding: 8,
228+
borderRadius: 6,
229+
}}
230+
>
231+
{[...Array(4)].map((_, index) => (
232+
<View
233+
// biome-ignore lint/suspicious/noArrayIndexKey: only have index as key
234+
key={index}
235+
style={{
236+
width: 14,
237+
height: 14,
238+
borderRadius: 7,
239+
backgroundColor: theme.colors.secondaryIconColor,
240+
}}
241+
/>
242+
))}
243+
</View>
244+
<ThemedText theme={theme} type="subtitle">
245+
Show all wallets
246+
</ThemedText>
247+
</TouchableOpacity>
248+
);
249+
}
250+
102251
function NewToWallets({
103252
theme,
104253
containerType,
@@ -144,4 +293,11 @@ const styles = StyleSheet.create({
144293
justifyContent: "flex-start",
145294
alignItems: "center",
146295
},
296+
searchContainer: {
297+
flexDirection: "row",
298+
gap: spacing.md,
299+
justifyContent: "flex-start",
300+
alignItems: "center",
301+
paddingHorizontal: spacing.lg,
302+
},
147303
});

0 commit comments

Comments
 (0)