Skip to content

Commit 2f9acb8

Browse files
feat: support showAllWallets prop in React Native Connect UI
1 parent 11de1f3 commit 2f9acb8

File tree

79 files changed

+1056
-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

+1056
-218
lines changed
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: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ 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 = [
@@ -202,6 +206,7 @@ await writeFile(
202206
export type MinimalWalletInfo = {
203207
id: string;
204208
name: string;
209+
hasMobileSupport: boolean;
205210
};
206211
207212
/**
@@ -320,7 +325,13 @@ const walletImports = allSupportedWallets
320325
)
321326
.join("\n");
322327

323-
const customWalletImports = ["smart", "inApp", "walletConnect", "embedded", "adapter"]
328+
const customWalletImports = [
329+
"smart",
330+
"inApp",
331+
"walletConnect",
332+
"embedded",
333+
"adapter",
334+
]
324335
.map(
325336
(walletId) =>
326337
`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: 163 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,22 @@ import {
811
} from "react-native";
912
import type { Chain } from "../../../../chains/types.js";
1013
import type { ThirdwebClient } from "../../../../client/client.js";
14+
import walletInfos, {
15+
type MinimalWalletInfo,
16+
} from "../../../../wallets/__generated__/wallet-infos.js";
1117
import type { Wallet } from "../../../../wallets/interfaces/wallet.js";
18+
import { createWallet } from "../../../../wallets/native/create-wallet.js";
19+
import type { WalletId } from "../../../../wallets/wallet-types.js";
1220
import type { Theme } from "../../../core/design-system/index.js";
1321
import { useWalletImage, useWalletInfo } from "../../../core/utils/wallet.js";
1422
import { spacing } from "../../design-system/index.js";
1523
import type { ContainerType } from "../components/Header.js";
24+
import { RNImage } from "../components/RNImage.js";
1625
import { Skeleton } from "../components/Skeleton.js";
26+
import { ThemedInput } from "../components/input.js";
27+
import { Spacer } from "../components/spacer.js";
1728
import { ThemedText } from "../components/text.js";
29+
import { SEARCH_ICON } from "../icons/svgs.js";
1830

1931
type ExternalWalletsUiProps = {
2032
theme: Theme;
@@ -27,9 +39,13 @@ type ExternalWalletsUiProps = {
2739
};
2840

2941
export function ExternalWalletsList(
30-
props: ExternalWalletsUiProps & { externalWallets: Wallet[] },
42+
props: ExternalWalletsUiProps & {
43+
externalWallets: Wallet[];
44+
showAllWalletsButton: boolean;
45+
onShowAllWallets: () => void;
46+
},
3147
) {
32-
const { connector, client, theme } = props;
48+
const { connector, client, theme, externalWallets, onShowAllWallets } = props;
3349
const connectWallet = (wallet: Wallet) => {
3450
connector({
3551
wallet,
@@ -42,32 +58,125 @@ export function ExternalWalletsList(
4258
},
4359
});
4460
};
45-
4661
return (
4762
<View style={styles.container}>
4863
<ScrollView
4964
style={{
5065
flex: 1,
5166
paddingHorizontal: props.containerType === "modal" ? spacing.lg : 0,
52-
paddingBottom: spacing.md,
5367
}}
5468
>
55-
<View style={{ flexDirection: "column", gap: spacing.md }}>
56-
{props.externalWallets.map((wallet) => (
69+
<View
70+
style={{
71+
flexDirection: "column",
72+
gap: spacing.md,
73+
paddingBottom: spacing.md,
74+
}}
75+
>
76+
{externalWallets.map((wallet) => (
5777
<ExternalWalletRow
5878
key={wallet.id}
5979
wallet={wallet}
6080
connectWallet={connectWallet}
6181
theme={theme}
6282
/>
6383
))}
84+
{props.showAllWalletsButton && (
85+
<ShowAllWalletsRow theme={theme} onPress={onShowAllWallets} />
86+
)}
6487
</View>
6588
</ScrollView>
6689
<NewToWallets theme={props.theme} containerType={props.containerType} />
6790
</View>
6891
);
6992
}
7093

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

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

packages/thirdweb/src/react/native/ui/icons/svgs.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,20 @@ export const COPY_ICON = `<svg
311311
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
312312
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
313313
</svg>`;
314+
315+
export const SEARCH_ICON = `<svg
316+
width="24"
317+
height="24"
318+
viewBox="0 0 24 24"
319+
fill="none"
320+
xmlns="http://www.w3.org/2000/svg"
321+
>
322+
<circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" />
323+
<path
324+
d="M21 21L15.5 15.5"
325+
stroke="currentColor"
326+
stroke-width="2"
327+
stroke-linecap="round"
328+
stroke-linejoin="round"
329+
/>
330+
</svg>`;

0 commit comments

Comments
 (0)