├── .env.example ├── .gitignore ├── README.md ├── app.json ├── app ├── (tabs) │ ├── _layout.tsx │ ├── index.tsx │ └── profile.tsx ├── +not-found.tsx ├── _layout.tsx ├── deposit.tsx ├── link-profile.tsx └── withdraw.tsx ├── assets ├── fonts │ └── SpaceMono-Regular.ttf └── images │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ ├── partial-react-logo.png │ ├── react-logo.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ └── splash-icon.png ├── babel.config.js ├── biome.json ├── components ├── HapticTab.tsx ├── profile │ ├── Deposit.tsx │ ├── LinkProfile.tsx │ ├── Profile.tsx │ └── Withdraw.tsx └── ui │ ├── IconSymbol.ios.tsx │ ├── IconSymbol.tsx │ ├── TabBarBackground.ios.tsx │ └── TabBarBackground.tsx ├── constants ├── Colors.ts └── thirdweb.ts ├── global.css ├── hooks ├── useExternalWallet.ts ├── useGuestConnect.ts └── useInAppWallet.ts ├── index.js ├── metro.config.js ├── nativewind-env.d.ts ├── output.css ├── package.json ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_THIRDWEB_CLIENT_ID= -------------------------------------------------------------------------------- /.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 | expo-env.d.ts 11 | ios/ 12 | android/ 13 | 14 | # Native 15 | *.orig.* 16 | *.jks 17 | *.p8 18 | *.p12 19 | *.key 20 | *.mobileprovision 21 | 22 | # Metro 23 | .metro-health-check* 24 | 25 | # debug 26 | npm-debug.* 27 | yarn-debug.* 28 | yarn-error.* 29 | 30 | # macOS 31 | .DS_Store 32 | *.pem 33 | 34 | # local env files 35 | .env*.local 36 | .env 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | 41 | app-example 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expo onchain app template 2 | 3 | This is a template that handles the basics of an onchain app, specifically has all the user management and smart wallet logic built in. 4 | 5 | - 🔹 auto login as guest on launch 6 | - 🔹 smart wallet only, gas sponsored, ready to transact with no popups 7 | - 🔹 all amounts shown in $ 8 | - 🔹 link social auth to backup account 9 | - 🔹 link external wallets 10 | - 🔹 UI to deposit into smart account 11 | - 🔹 UI to withdraw 12 | 13 | Built with 14 | 15 | - 🔸 react native 0.76+ 16 | - 🔸 expo 52+ 17 | - 🔸 nativewind 4+ 18 | - 🔸 thirdweb 19 | 20 | Fork this repo, add your own logic, and start building! 21 | 22 | ## Live demo 23 | 24 | [Watch the live demo](https://x.com/joenrv/status/1884729861732000221) 25 | 26 | ## Setup 27 | 28 | Create a new project in the [thirdweb dashboard](https://thirdweb.com/team). 29 | 30 | Copy the `.env.example` file to `.env` 31 | 32 | ```bash 33 | cp .env.example .env 34 | ``` 35 | 36 | Add your client id to the `.env` file 37 | 38 | ```bash 39 | EXPO_PUBLIC_THIRDWEB_CLIENT_ID=your_client_id 40 | ``` 41 | 42 | ## Run the app 43 | 44 | 1. Install dependencies 45 | 46 | ```bash 47 | yarn install 48 | ``` 49 | 50 | 3. Start the app (ios or android) 51 | 52 | ```bash 53 | yarn ios 54 | # or yarn android 55 | ``` 56 | 57 | ## Learn more 58 | 59 | Learn more about building apps with expo and thirdweb with the following resources: 60 | 61 | - [thirdweb dashboard](https://thirdweb.com/team) - manage your API keys, smart wallets policies, project settings, and more 62 | - [thirdweb docs](https://portal.thirdweb.com/) - learn how to use thirdweb to connect wallets, transact, and read onchain data 63 | 64 | ## Join the community 65 | 66 | Join our community of developers creating universal apps. 67 | 68 | - [thirdweb on GitHub](https://github.com/thirdweb-dev/js): View our open source platform and contribute. 69 | - [Discord community](https://discord.gg/thirdweb): Chat with thirdweb devs and ask questions. 70 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "token-trader", 4 | "slug": "token-trader", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "com.thirdweb.tokentrader", 9 | "userInterfaceStyle": "automatic", 10 | "backgroundColor": "#0F0F13", 11 | "newArchEnabled": true, 12 | "ios": { 13 | "supportsTablet": true, 14 | "bundleIdentifier": "com.thirdweb.tokentrader" 15 | }, 16 | "android": { 17 | "adaptiveIcon": { 18 | "foregroundImage": "./assets/images/adaptive-icon.png", 19 | "backgroundColor": "#ffffff" 20 | }, 21 | "package": "com.thirdweb.tokentrader" 22 | }, 23 | "web": { 24 | "bundler": "metro", 25 | "output": "static", 26 | "favicon": "./assets/images/favicon.png" 27 | }, 28 | "plugins": [ 29 | "expo-router", 30 | [ 31 | "expo-splash-screen", 32 | { 33 | "image": "./assets/images/splash-icon.png", 34 | "imageWidth": 200, 35 | "resizeMode": "contain", 36 | "backgroundColor": "#ffffff" 37 | } 38 | ], 39 | [ 40 | "expo-build-properties", 41 | { 42 | "android": { 43 | "minSdkVersion": 26 44 | }, 45 | "ios": { 46 | "extraPods": [ 47 | { 48 | "name": "OpenSSL-Universal", 49 | "configurations": ["Release", "Debug"], 50 | "modular_headers": true, 51 | "version": "3.1.5004" 52 | } 53 | ] 54 | } 55 | } 56 | ] 57 | ], 58 | "experiments": { 59 | "typedRoutes": true 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { HapticTab } from "@/components/HapticTab"; 2 | import { IconSymbol } from "@/components/ui/IconSymbol"; 3 | import { Colors } from "@/constants/colors"; 4 | import { Tabs } from "expo-router"; 5 | import React from "react"; 6 | import { Platform, View } from "react-native"; 7 | 8 | export default function TabLayout() { 9 | return ( 10 | 30 | ( 36 | 37 | ), 38 | }} 39 | /> 40 | ( 46 | 47 | ), 48 | }} 49 | /> 50 | 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/(tabs)/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "expo-router"; 2 | import { SafeAreaView, Text } from "react-native"; 3 | import { shortenAddress } from "thirdweb/utils"; 4 | import { useInAppWallet } from "../../hooks/useInAppWallet"; 5 | 6 | export default function HomeScreen() { 7 | const { account } = useInAppWallet(); 8 | return ( 9 | 10 | 11 | Ready to build your app? 12 | 13 | {account && ( 14 | <> 15 | 16 | User is authenticated and ready to go! 17 | 18 | 19 | Smart Wallet: {shortenAddress(account.address)} 20 | 21 | 22 | View Profile 23 | 24 | 25 | )} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/(tabs)/profile.tsx: -------------------------------------------------------------------------------- 1 | import { Profile } from "@/components/profile/Profile"; 2 | import { SafeAreaView, Text } from "react-native"; 3 | 4 | export default function ProfileScreen() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/+not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Stack } from "expo-router"; 2 | import { Text, View } from "react-native"; 3 | 4 | export default function NotFoundScreen() { 5 | return ( 6 | <> 7 | 8 | 9 | This screen doesn't exist. 10 | 11 | Go to home screen! 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import "../global.css"; 2 | import { useFonts } from "expo-font"; 3 | import { Stack } from "expo-router"; 4 | import * as SplashScreen from "expo-splash-screen"; 5 | import { StatusBar } from "expo-status-bar"; 6 | import { useEffect } from "react"; 7 | import "react-native-reanimated"; 8 | import { Colors } from "@/constants/colors"; 9 | import { client, inApp } from "@/constants/thirdweb"; 10 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 11 | import { ActivityIndicator, SafeAreaView, View } from "react-native"; 12 | import { ThirdwebProvider, useAutoConnect, useConnect } from "thirdweb/react"; 13 | import { useGuestConnect } from "../hooks/useGuestConnect"; 14 | 15 | // Prevent the splash screen from auto-hiding before asset loading is complete. 16 | SplashScreen.preventAutoHideAsync(); 17 | 18 | const queryClient = new QueryClient(); 19 | 20 | export default function RootLayout() { 21 | const [loaded] = useFonts({ 22 | SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"), 23 | }); 24 | 25 | useEffect(() => { 26 | if (loaded) { 27 | SplashScreen.hideAsync(); 28 | } 29 | }, [loaded]); 30 | 31 | if (!loaded) { 32 | return null; 33 | } 34 | 35 | return ( 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | function InnerApp() { 47 | const { isConnecting } = useGuestConnect(); 48 | 49 | if (isConnecting) { 50 | return ( 51 | <> 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | } 59 | 60 | return ( 61 | <> 62 | 63 | 64 | 74 | 84 | 94 | 95 | 96 | 97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /app/deposit.tsx: -------------------------------------------------------------------------------- 1 | import { View } from "react-native"; 2 | import Deposit from "../components/profile/Deposit"; 3 | 4 | export default function DepositScreen() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/link-profile.tsx: -------------------------------------------------------------------------------- 1 | import LinkProfile from "@/components/profile/LinkProfile"; 2 | import { View } from "react-native"; 3 | 4 | export default function LinkProfileScreen() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/withdraw.tsx: -------------------------------------------------------------------------------- 1 | import { View } from "react-native"; 2 | import Withdraw from "../components/profile/Withdraw"; 3 | 4 | export default function WithdrawScreen() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/expo-app-template/8d2d1b46d4d33f6bd379191de4fcb8e497054bfb/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/expo-app-template/8d2d1b46d4d33f6bd379191de4fcb8e497054bfb/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/expo-app-template/8d2d1b46d4d33f6bd379191de4fcb8e497054bfb/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/expo-app-template/8d2d1b46d4d33f6bd379191de4fcb8e497054bfb/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/expo-app-template/8d2d1b46d4d33f6bd379191de4fcb8e497054bfb/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/expo-app-template/8d2d1b46d4d33f6bd379191de4fcb8e497054bfb/assets/images/react-logo.png -------------------------------------------------------------------------------- /assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/expo-app-template/8d2d1b46d4d33f6bd379191de4fcb8e497054bfb/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/expo-app-template/8d2d1b46d4d33f6bd379191de4fcb8e497054bfb/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /assets/images/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thirdweb-dev/expo-app-template/8d2d1b46d4d33f6bd379191de4fcb8e497054bfb/assets/images/splash-icon.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: [ 5 | ["babel-preset-expo", { jsxImportSource: "nativewind" }], 6 | "nativewind/babel", 7 | ], 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "linter": { 7 | "enabled": false 8 | }, 9 | "formatter": { 10 | "enabled": true, 11 | "indentStyle": "space" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /components/HapticTab.tsx: -------------------------------------------------------------------------------- 1 | import { BottomTabBarButtonProps } from "@react-navigation/bottom-tabs"; 2 | import { PlatformPressable } from "@react-navigation/elements"; 3 | import * as Haptics from "expo-haptics"; 4 | 5 | export function HapticTab(props: BottomTabBarButtonProps) { 6 | return ( 7 | { 10 | if (process.env.EXPO_OS === "ios") { 11 | // Add a soft haptic feedback when pressing down on the tabs. 12 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); 13 | } 14 | props.onPressIn?.(ev); 15 | }} 16 | /> 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/profile/Deposit.tsx: -------------------------------------------------------------------------------- 1 | import Ionicons from "@expo/vector-icons/Ionicons"; 2 | import { useMutation } from "@tanstack/react-query"; 3 | import * as Clipboard from "expo-clipboard"; 4 | import { 5 | ActivityIndicator, 6 | Pressable, 7 | Text, 8 | TouchableOpacity, 9 | View, 10 | } from "react-native"; 11 | import { 12 | NATIVE_TOKEN_ADDRESS, 13 | prepareTransaction, 14 | sendTransaction, 15 | toWei, 16 | } from "thirdweb"; 17 | import { convertFiatToCrypto } from "thirdweb/pay"; 18 | import { 19 | AccountAddress, 20 | AccountAvatar, 21 | AccountBlobbie, 22 | AccountProvider, 23 | WalletIcon, 24 | WalletProvider, 25 | useConnect, 26 | useDisconnect, 27 | useSetActiveWallet, 28 | } from "thirdweb/react"; 29 | import { shortenAddress } from "thirdweb/utils"; 30 | import { WalletId, createWallet } from "thirdweb/wallets"; 31 | import { Colors } from "../../constants/colors"; 32 | import { chain, client, supportedWallets } from "../../constants/thirdweb"; 33 | import { useExternalWallet } from "../../hooks/useExternalWallet"; 34 | import { useInAppWallet } from "../../hooks/useInAppWallet"; 35 | 36 | export default function Deposit() { 37 | const { account } = useInAppWallet(); 38 | 39 | const copyAddressToClipboard = async () => { 40 | if (account) { 41 | await Clipboard.setStringAsync(account.address); 42 | } 43 | }; 44 | 45 | if (!account) { 46 | return ( 47 | No account found 48 | ); 49 | } 50 | 51 | return ( 52 | 53 | 54 | 55 | 58 | } 59 | fallbackComponent={ 60 | 61 | } 62 | style={{ 63 | width: 92, 64 | height: 92, 65 | borderRadius: 100, 66 | }} 67 | /> 68 | 69 | 70 | Quick Deposit 71 | 72 | 73 | 74 | 75 | 76 | 77 | OR 78 | 79 | 80 | 81 | 82 | Deposit Funds Manually 83 | 84 | 85 | 86 | Send funds to this address on the{" "} 87 | 88 | {chain.name} 89 | {" "} 90 | network 91 | 92 | 93 | 102 | 103 | 107 | 108 | Copy Address 109 | 110 | 111 | 112 | 113 | ); 114 | } 115 | 116 | function QuickDeposit() { 117 | const { account: localAccount } = useInAppWallet(); 118 | const { account: payerAccount, wallet: payerWallet } = useExternalWallet(); 119 | const { connect } = useConnect({ 120 | setWalletAsActive: false, // only connect, don't set as active 121 | client, 122 | }); 123 | const { disconnect } = useDisconnect(); 124 | 125 | const depositMutation = useMutation({ 126 | mutationFn: async (amountUSD: number) => { 127 | if (!localAccount) { 128 | throw new Error("No local account found"); 129 | } 130 | if (!payerAccount) { 131 | throw new Error("No payer account found"); 132 | } 133 | const amountInEth = await convertFiatToCrypto({ 134 | from: "USD", 135 | fromAmount: amountUSD, 136 | to: NATIVE_TOKEN_ADDRESS, 137 | chain, 138 | client, 139 | }); 140 | const transaction = prepareTransaction({ 141 | to: localAccount.address, 142 | value: toWei(amountInEth.result.toString()), 143 | chain, 144 | client, 145 | }); 146 | const result = await sendTransaction({ 147 | transaction, 148 | account: payerAccount, 149 | }); 150 | return result; 151 | }, 152 | }); 153 | 154 | const connectPayer = async (walletId: WalletId) => { 155 | await connect(async () => { 156 | const wc = createWallet(walletId); 157 | await wc.connect({ client, chain }); 158 | return wc; 159 | }); 160 | }; 161 | 162 | const disconnectPayer = async () => { 163 | if (!payerWallet) { 164 | return; 165 | } 166 | disconnect(payerWallet); 167 | }; 168 | 169 | const deposit = async (amount: number) => { 170 | await depositMutation.mutateAsync(amount); 171 | }; 172 | 173 | if (depositMutation.isPending) { 174 | return ( 175 | 176 | 177 | 178 | Transferring funds 179 | 180 | 181 | ); 182 | } 183 | 184 | if (!payerAccount) { 185 | return ( 186 | 187 | 188 | Connect a wallet to deposit funds 189 | 190 | 191 | {supportedWallets.map((walletId) => ( 192 | 197 | ))} 198 | 199 | 200 | ); 201 | } 202 | 203 | return ( 204 | <> 205 | 206 | 207 | Funding from {shortenAddress(payerAccount.address)}{" "} 208 | 209 | 210 | Disconnect 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | {depositMutation.isError && ( 220 | 221 | Error transferring funds: {depositMutation.error.message} 222 | 223 | )} 224 | {depositMutation.isSuccess && ( 225 | 226 | Funds transferred successfully 227 | 228 | )} 229 | 230 | ); 231 | } 232 | 233 | function QuickDepositButton({ 234 | amount, 235 | onClick, 236 | }: { 237 | amount: number; 238 | onClick: (amount: number) => void; 239 | }) { 240 | return ( 241 | onClick(amount)} 244 | > 245 | ${amount} 246 | 247 | ); 248 | } 249 | 250 | function WalletConnectButton({ 251 | walletId, 252 | onClick, 253 | }: { 254 | walletId: WalletId; 255 | onClick: (walletId: WalletId) => void; 256 | }) { 257 | return ( 258 | 259 | onClick(walletId)} 262 | > 263 | 264 | 265 | 266 | ); 267 | } 268 | -------------------------------------------------------------------------------- /components/profile/LinkProfile.tsx: -------------------------------------------------------------------------------- 1 | import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"; 2 | import { 3 | SocialIcon, 4 | WalletIcon, 5 | WalletName, 6 | WalletProvider, 7 | useLinkProfile, 8 | } from "thirdweb/react"; 9 | import { 10 | InAppWalletSocialAuth, 11 | WalletId, 12 | createWallet, 13 | } from "thirdweb/wallets"; 14 | import { Colors } from "../../constants/colors"; 15 | import { 16 | authStrategies, 17 | chain, 18 | client, 19 | supportedWallets, 20 | } from "../../constants/thirdweb"; 21 | 22 | export default function LinkProfile() { 23 | const { 24 | mutate: linkProfile, 25 | isPending: isLinkingProfile, 26 | error, 27 | } = useLinkProfile(); 28 | 29 | const linkSocial = (strategy: InAppWalletSocialAuth) => { 30 | linkProfile({ 31 | client, 32 | strategy, 33 | }); 34 | }; 35 | 36 | const linkWallet = (walletId: WalletId) => { 37 | linkProfile({ 38 | client, 39 | strategy: "wallet", 40 | wallet: createWallet(walletId), 41 | chain: chain, 42 | }); 43 | }; 44 | 45 | if (isLinkingProfile) { 46 | return ( 47 | 48 | 49 | 50 | ); 51 | } 52 | 53 | if (error) { 54 | return ( 55 | 56 | Error: {error.message} 57 | 58 | ); 59 | } 60 | 61 | return ( 62 | 63 | 64 | {authStrategies.map((strategy) => ( 65 | 70 | ))} 71 | {supportedWallets.map((walletId) => ( 72 | 77 | ))} 78 | 79 | 80 | ); 81 | } 82 | 83 | function SocialLinkButton({ 84 | strategy, 85 | onClick, 86 | }: { 87 | strategy: InAppWalletSocialAuth; 88 | onClick: (strategy: InAppWalletSocialAuth) => void; 89 | }) { 90 | return ( 91 | onClick(strategy)} 94 | > 95 | 101 | 102 | {strategy.charAt(0).toUpperCase() + strategy.slice(1)} 103 | 104 | 105 | ); 106 | } 107 | 108 | function WalletLinkButton({ 109 | walletId, 110 | onClick, 111 | }: { 112 | walletId: WalletId; 113 | onClick: (walletId: WalletId) => void; 114 | }) { 115 | return ( 116 | 117 | onClick(walletId)} 120 | > 121 | 122 | 125 | 126 | 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /components/profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | import FontAwesome5 from "@expo/vector-icons/FontAwesome5"; 2 | import Ionicons from "@expo/vector-icons/Ionicons"; 3 | import { Link, useRouter } from "expo-router"; 4 | import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"; 5 | import { 6 | AccountAddress, 7 | AccountAvatar, 8 | AccountBalance, 9 | AccountBlobbie, 10 | AccountProvider, 11 | SocialIcon, 12 | useProfiles, 13 | } from "thirdweb/react"; 14 | import { shortenAddress } from "thirdweb/utils"; 15 | import { Colors } from "../../constants/colors"; 16 | import { chain, client } from "../../constants/thirdweb"; 17 | import { useInAppWallet } from "../../hooks/useInAppWallet"; 18 | 19 | export function Profile() { 20 | const router = useRouter(); 21 | const { account } = useInAppWallet(); 22 | const profilesQuery = useProfiles({ 23 | client, 24 | }); 25 | const linkedProfiles = profilesQuery.data?.filter( 26 | (profile) => profile.type !== "guest", 27 | ); 28 | const hasLinkedProfiles = linkedProfiles && linkedProfiles.length > 0; 29 | const linkedEmail = linkedProfiles?.find( 30 | (profile) => profile.details.email !== undefined, 31 | )?.details.email; 32 | 33 | if (!account) { 34 | return ( 35 | 36 | No account connected 37 | 38 | ); 39 | } 40 | 41 | if (profilesQuery.isLoading) { 42 | return ( 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | return ( 50 | 51 | 52 | 53 | 56 | } 57 | fallbackComponent={ 58 | 59 | } 60 | style={{ 61 | width: 92, 62 | height: 92, 63 | borderRadius: 100, 64 | }} 65 | /> 66 | 67 | {linkedEmail?.split("@")[0] || 68 | `user#${account.address.slice(2, 4)}${account.address.slice(-2)}`.toLowerCase()} 69 | 70 | 74 | 75 | 76 | Account Balance 77 | 86 | } 87 | fallbackComponent={ 88 | Failed to load balance 89 | } 90 | style={{ 91 | color: "white", 92 | fontSize: 48, 93 | fontWeight: "bold", 94 | }} 95 | queryOptions={{ 96 | refetchInterval: 5000, 97 | }} 98 | /> 99 | 100 | 101 | { 105 | router.push("/deposit"); 106 | }} 107 | > 108 | 113 | Deposit 114 | 115 | { 119 | router.push("/withdraw"); 120 | }} 121 | > 122 | 127 | Withdraw 128 | 129 | 130 | 131 | 132 | 133 | Linked Accounts 134 | 135 | 136 | {profilesQuery.isLoading && ( 137 | Loading... 138 | )} 139 | {linkedProfiles?.map((profile) => ( 140 | 144 | 145 | 146 | 147 | {profile.type.charAt(0).toUpperCase() + 148 | profile.type.slice(1)} 149 | 150 | 151 | {profile.details.email} 152 | 153 | 154 | 155 | ))} 156 | 157 | 158 | 159 | 160 | 161 | Link an account 162 | 163 | {!hasLinkedProfiles && ( 164 | 165 | Link an account to recover access if you loose your 166 | device. 167 | 168 | )} 169 | {hasLinkedProfiles && ( 170 | 171 | Link another account to your profile. 172 | 173 | )} 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | ); 182 | } 183 | -------------------------------------------------------------------------------- /components/profile/Withdraw.tsx: -------------------------------------------------------------------------------- 1 | import FontAwesome5 from "@expo/vector-icons/FontAwesome5"; 2 | import { useEffect, useState } from "react"; 3 | import { 4 | ActivityIndicator, 5 | Linking, 6 | Pressable, 7 | Text, 8 | TextInput, 9 | TouchableOpacity, 10 | View, 11 | } from "react-native"; 12 | import { NATIVE_TOKEN_ADDRESS, prepareTransaction } from "thirdweb"; 13 | import { resolveAddress } from "thirdweb/extensions/ens"; 14 | import { convertCryptoToFiat } from "thirdweb/pay"; 15 | import { 16 | AccountAvatar, 17 | AccountBalance, 18 | AccountBlobbie, 19 | AccountProvider, 20 | useSendTransaction, 21 | useWalletBalance, 22 | } from "thirdweb/react"; 23 | import { shortenHex } from "thirdweb/utils"; 24 | import { Colors } from "../../constants/colors"; 25 | import { chain, client } from "../../constants/thirdweb"; 26 | import { useExternalWallet } from "../../hooks/useExternalWallet"; 27 | import { useInAppWallet } from "../../hooks/useInAppWallet"; 28 | 29 | export default function Withdraw() { 30 | const { account } = useInAppWallet(); 31 | const { account: externalAccount } = useExternalWallet(); 32 | const [recipientAddress, setRecipientAddress] = useState( 33 | externalAccount?.address || "", 34 | ); 35 | const [amountUSD, setAmountUSD] = useState(""); 36 | const [amountETH, setAmountETH] = useState(0n); 37 | const [totalBalanceUSD, setTotalBalanceUSD] = useState(0); 38 | const balanceQuery = useWalletBalance({ 39 | client, 40 | address: account?.address, 41 | chain, 42 | }); 43 | const sendTransactionMutation = useSendTransaction(); 44 | 45 | useEffect(() => { 46 | const convertBalanceToUSD = async () => { 47 | if (!balanceQuery.data?.displayValue) { 48 | return; 49 | } 50 | const usd = await convertCryptoToFiat({ 51 | fromTokenAddress: NATIVE_TOKEN_ADDRESS, 52 | to: "USD", 53 | fromAmount: Number(balanceQuery.data?.displayValue), 54 | chain, 55 | client, 56 | }); 57 | setTotalBalanceUSD(usd.result); 58 | }; 59 | convertBalanceToUSD(); 60 | }, [balanceQuery.data]); 61 | 62 | if (!account) { 63 | return ( 64 | 65 | No account connected 66 | 67 | ); 68 | } 69 | 70 | const handlePercentageSelect = (percentage: number) => { 71 | const amountUSD = (totalBalanceUSD * percentage) / 100; 72 | const amountETH = 73 | ((balanceQuery.data?.value || 0n) * BigInt(percentage)) / 100n; 74 | 75 | setAmountUSD(`$${amountUSD.toFixed(2)}`); 76 | setAmountETH(amountETH); 77 | }; 78 | 79 | const handleWithdraw = async () => { 80 | if (!amountETH) { 81 | return; 82 | } 83 | const resolvedAddress = await resolveAddress({ 84 | name: recipientAddress, 85 | client, 86 | }); 87 | const transaction = prepareTransaction({ 88 | to: resolvedAddress, 89 | value: amountETH, 90 | chain, 91 | client, 92 | }); 93 | sendTransactionMutation.mutate(transaction); 94 | }; 95 | 96 | return ( 97 | 98 | 99 | {/* Avatar Section */} 100 | 101 | 104 | } 105 | fallbackComponent={ 106 | 107 | } 108 | style={{ 109 | width: 92, 110 | height: 92, 111 | borderRadius: 100, 112 | }} 113 | /> 114 | 115 | 116 | {/* Balance Section */} 117 | 118 | Available Balance 119 | 128 | } 129 | fallbackComponent={ 130 | Failed to load balance 131 | } 132 | style={{ 133 | color: "white", 134 | fontSize: 48, 135 | fontWeight: "bold", 136 | }} 137 | queryOptions={{ 138 | refetchInterval: 5000, 139 | }} 140 | /> 141 | 142 | 143 | {/* Recipient Address Input */} 144 | 145 | Recipient Address 146 | 153 | 154 | 155 | {/* Amount Selection */} 156 | 157 | Amount 158 | 159 | {[25, 50, 100].map((percentage) => ( 160 | handlePercentageSelect(percentage)} 164 | > 165 | {percentage}% 166 | 167 | ))} 168 | 169 | 170 | 178 | 179 | 180 | {/* Withdraw Button */} 181 | 187 | 188 | 189 | 190 | {sendTransactionMutation.isPending 191 | ? "Withdrawing..." 192 | : `Withdraw ${amountUSD}`} 193 | 194 | 195 | 196 | 197 | {sendTransactionMutation.isError && ( 198 | 199 | {sendTransactionMutation.error.message} 200 | 201 | )} 202 | 203 | {sendTransactionMutation.isSuccess && ( 204 | 206 | Linking.openURL( 207 | `${chain.blockExplorers?.[0]?.url}/tx/${sendTransactionMutation.data?.transactionHash}`, 208 | ) 209 | } 210 | > 211 | 212 | Trasaction sent:{" "} 213 | {shortenHex(sendTransactionMutation.data?.transactionHash)} 214 | 215 | 216 | )} 217 | 218 | 219 | ); 220 | } 221 | -------------------------------------------------------------------------------- /components/ui/IconSymbol.ios.tsx: -------------------------------------------------------------------------------- 1 | import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols'; 2 | import { StyleProp, ViewStyle } from 'react-native'; 3 | 4 | export function IconSymbol({ 5 | name, 6 | size = 24, 7 | color, 8 | style, 9 | weight = 'regular', 10 | }: { 11 | name: SymbolViewProps['name']; 12 | size?: number; 13 | color: string; 14 | style?: StyleProp; 15 | weight?: SymbolWeight; 16 | }) { 17 | return ( 18 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/ui/IconSymbol.tsx: -------------------------------------------------------------------------------- 1 | // This file is a fallback for using MaterialIcons on Android and web. 2 | 3 | import MaterialIcons from "@expo/vector-icons/MaterialIcons"; 4 | import { SymbolWeight } from "expo-symbols"; 5 | import React from "react"; 6 | import { OpaqueColorValue, StyleProp, ViewStyle } from "react-native"; 7 | 8 | // Add your SFSymbol to MaterialIcons mappings here. 9 | const MAPPING = { 10 | // See MaterialIcons here: https://icons.expo.fyi 11 | // See SF Symbols in the SF Symbols app on Mac. 12 | "house.fill": "home", 13 | "person.fill": "person", 14 | "paperplane.fill": "send", 15 | "chevron.left.forwardslash.chevron.right": "code", 16 | "chevron.right": "chevron-right", 17 | } as Partial< 18 | Record< 19 | import("expo-symbols").SymbolViewProps["name"], 20 | React.ComponentProps["name"] 21 | > 22 | >; 23 | 24 | export type IconSymbolName = keyof typeof MAPPING; 25 | 26 | /** 27 | * An icon component that uses native SFSymbols on iOS, and MaterialIcons on Android and web. This ensures a consistent look across platforms, and optimal resource usage. 28 | * 29 | * Icon `name`s are based on SFSymbols and require manual mapping to MaterialIcons. 30 | */ 31 | export function IconSymbol({ 32 | name, 33 | size = 24, 34 | color, 35 | style, 36 | }: { 37 | name: IconSymbolName; 38 | size?: number; 39 | color: string | OpaqueColorValue; 40 | style?: StyleProp; 41 | weight?: SymbolWeight; 42 | }) { 43 | return ( 44 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /components/ui/TabBarBackground.ios.tsx: -------------------------------------------------------------------------------- 1 | import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; 2 | import { BlurView } from 'expo-blur'; 3 | import { StyleSheet } from 'react-native'; 4 | import { useSafeAreaInsets } from 'react-native-safe-area-context'; 5 | 6 | export default function BlurTabBarBackground() { 7 | return ( 8 | 15 | ); 16 | } 17 | 18 | export function useBottomTabOverflow() { 19 | const tabHeight = useBottomTabBarHeight(); 20 | const { bottom } = useSafeAreaInsets(); 21 | return tabHeight - bottom; 22 | } 23 | -------------------------------------------------------------------------------- /components/ui/TabBarBackground.tsx: -------------------------------------------------------------------------------- 1 | // This is a shim for web and Android where the tab bar is generally opaque. 2 | export default undefined; 3 | 4 | export function useBottomTabOverflow() { 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /constants/Colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode. 3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. 4 | */ 5 | 6 | export const Colors = { 7 | background: "#0F0F13", 8 | backgroundSecondary: "#1A1A20", 9 | primary: "#ECEDEE", 10 | secondary: "#9BA1A6", 11 | link: "#8B5CF6", 12 | border: "#2A2A35", 13 | accent: "#8B5CF6", // Purple accent 14 | icon: "#9BA1A6", 15 | tabIconDefault: "#9BA1A6", 16 | tabIconSelected: "#8B5CF6", 17 | }; 18 | -------------------------------------------------------------------------------- /constants/thirdweb.ts: -------------------------------------------------------------------------------- 1 | import { createThirdwebClient, getContract } from "thirdweb"; 2 | import { base, baseSepolia } from "thirdweb/chains"; 3 | import { InAppWalletSocialAuth, WalletId, inAppWallet } from "thirdweb/wallets"; 4 | 5 | const clientId = process.env.EXPO_PUBLIC_THIRDWEB_CLIENT_ID!; 6 | 7 | if (!clientId) { 8 | throw new Error( 9 | "Missing EXPO_PUBLIC_THIRDWEB_CLIENT_ID - make sure to set it in your .env file", 10 | ); 11 | } 12 | 13 | export const client = createThirdwebClient({ 14 | clientId, 15 | }); 16 | 17 | export const chain = base; 18 | 19 | export const inApp = inAppWallet({ 20 | smartAccount: { 21 | chain, 22 | sponsorGas: true, 23 | }, 24 | }); 25 | 26 | export const authStrategies: InAppWalletSocialAuth[] = ["google"]; 27 | export const supportedWallets: WalletId[] = [ 28 | "io.metamask", 29 | "me.rainbow", 30 | "io.zerion.wallet", 31 | "com.trustwallet.app", 32 | ]; 33 | -------------------------------------------------------------------------------- /global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /hooks/useExternalWallet.ts: -------------------------------------------------------------------------------- 1 | import { useConnectedWallets } from "thirdweb/react"; 2 | 3 | export function useExternalWallet() { 4 | const wallets = useConnectedWallets(); 5 | const externalWallet = wallets.find((wallet) => wallet.id !== "inApp"); 6 | return { 7 | wallet: externalWallet, 8 | account: externalWallet?.getAccount(), 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /hooks/useGuestConnect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useAutoConnect, useConnect } from "thirdweb/react"; 3 | import { client, inApp } from "../constants/thirdweb"; 4 | 5 | export function useGuestConnect() { 6 | const connectMutation = useConnect(); 7 | const autoConnecQuery = useAutoConnect({ 8 | client, 9 | wallets: [inApp], 10 | }); 11 | useEffect(() => { 12 | if (autoConnecQuery.data || autoConnecQuery.isLoading) { 13 | return; 14 | } 15 | // if not autoconnected, login as guest 16 | connectMutation.connect(async () => { 17 | await inApp.connect({ 18 | strategy: "guest", 19 | client, 20 | }); 21 | return inApp; 22 | }); 23 | }, [autoConnecQuery.data, autoConnecQuery.isLoading]); 24 | 25 | return { 26 | isConnecting: autoConnecQuery.isLoading || connectMutation.isConnecting, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /hooks/useInAppWallet.ts: -------------------------------------------------------------------------------- 1 | import { useConnectedWallets } from "thirdweb/react"; 2 | 3 | export function useInAppWallet() { 4 | const wallets = useConnectedWallets(); 5 | const inAppWallet = wallets.find((wallet) => wallet.id === "inApp"); 6 | return { 7 | wallet: inAppWallet, 8 | account: inAppWallet?.getAccount(), 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // this needs to be imported before expo-router 2 | import "@thirdweb-dev/react-native-adapter"; 3 | import "expo-router/entry"; 4 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require("expo/metro-config"); 2 | const { withNativeWind } = require("nativewind/metro"); 3 | 4 | const config = getDefaultConfig(__dirname); 5 | 6 | config.resolver.unstable_enablePackageExports = true; 7 | config.resolver.unstable_conditionNames = [ 8 | "react-native", 9 | "browser", 10 | "require", 11 | ]; 12 | 13 | module.exports = withNativeWind(config, { input: "./global.css" }); 14 | -------------------------------------------------------------------------------- /nativewind-env.d.ts: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /output.css: -------------------------------------------------------------------------------- 1 | *, ::before, ::after { 2 | --tw-border-spacing-x: 0; 3 | --tw-border-spacing-y: 0; 4 | --tw-translate-x: 0; 5 | --tw-translate-y: 0; 6 | --tw-rotate: 0; 7 | --tw-skew-x: 0; 8 | --tw-skew-y: 0; 9 | --tw-scale-x: 1; 10 | --tw-scale-y: 1; 11 | --tw-pan-x: ; 12 | --tw-pan-y: ; 13 | --tw-pinch-zoom: ; 14 | --tw-scroll-snap-strictness: proximity; 15 | --tw-gradient-from-position: ; 16 | --tw-gradient-via-position: ; 17 | --tw-gradient-to-position: ; 18 | --tw-ordinal: ; 19 | --tw-slashed-zero: ; 20 | --tw-numeric-figure: ; 21 | --tw-numeric-spacing: ; 22 | --tw-numeric-fraction: ; 23 | --tw-ring-inset: ; 24 | --tw-ring-offset-width: 0px; 25 | --tw-ring-offset-color: #fff; 26 | --tw-ring-color: rgb(59 130 246 / 0.5); 27 | --tw-ring-offset-shadow: 0 0 #0000; 28 | --tw-ring-shadow: 0 0 #0000; 29 | --tw-shadow: 0 0 #0000; 30 | --tw-shadow-colored: 0 0 #0000; 31 | --tw-blur: ; 32 | --tw-brightness: ; 33 | --tw-contrast: ; 34 | --tw-grayscale: ; 35 | --tw-hue-rotate: ; 36 | --tw-invert: ; 37 | --tw-saturate: ; 38 | --tw-sepia: ; 39 | --tw-drop-shadow: ; 40 | --tw-backdrop-blur: ; 41 | --tw-backdrop-brightness: ; 42 | --tw-backdrop-contrast: ; 43 | --tw-backdrop-grayscale: ; 44 | --tw-backdrop-hue-rotate: ; 45 | --tw-backdrop-invert: ; 46 | --tw-backdrop-opacity: ; 47 | --tw-backdrop-saturate: ; 48 | --tw-backdrop-sepia: ; 49 | --tw-contain-size: ; 50 | --tw-contain-layout: ; 51 | --tw-contain-paint: ; 52 | --tw-contain-style: ; 53 | } 54 | 55 | ::backdrop { 56 | --tw-border-spacing-x: 0; 57 | --tw-border-spacing-y: 0; 58 | --tw-translate-x: 0; 59 | --tw-translate-y: 0; 60 | --tw-rotate: 0; 61 | --tw-skew-x: 0; 62 | --tw-skew-y: 0; 63 | --tw-scale-x: 1; 64 | --tw-scale-y: 1; 65 | --tw-pan-x: ; 66 | --tw-pan-y: ; 67 | --tw-pinch-zoom: ; 68 | --tw-scroll-snap-strictness: proximity; 69 | --tw-gradient-from-position: ; 70 | --tw-gradient-via-position: ; 71 | --tw-gradient-to-position: ; 72 | --tw-ordinal: ; 73 | --tw-slashed-zero: ; 74 | --tw-numeric-figure: ; 75 | --tw-numeric-spacing: ; 76 | --tw-numeric-fraction: ; 77 | --tw-ring-inset: ; 78 | --tw-ring-offset-width: 0px; 79 | --tw-ring-offset-color: #fff; 80 | --tw-ring-color: rgb(59 130 246 / 0.5); 81 | --tw-ring-offset-shadow: 0 0 #0000; 82 | --tw-ring-shadow: 0 0 #0000; 83 | --tw-shadow: 0 0 #0000; 84 | --tw-shadow-colored: 0 0 #0000; 85 | --tw-blur: ; 86 | --tw-brightness: ; 87 | --tw-contrast: ; 88 | --tw-grayscale: ; 89 | --tw-hue-rotate: ; 90 | --tw-invert: ; 91 | --tw-saturate: ; 92 | --tw-sepia: ; 93 | --tw-drop-shadow: ; 94 | --tw-backdrop-blur: ; 95 | --tw-backdrop-brightness: ; 96 | --tw-backdrop-contrast: ; 97 | --tw-backdrop-grayscale: ; 98 | --tw-backdrop-hue-rotate: ; 99 | --tw-backdrop-invert: ; 100 | --tw-backdrop-opacity: ; 101 | --tw-backdrop-saturate: ; 102 | --tw-backdrop-sepia: ; 103 | --tw-contain-size: ; 104 | --tw-contain-layout: ; 105 | --tw-contain-paint: ; 106 | --tw-contain-style: ; 107 | } 108 | 109 | /* 110 | ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com 111 | */ 112 | 113 | /* 114 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 115 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 116 | */ 117 | 118 | *, 119 | ::before, 120 | ::after { 121 | box-sizing: border-box; 122 | /* 1 */ 123 | border-width: 0; 124 | /* 2 */ 125 | border-style: solid; 126 | /* 2 */ 127 | border-color: #e5e7eb; 128 | /* 2 */ 129 | } 130 | 131 | ::before, 132 | ::after { 133 | --tw-content: ''; 134 | } 135 | 136 | /* 137 | 1. Use a consistent sensible line-height in all browsers. 138 | 2. Prevent adjustments of font size after orientation changes in iOS. 139 | 3. Use a more readable tab size. 140 | 4. Use the user's configured `sans` font-family by default. 141 | 5. Use the user's configured `sans` font-feature-settings by default. 142 | 6. Use the user's configured `sans` font-variation-settings by default. 143 | 7. Disable tap highlights on iOS 144 | */ 145 | 146 | html, 147 | :host { 148 | line-height: 1.5; 149 | /* 1 */ 150 | -webkit-text-size-adjust: 100%; 151 | /* 2 */ 152 | -moz-tab-size: 4; 153 | /* 3 */ 154 | -o-tab-size: 4; 155 | tab-size: 4; 156 | /* 3 */ 157 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 158 | /* 4 */ 159 | font-feature-settings: normal; 160 | /* 5 */ 161 | font-variation-settings: normal; 162 | /* 6 */ 163 | -webkit-tap-highlight-color: transparent; 164 | /* 7 */ 165 | } 166 | 167 | /* 168 | 1. Remove the margin in all browsers. 169 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 170 | */ 171 | 172 | body { 173 | margin: 0; 174 | /* 1 */ 175 | line-height: inherit; 176 | /* 2 */ 177 | } 178 | 179 | /* 180 | 1. Add the correct height in Firefox. 181 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 182 | 3. Ensure horizontal rules are visible by default. 183 | */ 184 | 185 | hr { 186 | height: 0; 187 | /* 1 */ 188 | color: inherit; 189 | /* 2 */ 190 | border-top-width: 1px; 191 | /* 3 */ 192 | } 193 | 194 | /* 195 | Add the correct text decoration in Chrome, Edge, and Safari. 196 | */ 197 | 198 | abbr:where([title]) { 199 | -webkit-text-decoration: underline dotted; 200 | text-decoration: underline dotted; 201 | } 202 | 203 | /* 204 | Remove the default font size and weight for headings. 205 | */ 206 | 207 | h1, 208 | h2, 209 | h3, 210 | h4, 211 | h5, 212 | h6 { 213 | font-size: inherit; 214 | font-weight: inherit; 215 | } 216 | 217 | /* 218 | Reset links to optimize for opt-in styling instead of opt-out. 219 | */ 220 | 221 | a { 222 | color: inherit; 223 | text-decoration: inherit; 224 | } 225 | 226 | /* 227 | Add the correct font weight in Edge and Safari. 228 | */ 229 | 230 | b, 231 | strong { 232 | font-weight: bolder; 233 | } 234 | 235 | /* 236 | 1. Use the user's configured `mono` font-family by default. 237 | 2. Use the user's configured `mono` font-feature-settings by default. 238 | 3. Use the user's configured `mono` font-variation-settings by default. 239 | 4. Correct the odd `em` font sizing in all browsers. 240 | */ 241 | 242 | code, 243 | kbd, 244 | samp, 245 | pre { 246 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 247 | /* 1 */ 248 | font-feature-settings: normal; 249 | /* 2 */ 250 | font-variation-settings: normal; 251 | /* 3 */ 252 | font-size: 1em; 253 | /* 4 */ 254 | } 255 | 256 | /* 257 | Add the correct font size in all browsers. 258 | */ 259 | 260 | small { 261 | font-size: 80%; 262 | } 263 | 264 | /* 265 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 266 | */ 267 | 268 | sub, 269 | sup { 270 | font-size: 75%; 271 | line-height: 0; 272 | position: relative; 273 | vertical-align: baseline; 274 | } 275 | 276 | sub { 277 | bottom: -0.25em; 278 | } 279 | 280 | sup { 281 | top: -0.5em; 282 | } 283 | 284 | /* 285 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 286 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 287 | 3. Remove gaps between table borders by default. 288 | */ 289 | 290 | table { 291 | text-indent: 0; 292 | /* 1 */ 293 | border-color: inherit; 294 | /* 2 */ 295 | border-collapse: collapse; 296 | /* 3 */ 297 | } 298 | 299 | /* 300 | 1. Change the font styles in all browsers. 301 | 2. Remove the margin in Firefox and Safari. 302 | 3. Remove default padding in all browsers. 303 | */ 304 | 305 | button, 306 | input, 307 | optgroup, 308 | select, 309 | textarea { 310 | font-family: inherit; 311 | /* 1 */ 312 | font-feature-settings: inherit; 313 | /* 1 */ 314 | font-variation-settings: inherit; 315 | /* 1 */ 316 | font-size: 100%; 317 | /* 1 */ 318 | font-weight: inherit; 319 | /* 1 */ 320 | line-height: inherit; 321 | /* 1 */ 322 | letter-spacing: inherit; 323 | /* 1 */ 324 | color: inherit; 325 | /* 1 */ 326 | margin: 0; 327 | /* 2 */ 328 | padding: 0; 329 | /* 3 */ 330 | } 331 | 332 | /* 333 | Remove the inheritance of text transform in Edge and Firefox. 334 | */ 335 | 336 | button, 337 | select { 338 | text-transform: none; 339 | } 340 | 341 | /* 342 | 1. Correct the inability to style clickable types in iOS and Safari. 343 | 2. Remove default button styles. 344 | */ 345 | 346 | button, 347 | input:where([type='button']), 348 | input:where([type='reset']), 349 | input:where([type='submit']) { 350 | -webkit-appearance: button; 351 | /* 1 */ 352 | background-color: transparent; 353 | /* 2 */ 354 | background-image: none; 355 | /* 2 */ 356 | } 357 | 358 | /* 359 | Use the modern Firefox focus style for all focusable elements. 360 | */ 361 | 362 | :-moz-focusring { 363 | outline: auto; 364 | } 365 | 366 | /* 367 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 368 | */ 369 | 370 | :-moz-ui-invalid { 371 | box-shadow: none; 372 | } 373 | 374 | /* 375 | Add the correct vertical alignment in Chrome and Firefox. 376 | */ 377 | 378 | progress { 379 | vertical-align: baseline; 380 | } 381 | 382 | /* 383 | Correct the cursor style of increment and decrement buttons in Safari. 384 | */ 385 | 386 | ::-webkit-inner-spin-button, 387 | ::-webkit-outer-spin-button { 388 | height: auto; 389 | } 390 | 391 | /* 392 | 1. Correct the odd appearance in Chrome and Safari. 393 | 2. Correct the outline style in Safari. 394 | */ 395 | 396 | [type='search'] { 397 | -webkit-appearance: textfield; 398 | /* 1 */ 399 | outline-offset: -2px; 400 | /* 2 */ 401 | } 402 | 403 | /* 404 | Remove the inner padding in Chrome and Safari on macOS. 405 | */ 406 | 407 | ::-webkit-search-decoration { 408 | -webkit-appearance: none; 409 | } 410 | 411 | /* 412 | 1. Correct the inability to style clickable types in iOS and Safari. 413 | 2. Change font properties to `inherit` in Safari. 414 | */ 415 | 416 | ::-webkit-file-upload-button { 417 | -webkit-appearance: button; 418 | /* 1 */ 419 | font: inherit; 420 | /* 2 */ 421 | } 422 | 423 | /* 424 | Add the correct display in Chrome and Safari. 425 | */ 426 | 427 | summary { 428 | display: list-item; 429 | } 430 | 431 | /* 432 | Removes the default spacing and border for appropriate elements. 433 | */ 434 | 435 | blockquote, 436 | dl, 437 | dd, 438 | h1, 439 | h2, 440 | h3, 441 | h4, 442 | h5, 443 | h6, 444 | hr, 445 | figure, 446 | p, 447 | pre { 448 | margin: 0; 449 | } 450 | 451 | fieldset { 452 | margin: 0; 453 | padding: 0; 454 | } 455 | 456 | legend { 457 | padding: 0; 458 | } 459 | 460 | ol, 461 | ul, 462 | menu { 463 | list-style: none; 464 | margin: 0; 465 | padding: 0; 466 | } 467 | 468 | /* 469 | Reset default styling for dialogs. 470 | */ 471 | 472 | dialog { 473 | padding: 0; 474 | } 475 | 476 | /* 477 | Prevent resizing textareas horizontally by default. 478 | */ 479 | 480 | textarea { 481 | resize: vertical; 482 | } 483 | 484 | /* 485 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 486 | 2. Set the default placeholder color to the user's configured gray 400 color. 487 | */ 488 | 489 | input::-moz-placeholder, textarea::-moz-placeholder { 490 | opacity: 1; 491 | /* 1 */ 492 | color: #9ca3af; 493 | /* 2 */ 494 | } 495 | 496 | input::placeholder, 497 | textarea::placeholder { 498 | opacity: 1; 499 | /* 1 */ 500 | color: #9ca3af; 501 | /* 2 */ 502 | } 503 | 504 | /* 505 | Set the default cursor for buttons. 506 | */ 507 | 508 | button, 509 | [role="button"] { 510 | cursor: pointer; 511 | } 512 | 513 | /* 514 | Make sure disabled buttons don't get the pointer cursor. 515 | */ 516 | 517 | :disabled { 518 | cursor: default; 519 | } 520 | 521 | /* 522 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 523 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 524 | This can trigger a poorly considered lint error in some tools but is included by design. 525 | */ 526 | 527 | img, 528 | svg, 529 | video, 530 | canvas, 531 | audio, 532 | iframe, 533 | embed, 534 | object { 535 | display: block; 536 | /* 1 */ 537 | vertical-align: middle; 538 | /* 2 */ 539 | } 540 | 541 | /* 542 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 543 | */ 544 | 545 | img, 546 | video { 547 | max-width: 100%; 548 | height: auto; 549 | } 550 | 551 | /* Make elements with the HTML hidden attribute stay hidden by default */ 552 | 553 | [hidden]:where(:not([hidden="until-found"])) { 554 | display: none; 555 | } 556 | 557 | :root { 558 | --css-interop-darkMode: media; 559 | --css-interop: true; 560 | --css-interop-nativewind: true; 561 | } 562 | 563 | .mt-4 { 564 | margin-top: 1rem; 565 | } 566 | 567 | .mt-6 { 568 | margin-top: 1.5rem; 569 | } 570 | 571 | .h-full { 572 | height: 100%; 573 | } 574 | 575 | .w-full { 576 | width: 100%; 577 | } 578 | 579 | .flex-1 { 580 | flex: 1 1 0%; 581 | } 582 | 583 | .flex-col { 584 | flex-direction: column; 585 | } 586 | 587 | .items-center { 588 | align-items: center; 589 | } 590 | 591 | .justify-center { 592 | justify-content: center; 593 | } 594 | 595 | .border { 596 | border-width: 1px; 597 | } 598 | 599 | .bg-background { 600 | --tw-bg-opacity: 1; 601 | background-color: rgb(15 15 19 / var(--tw-bg-opacity, 1)); 602 | } 603 | 604 | .p-4 { 605 | padding: 1rem; 606 | } 607 | 608 | .p-8 { 609 | padding: 2rem; 610 | } 611 | 612 | .text-3xl { 613 | font-size: 1.875rem; 614 | line-height: 2.25rem; 615 | } 616 | 617 | .text-lg { 618 | font-size: 1.125rem; 619 | line-height: 1.75rem; 620 | } 621 | 622 | .font-bold { 623 | font-weight: 700; 624 | } 625 | 626 | .text-link { 627 | --tw-text-opacity: 1; 628 | color: rgb(139 92 246 / var(--tw-text-opacity, 1)); 629 | } 630 | 631 | .text-primary { 632 | --tw-text-opacity: 1; 633 | color: rgb(236 237 238 / var(--tw-text-opacity, 1)); 634 | } 635 | 636 | .text-secondary { 637 | --tw-text-opacity: 1; 638 | color: rgb(155 161 166 / var(--tw-text-opacity, 1)); 639 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "token-trader", 3 | "main": "./index.js", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "start": "expo start --dev-client --clear", 7 | "prebuild": "expo prebuild", 8 | "android": "expo run:android", 9 | "ios": "expo run:ios", 10 | "web": "expo start --web", 11 | "test": "jest --watchAll", 12 | "lint": "expo lint" 13 | }, 14 | "jest": { 15 | "preset": "jest-expo" 16 | }, 17 | "dependencies": { 18 | "@coinbase/wallet-mobile-sdk": "^1.1.2", 19 | "@expo/vector-icons": "^14.0.4", 20 | "@react-native-async-storage/async-storage": "1.23.1", 21 | "@react-native-community/netinfo": "11.4.1", 22 | "@react-navigation/bottom-tabs": "^7.2.0", 23 | "@react-navigation/native": "^7.0.14", 24 | "@tanstack/react-query": "^5.65.1", 25 | "@thirdweb-dev/react-native-adapter": "^1.5.3", 26 | "@walletconnect/react-native-compat": "^2.17.5", 27 | "expo": "~52.0.27", 28 | "expo-application": "~6.0.2", 29 | "expo-blur": "~14.0.2", 30 | "expo-build-properties": "^0.13.2", 31 | "expo-clipboard": "^7.0.1", 32 | "expo-constants": "~17.0.4", 33 | "expo-font": "~13.0.3", 34 | "expo-haptics": "~14.0.1", 35 | "expo-linking": "~7.0.4", 36 | "expo-router": "~4.0.17", 37 | "expo-splash-screen": "~0.29.21", 38 | "expo-status-bar": "~2.0.1", 39 | "expo-symbols": "~0.2.1", 40 | "expo-system-ui": "~4.0.7", 41 | "expo-web-browser": "~14.0.2", 42 | "nativewind": "^4.1.23", 43 | "react": "18.3.1", 44 | "react-dom": "18.3.1", 45 | "react-native": "0.76.6", 46 | "react-native-aes-gcm-crypto": "^0.2.2", 47 | "react-native-gesture-handler": "~2.20.2", 48 | "react-native-get-random-values": "~1.11.0", 49 | "react-native-mmkv": "^3.2.0", 50 | "react-native-passkey": "^3.1.0", 51 | "react-native-quick-crypto": "^0.7.11", 52 | "react-native-reanimated": "^3.16.7", 53 | "react-native-safe-area-context": "^5.1.0", 54 | "react-native-screens": "~4.4.0", 55 | "react-native-svg": "15.8.0", 56 | "react-native-web": "~0.19.13", 57 | "react-native-webview": "13.12.5", 58 | "tailwindcss": "3.4.17", 59 | "thirdweb": "5.87.1" 60 | }, 61 | "devDependencies": { 62 | "@babel/core": "^7.25.2", 63 | "@types/jest": "^29.5.12", 64 | "@types/react": "~18.3.12", 65 | "@types/react-test-renderer": "^18.3.0", 66 | "biome": "^0.3.3", 67 | "jest": "^29.2.1", 68 | "jest-expo": "~52.0.3", 69 | "react-test-renderer": "18.3.1", 70 | "typescript": "^5.3.3" 71 | }, 72 | "private": true 73 | } 74 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { Colors } from "./constants/colors"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"], 6 | presets: [require("nativewind/preset")], 7 | theme: { 8 | extend: { 9 | colors: { 10 | background: Colors.background, 11 | backgroundSecondary: Colors.backgroundSecondary, 12 | primary: Colors.primary, 13 | secondary: Colors.secondary, 14 | link: Colors.link, 15 | accent: Colors.accent, 16 | border: Colors.border, 17 | icon: Colors.icon, 18 | tabIconDefault: Colors.tabIconDefault, 19 | tabIconSelected: Colors.tabIconSelected, 20 | }, 21 | spacing: { 22 | xs: 4, 23 | sm: 8, 24 | md: 16, 25 | lg: 24, 26 | xl: 36, 27 | }, 28 | }, 29 | }, 30 | plugins: [], 31 | }; 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "paths": { 6 | "@/*": [ 7 | "./*" 8 | ] 9 | } 10 | }, 11 | "include": [ 12 | "**/*.ts", 13 | "**/*.tsx", 14 | ".expo/types/**/*.ts", 15 | "expo-env.d.ts", 16 | "nativewind-env.d.ts" 17 | ] 18 | } --------------------------------------------------------------------------------