├── .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 | ![demo.gif](demo.gif) 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 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /assets/svg/matic-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/svg/uni-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 14 | 19 | 21 | 24 | 28 | 31 | 37 | 45 | 48 | 49 | -------------------------------------------------------------------------------- /assets/svg/usdc-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 |