├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── App.tsx ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── package-lock.json ├── package.json ├── polyfills.ts ├── src ├── clients │ ├── public.ts │ └── walletConnect.ts └── pages │ └── HomePage.tsx └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@callstack", 3 | "rules": { 4 | "promise/always-return": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.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 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | # JetBrains 38 | .idea 39 | 40 | # ENV 41 | .env 42 | -------------------------------------------------------------------------------- /.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 from 'react'; 3 | import { StatusBar } from 'expo-status-bar'; 4 | import { StyleSheet, View } from 'react-native'; 5 | import { WalletConnectModal } from '@walletconnect/modal-react-native'; 6 | import HomePage from './src/pages/HomePage'; 7 | import { providerMetadata } from './src/clients/walletConnect'; 8 | 9 | export default function App() { 10 | return ( 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | const styles = StyleSheet.create({ 25 | container: { 26 | flex: 1, 27 | marginHorizontal: 20, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web3-react-native-dapp-viem 2 | 3 | This repository showcases how to build a modern web3 dApp with React Native, [viem](https://viem.sh) and [WalletConnectModal](https://docs.walletconnect.com/advanced/walletconnectmodal/about) 4 | 5 | ## Requirements 6 | 7 | - [Expo environment setup](https://docs.expo.dev/get-started/installation/#requirements) (Node.js, Git, Watchman) 8 | - A [Wallet Connect Cloud](https://cloud.walletconnect.com/sign-in) project ID 9 | - Expo Go app installed in your smartphone 10 | - One or more web3 wallets installed in your smartphone (e.g. MetaMask, Rainbow Wallet, Trust Wallet, etc) 11 | 12 | ## How to run 13 | 14 | - In `App.tsx` fill in `projectId="..."` with your Wallet Connect Cloud project ID 15 | - `npm start` 16 | - Open Expo Go app in your smartphone 17 | - 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 18 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-viem", 4 | "slug": "react-native-viem", 5 | "scheme": "rnviem", 6 | "version": "1.0.0", 7 | "orientation": "portrait", 8 | "icon": "./assets/icon.png", 9 | "userInterfaceStyle": "light", 10 | "splash": { 11 | "image": "./assets/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "assetBundlePatterns": ["**/*"], 16 | "ios": { 17 | "supportsTablet": true, 18 | "infoPlist": { 19 | "LSApplicationQueriesSchemes": [ 20 | "metamask", 21 | "trust", 22 | "safe", 23 | "rainbow", 24 | "uniswap" 25 | ] 26 | } 27 | }, 28 | "android": { 29 | "adaptiveIcon": { 30 | "foregroundImage": "./assets/adaptive-icon.png", 31 | "backgroundColor": "#ffffff" 32 | } 33 | }, 34 | "web": { 35 | "favicon": "./assets/favicon.png" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callstack-internal/web3-react-native-dapp-viem/17ddf52a4362bf8b8d01c04aea4d0ceb45e77ac5/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callstack-internal/web3-react-native-dapp-viem/17ddf52a4362bf8b8d01c04aea4d0ceb45e77ac5/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callstack-internal/web3-react-native-dapp-viem/17ddf52a4362bf8b8d01c04aea4d0ceb45e77ac5/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/callstack-internal/web3-react-native-dapp-viem/17ddf52a4362bf8b8d01c04aea4d0ceb45e77ac5/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-viem", 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 | "@react-native-async-storage/async-storage": "1.18.2", 13 | "@react-native-community/netinfo": "9.3.10", 14 | "@walletconnect/modal-react-native": "^1.0.0-rc.11", 15 | "expo": "~49.0.13", 16 | "expo-status-bar": "~1.6.0", 17 | "fastestsmallesttextencoderdecoder": "^1.0.22", 18 | "react": "18.2.0", 19 | "react-native": "0.72.5", 20 | "react-native-get-random-values": "^1.9.0", 21 | "react-native-modal": "^13.0.1", 22 | "react-native-svg": "13.9.0", 23 | "viem": "^1.16.2" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.20.0", 27 | "@callstack/eslint-config": "^14.0.0", 28 | "@types/react": "~18.2.14", 29 | "eslint": "^8.51.0", 30 | "eslint-config-prettier": "^9.0.0", 31 | "prettier": "^3.0.3", 32 | "typescript": "^5.2.2" 33 | }, 34 | "private": true, 35 | "expo": { 36 | "install": { 37 | "exclude": [ 38 | "react-native-get-random-values" 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'fastestsmallesttextencoderdecoder'; 2 | -------------------------------------------------------------------------------- /src/clients/public.ts: -------------------------------------------------------------------------------- 1 | import { createPublicClient, http } from 'viem'; 2 | import { mainnet } from 'viem/chains'; 3 | 4 | export const publicClient = createPublicClient({ 5 | chain: mainnet, 6 | transport: http(), 7 | }); 8 | -------------------------------------------------------------------------------- /src/clients/walletConnect.ts: -------------------------------------------------------------------------------- 1 | export const providerMetadata = { 2 | name: 'Example dApp', 3 | description: 'Modern Example dApp from Callstack', 4 | url: 'https://callstack.com/', 5 | icons: ['https://example.com/'], 6 | redirect: { 7 | native: 'rnviem://', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react'; 2 | import { Button, StyleSheet, Text, View } from 'react-native'; 3 | import { Address, Chain, createWalletClient, custom, formatEther } from 'viem'; 4 | import { mainnet, sepolia } from 'viem/chains'; 5 | import { 6 | useWalletConnectModal, 7 | IProvider, 8 | } from '@walletconnect/modal-react-native'; 9 | import { publicClient } from '../clients/public'; 10 | 11 | export const CHAINS = [mainnet, sepolia]; 12 | 13 | export default function HomePage() { 14 | const { 15 | open, 16 | isConnected, 17 | provider, 18 | address: wcAddress, 19 | } = useWalletConnectModal(); 20 | const address = wcAddress as Address | undefined; 21 | 22 | const walletClient = useMemo( 23 | () => 24 | createWalletClient({ 25 | chain: mainnet, 26 | transport: custom({ 27 | async request({ method, params }) { 28 | return await provider?.request({ method, params }); 29 | }, 30 | }), 31 | }), 32 | [provider], 33 | ); 34 | 35 | const [blockNumber, setBlockNumber] = useState(0n); 36 | const [gasPrice, setGasPrice] = useState(0n); 37 | const [chain, setChain] = useState(CHAINS[0]); 38 | const [balance, setBalance] = useState(0n); 39 | const [signature, setSignature] = useState<`0x${string}`>(); 40 | 41 | const onSignMessage = async () => { 42 | if (address) { 43 | const signature = await walletClient.signMessage({ 44 | account: address, 45 | message: 'Sign this message to prove you are the owner of this wallet', 46 | }); 47 | 48 | setSignature(signature); 49 | } 50 | }; 51 | 52 | useEffect(() => { 53 | const getNetworkData = async () => { 54 | const [blockNumber, gasPrice] = await Promise.all([ 55 | publicClient.getBlockNumber(), 56 | publicClient.getGasPrice(), 57 | ]); 58 | 59 | setBlockNumber(blockNumber); 60 | setGasPrice(gasPrice); 61 | }; 62 | 63 | void getNetworkData(); 64 | }, []); 65 | 66 | useEffect(() => { 67 | const onChainChangedEvent = (chainId: string) => { 68 | const chain = 69 | CHAINS.find(chain => chain.id === Number(chainId)) ?? CHAINS[0]; 70 | setChain(chain); 71 | }; 72 | 73 | const onConnectEvent = async ({ 74 | session, 75 | }: { 76 | session: IProvider['session']; 77 | }) => { 78 | const chainId = session?.namespaces.eip155.chains?.[0].replace( 79 | 'eip155:', 80 | '', 81 | ); 82 | 83 | if (chainId) { 84 | onChainChangedEvent(chainId); 85 | } 86 | 87 | if (address) { 88 | const balance = await publicClient.getBalance({ 89 | address, 90 | }); 91 | setBalance(balance); 92 | } 93 | }; 94 | 95 | // @ts-ignore 96 | provider?.on('connect', onConnectEvent); 97 | provider?.on('chainChanged', onChainChangedEvent); 98 | 99 | return () => { 100 | provider?.removeListener('chainChanged', onChainChangedEvent); 101 | provider?.removeListener('connect', onConnectEvent); 102 | }; 103 | }, [address, provider, walletClient]); 104 | 105 | return ( 106 | 107 | Block number: {String(blockNumber)} 108 | Gas price: {formatEther(gasPrice)} ETH 109 | 110 | {isConnected && ( 111 | 112 | Address: {address} 113 | Balance: {formatEther(balance)} ETH 114 | Connected to: {chain.name} 115 | 116 | {signature && ( 117 | 118 | Signature: {signature} 119 | 120 | )} 121 | 122 | )} 123 | 124 | 125 | {isConnected ? ( 126 | <> 127 |