├── .env.example
├── .eslintrc
├── .gitignore
├── .prettierignore
├── .prettierrc
├── App.tsx
├── README.md
├── app.config.js
├── app.json
├── assets
├── adaptive-icon.png
├── favicon.png
├── icon.png
├── splash.png
└── svg
│ ├── eth-logo.svg
│ ├── matic-logo.svg
│ ├── uni-logo.svg
│ └── usdc-logo.svg
├── babel.config.js
├── demo.gif
├── package.json
├── polyfills.ts
├── polyfills.web.ts
├── src
├── components
│ ├── Logo
│ │ └── index.tsx
│ ├── Separator
│ │ └── index.tsx
│ └── Text
│ │ └── index.tsx
├── constants.ts
├── features
│ ├── balance
│ │ ├── BalanceItem.tsx
│ │ └── BalanceScreen.tsx
│ ├── bookmarks
│ │ └── BookmarksScreen.tsx
│ ├── home
│ │ ├── HomeScreen.tsx
│ │ ├── NftList.tsx
│ │ └── NftListItem.tsx
│ ├── messages
│ │ └── MessagesScreen.tsx
│ ├── notifications
│ │ └── NotificationsScreen.tsx
│ └── profile
│ │ └── ProfileScreen.tsx
├── navigation
│ ├── DrawerContent.tsx
│ ├── NavIcon.tsx
│ ├── RootNavigator.tsx
│ ├── TabHeader.tsx
│ └── index.ts
├── services
│ └── alchemy.ts
├── theme.ts
└── web3modal
│ ├── common.ts
│ ├── index.ts
│ └── index.web.tsx
├── tsconfig.json
└── yarn.lock
/.env.example:
--------------------------------------------------------------------------------
1 | EXPO_PUBLIC_WALLETCONNECT_CLOUD_PROJECT_ID="YOUR WALLETCONNECT CLOUD PROJECT ID HERE"
2 | EXPO_PUBLIC_ALCHEMY_API_KEY="YOUR ALCHEMY API KEY HERE"
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@callstack"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 | .env
34 |
35 | # typescript
36 | *.tsbuildinfo
37 |
38 | # JetBrains
39 | .idea
40 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | # JetBrains
38 | .idea
39 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "bracketSameLine": true,
4 | "bracketSpacing": true,
5 | "singleQuote": true,
6 | "trailingComma": "all"
7 | }
8 |
--------------------------------------------------------------------------------
/App.tsx:
--------------------------------------------------------------------------------
1 | import './polyfills';
2 | import 'react-native-gesture-handler';
3 |
4 | import React from 'react';
5 | import { WagmiConfig } from 'wagmi';
6 | import { StatusBar } from 'expo-status-bar';
7 | import { NavigationContainer } from '@react-navigation/native';
8 | import { SafeAreaProvider } from 'react-native-safe-area-context';
9 | import { Web3Modal, wagmiConfig } from './src/web3modal';
10 | import RootNavigator from './src/navigation/RootNavigator';
11 | import { reactNavigationTheme } from './src/theme';
12 |
13 | export default function App() {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # web3-cross-platform-dapp
2 |
3 | This repository showcases how to build a modern cross-platform (web / iOS / Android) dApp with [Expo](https://expo.dev), [React Native for Web](https://necolas.github.io/react-native-web/), [wagmi](https://wagmi.sh) and [WalletConnect's Web3Modal](https://docs.walletconnect.com/web3modal/about)
4 |
5 | Read the article for a more detailed explanation: [Cross-Platform Web3 dApps With React Native
6 | ](https://www.callstack.com/blog/cross-platform-web3-dapps-with-react-native)
7 |
8 | 
9 |
10 | ## Requirements
11 |
12 | - [Expo environment setup](https://docs.expo.dev/get-started/installation/#requirements) (Node.js, Git, Watchman)
13 | - A [Wallet Connect Cloud](https://cloud.walletconnect.com/sign-in) project ID
14 | - An [Alchemy](https://www.alchemy.com/) API key
15 | - Expo Go app installed in your smartphone
16 | - One or more web3 wallets installed in your smartphone (e.g. MetaMask, Rainbow Wallet, Trust Wallet, etc)
17 | - One or more web3 wallets installed in your browser (e.g. MetaMask, Rainbow Wallet, Trust Wallet, etc)
18 |
19 | ## How to run
20 |
21 | - Rename `.env.example` to `.env` and fill in your Wallet Connect Cloud project ID, and Alchemy API key
22 | - `yarn install`
23 |
24 | ### Mobile
25 |
26 | - `yarn start`
27 | - Open Expo Go app in your smartphone
28 | - If your smartphone is in the same network as your computer, the local dev server should appear as the first option. If it doesn't, use the app to scan the QR Code presented in the terminal
29 |
30 | ### Web
31 |
32 | - `yarn web`
33 | - Open `http://localhost:19006`
34 |
--------------------------------------------------------------------------------
/app.config.js:
--------------------------------------------------------------------------------
1 | // Expo web is not replacing process.env variables.
2 | // Issue: https://github.com/expo/expo/issues/23812
3 | const env = Object.fromEntries(
4 | Object.entries(process.env).filter(([key]) => key.startsWith('EXPO_PUBLIC_')),
5 | );
6 |
7 | module.exports = ({ config }) => {
8 | return {
9 | ...config,
10 | extra: { ...env },
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "web3-cross-platform-dapp",
4 | "slug": "web3-cross-platform-dapp",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "light",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "assetBundlePatterns": [
15 | "**/*"
16 | ],
17 | "ios": {
18 | "supportsTablet": true,
19 | "infoPlist": {
20 | "LSApplicationQueriesSchemes": [
21 | "metamask",
22 | "trust",
23 | "safe",
24 | "rainbow",
25 | "uniswap"
26 | ]
27 | }
28 | },
29 | "android": {
30 | "adaptiveIcon": {
31 | "foregroundImage": "./assets/adaptive-icon.png",
32 | "backgroundColor": "#ffffff"
33 | }
34 | },
35 | "web": {
36 | "favicon": "./assets/favicon.png"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callstack/web3-cross-platform-dapp/5067d2fe72c7e51454c187fb18f1afb2e66bbc86/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callstack/web3-cross-platform-dapp/5067d2fe72c7e51454c187fb18f1afb2e66bbc86/assets/favicon.png
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callstack/web3-cross-platform-dapp/5067d2fe72c7e51454c187fb18f1afb2e66bbc86/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callstack/web3-cross-platform-dapp/5067d2fe72c7e51454c187fb18f1afb2e66bbc86/assets/splash.png
--------------------------------------------------------------------------------
/assets/svg/eth-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
22 |
--------------------------------------------------------------------------------
/assets/svg/matic-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
--------------------------------------------------------------------------------
/assets/svg/uni-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
49 |
--------------------------------------------------------------------------------
/assets/svg/usdc-logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: ['react-native-reanimated/plugin'],
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/callstack/web3-cross-platform-dapp/5067d2fe72c7e51454c187fb18f1afb2e66bbc86/demo.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web3-cross-platform-dapp",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "@expo/vector-icons": "^13.0.0",
13 | "@react-native-async-storage/async-storage": "1.18.2",
14 | "@react-native-community/netinfo": "9.3.10",
15 | "@react-navigation/bottom-tabs": "^7.0.0-alpha.7",
16 | "@react-navigation/drawer": "^7.0.0-alpha.7",
17 | "@react-navigation/native": "^7.0.0-alpha.6",
18 | "@react-navigation/native-stack": "^7.0.0-alpha.7",
19 | "@walletconnect/react-native-compat": "^2.10.6",
20 | "@web3modal/wagmi": "^3.3.2",
21 | "@web3modal/wagmi-react-native": "^1.0.1",
22 | "alchemy-sdk": "^3.0.0",
23 | "expo": "~49.0.15",
24 | "expo-constants": "~14.4.2",
25 | "expo-image": "~1.3.5",
26 | "expo-status-bar": "~1.6.0",
27 | "react": "18.2.0",
28 | "react-dom": "18.2.0",
29 | "react-native": "0.72.6",
30 | "react-native-gesture-handler": "~2.12.0",
31 | "react-native-get-random-values": "~1.9.0",
32 | "react-native-modal": "^13.0.1",
33 | "react-native-reanimated": "~3.3.0",
34 | "react-native-safe-area-context": "4.6.3",
35 | "react-native-screens": "~3.22.0",
36 | "react-native-svg": "13.9.0",
37 | "react-native-web": "~0.19.6",
38 | "viem": "^1.20.1",
39 | "wagmi": "^1.4.12"
40 | },
41 | "devDependencies": {
42 | "@babel/core": "^7.20.0",
43 | "@callstack/eslint-config": "^14.0.0",
44 | "@expo/webpack-config": "^19.0.0",
45 | "@types/react": "~18.2.14",
46 | "eslint": "^8.52.0",
47 | "eslint-config-prettier": "^9.0.0",
48 | "prettier": "^3.0.3",
49 | "typescript": "^5.1.3"
50 | },
51 | "resolutions": {
52 | "valtio": "1.11.2"
53 | },
54 | "private": true
55 | }
56 |
--------------------------------------------------------------------------------
/polyfills.ts:
--------------------------------------------------------------------------------
1 | // ⚠️ Important: `@walletconnect/react-native-compat` needs to be imported before other `wagmi` packages.
2 | // This is because it applies a polyfill necessary for the TextEncoder API.
3 | import '@walletconnect/react-native-compat';
4 |
5 | // Polyfills for Alchemy SDK
6 | if (typeof btoa === 'undefined') {
7 | global.btoa = function (str) {
8 | return new Buffer(str, 'binary').toString('base64');
9 | };
10 | }
11 |
12 | if (typeof atob === 'undefined') {
13 | global.atob = function (b64Encoded) {
14 | return new Buffer(b64Encoded, 'base64').toString('binary');
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/polyfills.web.ts:
--------------------------------------------------------------------------------
1 | // No polyfills needed for web
2 |
--------------------------------------------------------------------------------
/src/components/Logo/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet } from 'react-native';
3 | import Text from '../Text';
4 | import { theme } from '../../theme';
5 |
6 | function Logo() {
7 | return Native3;
8 | }
9 |
10 | const styles = StyleSheet.create({
11 | logo: {
12 | fontWeight: '900',
13 | fontStyle: 'italic',
14 | letterSpacing: -2.5,
15 | fontSize: 24,
16 | color: theme.colors.primary,
17 | },
18 | });
19 |
20 | export default Logo;
21 |
--------------------------------------------------------------------------------
/src/components/Separator/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 |
4 | function Separator() {
5 | return ;
6 | }
7 |
8 | const styles = StyleSheet.create({
9 | separator: {
10 | height: 20,
11 | },
12 | });
13 |
14 | export default Separator;
15 |
--------------------------------------------------------------------------------
/src/components/Text/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text as RNText, TextProps as RNTextProps } from 'react-native';
3 | import { theme } from '../../theme';
4 |
5 | type TextProps = RNTextProps & {
6 | disabled?: boolean;
7 | color?: string;
8 | };
9 |
10 | const Text = ({
11 | disabled = false,
12 | color = theme.colors.text,
13 | ...props
14 | }: TextProps) => {
15 | return (
16 |
24 | );
25 | };
26 |
27 | export default Text;
28 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | // Base URL for the OpenSea marketplace
2 | export const OPENSEA_BASE_URL = 'https://opensea.io/assets/ethereum';
3 |
4 | // Address of the USDC token on Ethereum Mainnet
5 | export const USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
6 |
7 | // Address of the LINK token on Ethereum Mainnet
8 | export const LINK_ADDRESS = '0x514910771af9ca656af840dff83e8264ecf986ca';
9 |
10 | // Address of the MATIC token on Ethereum Mainnet
11 | export const MATIC_ADDRESS = '0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0';
12 |
13 | // Address of the UNI token on Ethereum Mainnet
14 | export const UNI_ADDRESS = '0x1f9840a85d5af5bf1d1762f925bdaddc4201f984';
15 |
--------------------------------------------------------------------------------
/src/features/balance/BalanceItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { Image } from 'expo-image';
4 | import Text from '../../components/Text';
5 | import { theme } from '../../theme';
6 |
7 | type BalanceItemProps = {
8 | balance: string;
9 | symbol: string;
10 | iconSource: string;
11 | };
12 |
13 | function BalanceItem({ balance, symbol, iconSource }: BalanceItemProps) {
14 | return (
15 |
16 |
22 |
23 | {Number(balance).toFixed(3)} {symbol}
24 |
25 |
26 | );
27 | }
28 |
29 | const styles = StyleSheet.create({
30 | container: {
31 | flex: 1,
32 | flexDirection: 'row',
33 | alignItems: 'center',
34 | maxHeight: 150,
35 | gap: 20,
36 | padding: 20,
37 | borderWidth: 1,
38 | borderColor: theme.colors.border,
39 | borderRadius: 12,
40 | },
41 | icon: {
42 | width: 50,
43 | height: 50,
44 | },
45 | });
46 |
47 | export default BalanceItem;
48 |
--------------------------------------------------------------------------------
/src/features/balance/BalanceScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { polygon } from 'viem/chains';
4 | import { useAccount, useBalance } from 'wagmi';
5 | import { UNI_ADDRESS, USDC_ADDRESS } from '../../constants';
6 | import Text from '../../components/Text';
7 | import BalanceItem from './BalanceItem';
8 |
9 | export default function BalanceScreen() {
10 | const { address, isConnected } = useAccount();
11 |
12 | const { data: balanceETH } = useBalance({
13 | address,
14 | formatUnits: 'ether',
15 | });
16 | const { data: balanceUSDC } = useBalance({
17 | address,
18 | token: USDC_ADDRESS,
19 | });
20 | const { data: balanceUNI } = useBalance({
21 | address,
22 | token: UNI_ADDRESS,
23 | });
24 | const { data: balanceMATIC } = useBalance({
25 | address,
26 | chainId: polygon.id,
27 | });
28 |
29 | if (!isConnected) {
30 | return (
31 |
32 | Connect wallet to display balances
33 |
34 | );
35 | }
36 |
37 | return (
38 |
39 | {balanceETH && (
40 |
45 | )}
46 | {balanceUSDC && (
47 |
52 | )}
53 | {balanceUNI && (
54 |
59 | )}
60 | {balanceMATIC && (
61 |
66 | )}
67 |
68 | );
69 | }
70 |
71 | const styles = StyleSheet.create({
72 | container: {
73 | flex: 1,
74 | gap: 20,
75 | paddingVertical: 20,
76 | marginHorizontal: 20,
77 | },
78 | emptyContainer: {
79 | flex: 1,
80 | alignItems: 'center',
81 | justifyContent: 'center',
82 | },
83 | });
84 |
--------------------------------------------------------------------------------
/src/features/bookmarks/BookmarksScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import Text from '../../components/Text';
4 |
5 | export default function BookmarksScreen() {
6 | return (
7 |
8 | Bookmarks
9 |
10 | );
11 | }
12 |
13 | const styles = StyleSheet.create({
14 | container: {
15 | flex: 1,
16 | alignItems: 'center',
17 | justifyContent: 'center',
18 | marginHorizontal: 20,
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/src/features/home/HomeScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button, StyleSheet, View } from 'react-native';
3 | import { useAccount, useSignMessage } from 'wagmi';
4 | import Text from '../../components/Text';
5 | import { theme } from '../../theme';
6 | import NftList from './NftList';
7 |
8 | export default function HomeScreen() {
9 | const { isConnected } = useAccount();
10 | const { isSuccess: isSigned, signMessage } = useSignMessage({
11 | message: 'Sign this message to prove you are the owner of this wallet',
12 | });
13 |
14 | if (!isConnected) {
15 | return (
16 |
17 | Connect wallet to display NFTs
18 |
19 | );
20 | }
21 |
22 | if (!isSigned) {
23 | return (
24 |
25 |
31 | );
32 | }
33 |
34 | return ;
35 | }
36 |
37 | const styles = StyleSheet.create({
38 | container: {
39 | flex: 1,
40 | alignItems: 'center',
41 | justifyContent: 'center',
42 | marginHorizontal: 20,
43 | },
44 | });
45 |
--------------------------------------------------------------------------------
/src/features/home/NftList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FlatList, Platform, StyleSheet, View } from 'react-native';
3 | import { useAccount } from 'wagmi';
4 | import Separator from '../../components/Separator';
5 | import { useNftsForAddress } from '../../services/alchemy';
6 | import Text from '../../components/Text';
7 | import NftListItem from './NftListItem';
8 |
9 | const NUM_COLUMNS = Platform.OS === 'web' ? 4 : 1;
10 |
11 | function NftList() {
12 | const { address } = useAccount();
13 | let { nfts, isLoading } = useNftsForAddress(address);
14 |
15 | const renderItem = ({ item }) => {
16 | // Dynamically calculating width to avoid items having different widths
17 | // if the last row is not full. Needed because the columns are generated
18 | // by the FlatList itself.
19 | const width: `${number}%` =
20 | NUM_COLUMNS > 1 ? `${90 / NUM_COLUMNS}%` : '100%';
21 |
22 | return ;
23 | };
24 |
25 | if (nfts.length === 0) {
26 | return (
27 |
28 | {isLoading ? (
29 | Loading...
30 | ) : (
31 | No NFTs were found for address: {address}
32 | )}
33 |
34 | );
35 | }
36 |
37 | return (
38 | `${item.contract?.address}-${item.tokenId}`}
43 | contentContainerStyle={styles.contentContainerStyle}
44 | columnWrapperStyle={NUM_COLUMNS > 1 ? styles.columnWrapper : undefined}
45 | ItemSeparatorComponent={Separator}
46 | />
47 | );
48 | }
49 |
50 | const styles = StyleSheet.create({
51 | contentContainerStyle: {
52 | paddingVertical: 20,
53 | paddingHorizontal: '5%',
54 | },
55 | columnWrapper: {
56 | columnGap: 20,
57 | },
58 | emptyContainer: {
59 | flex: 1,
60 | alignItems: 'center',
61 | justifyContent: 'center',
62 | },
63 | });
64 |
65 | export default NftList;
66 |
--------------------------------------------------------------------------------
/src/features/home/NftListItem.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Linking, Pressable, StyleSheet, View, ViewStyle } from 'react-native';
3 | import { Nft } from 'alchemy-sdk';
4 | import { Image } from 'expo-image';
5 | import Text from '../../components/Text';
6 | import { theme } from '../../theme';
7 | import { OPENSEA_BASE_URL } from '../../constants';
8 |
9 | type NftListItemProps = {
10 | nft: Nft;
11 | style: ViewStyle;
12 | };
13 |
14 | function NftListItem({ nft, style }: NftListItemProps) {
15 | const openMarketplace = () => {
16 | void Linking.openURL(
17 | `${OPENSEA_BASE_URL}/${nft.contract.address}/${nft.tokenId}`,
18 | );
19 | };
20 |
21 | return (
22 |
26 | {/* @ts-expect-error: react-native-web does not export TS typings for the `hovered` argument */}
27 | {({ hovered, pressed }) => (
28 |
29 |
34 |
35 |
36 | {nft.name}
37 |
38 | {nft.collection?.name && (
39 | {nft.collection.name}
40 | )}
41 |
42 |
43 | )}
44 |
45 | );
46 | }
47 |
48 | const styles = StyleSheet.create({
49 | container: {
50 | borderWidth: 2,
51 | borderRadius: 12,
52 | borderColor: theme.colors.border,
53 | },
54 | pressed: {
55 | opacity: 0.8,
56 | },
57 | nameContainer: {
58 | justifyContent: 'center',
59 | gap: 4,
60 | padding: 10,
61 | },
62 | name: {
63 | fontSize: 16,
64 | fontWeight: 'bold',
65 | lineHeight: 30,
66 | },
67 | image: {
68 | height: 250,
69 | borderTopLeftRadius: 12,
70 | borderTopRightRadius: 12,
71 | },
72 | });
73 |
74 | export default NftListItem;
75 |
--------------------------------------------------------------------------------
/src/features/messages/MessagesScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import Text from '../../components/Text';
4 |
5 | export default function MessagesScreen() {
6 | return (
7 |
8 | Messages
9 |
10 | );
11 | }
12 |
13 | const styles = StyleSheet.create({
14 | container: {
15 | flex: 1,
16 | alignItems: 'center',
17 | justifyContent: 'center',
18 | marginHorizontal: 20,
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/src/features/notifications/NotificationsScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import Text from '../../components/Text';
4 |
5 | export default function NotificationsScreen() {
6 | return (
7 |
8 | Notifications
9 |
10 | );
11 | }
12 |
13 | const styles = StyleSheet.create({
14 | container: {
15 | flex: 1,
16 | alignItems: 'center',
17 | justifyContent: 'center',
18 | marginHorizontal: 20,
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/src/features/profile/ProfileScreen.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import Text from '../../components/Text';
4 |
5 | export default function ProfileScreen() {
6 | return (
7 |
8 | Profile
9 |
10 | );
11 | }
12 |
13 | const styles = StyleSheet.create({
14 | container: {
15 | flex: 1,
16 | alignItems: 'center',
17 | justifyContent: 'center',
18 | marginHorizontal: 20,
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/src/navigation/DrawerContent.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | DrawerContentScrollView,
4 | DrawerItemList,
5 | } from '@react-navigation/drawer';
6 | import { StyleSheet, View } from 'react-native';
7 | import { W3mButton } from '../web3modal';
8 | import Logo from '../components/Logo';
9 |
10 | function CustomDrawerContent(props) {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
24 | const styles = StyleSheet.create({
25 | logoContainer: {
26 | marginVertical: 20,
27 | marginLeft: 24,
28 | },
29 | w3mButton: {
30 | marginTop: 20,
31 | marginLeft: 18,
32 | },
33 | });
34 |
35 | export default CustomDrawerContent;
36 |
--------------------------------------------------------------------------------
/src/navigation/NavIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import Octicons from '@expo/vector-icons/Octicons';
4 | import { theme } from '../theme';
5 | import { ParamList } from './index';
6 |
7 | type NavIconProps = {
8 | screenName: keyof ParamList;
9 | focused: boolean;
10 | };
11 |
12 | export function getIconForRoute(
13 | routeName: string,
14 | ): keyof typeof Octicons.glyphMap {
15 | switch (routeName) {
16 | case 'Home':
17 | return 'apps';
18 | case 'Balance':
19 | return 'ruby';
20 | case 'Notifications':
21 | return 'bell';
22 | case 'Messages':
23 | return 'mail';
24 | case 'Bookmarks':
25 | return 'bookmark';
26 | case 'Profile':
27 | return 'person';
28 | default:
29 | return 'question';
30 | }
31 | }
32 |
33 | function NavIcon({ screenName, focused }: NavIconProps) {
34 | return (
35 |
36 |
41 |
42 | );
43 | }
44 |
45 | const styles = StyleSheet.create({
46 | container: {
47 | width: 32,
48 | height: 32,
49 | alignItems: 'center',
50 | justifyContent: 'center',
51 | },
52 | });
53 |
54 | export default NavIcon;
55 |
--------------------------------------------------------------------------------
/src/navigation/RootNavigator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Platform } from 'react-native';
3 | import HomeScreen from '../features/home/HomeScreen';
4 | import BalanceScreen from '../features/balance/BalanceScreen';
5 | import NotificationsScreen from '../features/notifications/NotificationsScreen';
6 | import MessagesScreen from '../features/messages/MessagesScreen';
7 | import BookmarksScreen from '../features/bookmarks/BookmarksScreen';
8 | import ProfileScreen from '../features/profile/ProfileScreen';
9 | import NavIcon from './NavIcon';
10 | import DrawerContent from './DrawerContent';
11 | import TabHeader from './TabHeader';
12 | import { RootDrawer, RootTab } from './index';
13 |
14 | const screens = [
15 | {
16 | name: 'Home',
17 | component: HomeScreen,
18 | },
19 | {
20 | name: 'Balance',
21 | component: BalanceScreen,
22 | },
23 | {
24 | name: 'Notifications',
25 | component: NotificationsScreen,
26 | },
27 | {
28 | name: 'Messages',
29 | component: MessagesScreen,
30 | },
31 | {
32 | name: 'Bookmarks',
33 | component: BookmarksScreen,
34 | },
35 | {
36 | name: 'Profile',
37 | component: ProfileScreen,
38 | },
39 | ] as const;
40 |
41 | const RootNavigator = () => {
42 | if (Platform.OS === 'web') {
43 | return (
44 | null,
48 | drawerType: 'permanent',
49 | }}>
50 | {screens.map(screen => (
51 | (
57 |
58 | ),
59 | }}
60 | />
61 | ))}
62 |
63 | );
64 | }
65 |
66 | return (
67 | ,
70 | }}>
71 | {screens.map(screen => (
72 | null,
78 | tabBarIcon: ({ focused }) => (
79 |
80 | ),
81 | }}
82 | />
83 | ))}
84 |
85 | );
86 | };
87 |
88 | export default RootNavigator;
89 |
--------------------------------------------------------------------------------
/src/navigation/TabHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { StyleSheet, View } from 'react-native';
3 | import { useSafeAreaInsets } from 'react-native-safe-area-context';
4 | import { W3mButton } from '../web3modal';
5 | import Logo from '../components/Logo';
6 |
7 | function TabHeader() {
8 | const insets = useSafeAreaInsets();
9 |
10 | return (
11 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | const styles = StyleSheet.create({
29 | container: {
30 | flexDirection: 'row',
31 | alignItems: 'center',
32 | marginHorizontal: 20,
33 | },
34 | w3mButton: {
35 | marginLeft: 'auto',
36 | },
37 | });
38 |
39 | export default TabHeader;
40 |
--------------------------------------------------------------------------------
/src/navigation/index.ts:
--------------------------------------------------------------------------------
1 | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
2 | import { createDrawerNavigator } from '@react-navigation/drawer';
3 |
4 | export type ParamList = {
5 | Home: undefined;
6 | Balance: undefined;
7 | Notifications: undefined;
8 | Messages: undefined;
9 | Bookmarks: undefined;
10 | Profile: undefined;
11 | };
12 |
13 | export const RootTab = createBottomTabNavigator();
14 |
15 | export const RootDrawer = createDrawerNavigator();
16 |
--------------------------------------------------------------------------------
/src/services/alchemy.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Alchemy, Network, Nft } from 'alchemy-sdk';
3 |
4 | const alchemy = new Alchemy({
5 | apiKey: process.env.EXPO_PUBLIC_ALCHEMY_API_KEY,
6 | network: Network.ETH_MAINNET,
7 | });
8 |
9 | export function useNftsForAddress(address: string) {
10 | const [isLoading, setIsLoading] = useState(true);
11 | const [nfts, setNfts] = useState([]);
12 |
13 | useEffect(() => {
14 | const getNfts = async () => {
15 | const { ownedNfts } = await alchemy.nft.getNftsForOwner(address);
16 | const filtered = ownedNfts.filter(nft => !!nft.image.thumbnailUrl);
17 | setNfts(filtered);
18 | setIsLoading(false);
19 | };
20 |
21 | if (address) {
22 | void getNfts();
23 | }
24 | }, [address]);
25 |
26 | return { nfts, isLoading };
27 | }
28 |
--------------------------------------------------------------------------------
/src/theme.ts:
--------------------------------------------------------------------------------
1 | import { DarkTheme } from '@react-navigation/native';
2 |
3 | const reactNavigationTheme = {
4 | ...DarkTheme,
5 | colors: {
6 | ...DarkTheme.colors,
7 | primary: 'rgb(150,122,255)',
8 | background: 'rgb(0, 0, 0)',
9 | card: 'rgb(0, 0, 0)',
10 | text: 'rgb(255, 255, 255)',
11 | border: 'rgb(35,35,35)',
12 | notification: 'rgb(255, 69, 58)',
13 | },
14 | };
15 |
16 | const theme = {
17 | colors: {
18 | ...reactNavigationTheme.colors,
19 | disabled: 'rgb(52,50,50)',
20 | },
21 | };
22 |
23 | export { reactNavigationTheme, theme };
24 |
--------------------------------------------------------------------------------
/src/web3modal/common.ts:
--------------------------------------------------------------------------------
1 | import { arbitrum, mainnet, polygon, polygonMumbai } from 'viem/chains';
2 | import Constants from 'expo-constants';
3 |
4 | const projectId =
5 | Constants.expoConfig.extra.EXPO_PUBLIC_WALLETCONNECT_CLOUD_PROJECT_ID;
6 |
7 | const metadata = {
8 | name: 'Web3Modal cross-platform',
9 | description: 'Web3Modal RN + web cross-platform example',
10 | url: 'https://web3modal.com',
11 | icons: ['https://avatars.githubusercontent.com/u/37784886'],
12 | redirect: {
13 | native: 'YOUR_APP_SCHEME://',
14 | universal: 'YOUR_APP_UNIVERSAL_LINK.com',
15 | },
16 | };
17 |
18 | const chains = [mainnet, polygon, polygonMumbai, arbitrum];
19 |
20 | export { projectId, metadata, chains };
21 |
--------------------------------------------------------------------------------
/src/web3modal/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | createWeb3Modal,
3 | defaultWagmiConfig,
4 | useWeb3Modal,
5 | W3mButton,
6 | Web3Modal,
7 | } from '@web3modal/wagmi-react-native';
8 | import { chains, metadata, projectId } from './common';
9 |
10 | // Create Wagmi config
11 | const wagmiConfig = defaultWagmiConfig({ chains, projectId, metadata });
12 |
13 | // Init Web3Modal RN SDK
14 | createWeb3Modal({
15 | projectId,
16 | chains,
17 | wagmiConfig,
18 | });
19 |
20 | // Re-export components
21 | export { wagmiConfig, useWeb3Modal, W3mButton, Web3Modal };
22 |
--------------------------------------------------------------------------------
/src/web3modal/index.web.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | createWeb3Modal,
4 | defaultWagmiConfig,
5 | useWeb3Modal,
6 | } from '@web3modal/wagmi/react';
7 | import { chains, metadata, projectId } from './common';
8 |
9 | // Create Wagmi config
10 | const wagmiConfig = defaultWagmiConfig({ chains, projectId, metadata });
11 |
12 | // Init Web3Modal Web SDK
13 | createWeb3Modal({
14 | projectId,
15 | chains,
16 | wagmiConfig,
17 | });
18 |
19 | // Standardize the web button component to be used the same as the native button
20 | const W3mButton = () => ;
21 |
22 | // On web, modal doesn't need to be rendered because it's a globally-available web component
23 | const Web3Modal = () => null;
24 |
25 | // Re-export components
26 | export { wagmiConfig, useWeb3Modal, W3mButton, Web3Modal };
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {},
3 | "extends": "expo/tsconfig.base"
4 | }
5 |
--------------------------------------------------------------------------------