├── .env.example ├── .gitignore ├── README.md ├── app.json ├── app ├── (tabs) │ ├── _layout.tsx │ ├── buy.tsx │ ├── index.tsx │ ├── read.tsx │ └── write.tsx ├── +html.tsx ├── +native-intent.tsx ├── +not-found.tsx └── _layout.tsx ├── assets ├── fonts │ └── SpaceMono-Regular.ttf └── images │ ├── adaptive-icon.png │ ├── apple.png │ ├── facebook.png │ ├── favicon.png │ ├── google.png │ ├── icon.png │ ├── partial-react-logo.png │ ├── react-banner.png │ ├── react-logo.png │ ├── react-logo@2x.png │ ├── react-logo@3x.png │ ├── splash.png │ ├── thirdweb.png │ └── title.png ├── babel.config.js ├── components ├── Collapsible.tsx ├── ExternalLink.tsx ├── HelloWave.tsx ├── ParallaxScrollView.tsx ├── SocialProfileCard.tsx ├── ThemedButton.tsx ├── ThemedInput.tsx ├── ThemedText.tsx ├── ThemedView.tsx ├── __tests__ │ ├── ThemedText-test.tsx │ └── __snapshots__ │ │ └── ThemedText-test.tsx.snap └── navigation │ └── TabBarIcon.tsx ├── constants ├── Colors.ts └── thirdweb.ts ├── eas.json ├── hooks ├── useColorScheme.ts ├── useColorScheme.web.ts └── useThemeColor.ts ├── index.js ├── metro.config.js ├── package.json ├── scripts └── reset-project.js ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_THIRDWEB_CLIENT_ID= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 17 | # The following patterns were generated by expo-cli 18 | 19 | expo-env.d.ts 20 | # @end expo-cli 21 | 22 | .env 23 | .env.local 24 | biome.json 25 | .yalc/ 26 | yalc.lock 27 | android/ 28 | ios/ 29 | eas/ 30 | credentials.json 31 | public/.well-known/apple-app-site-association 32 | public/.well-known/assetlinks.json 33 | eas-android.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Banner](https://github.com/thirdweb-example/thirdweb-auth-express/assets/17715009/06383e68-9c65-4265-8505-e88e573443f9) 2 | # thirdweb expo starter 3 | 4 | Starter template to build an onchain react native app with [thirdweb](https://thirdweb.com/) and [expo](https://expo.dev/). 5 | 6 | ### Features 7 | 8 | - in-app wallets using phone number, email or social logins to create a wallet for the user 9 | - smart accounts to sponsor gas 10 | - connecting to external wallets like MetaMask via WalletConnect 11 | - autoconnecting to the last connected wallet on launch 12 | - reading contract state and events 13 | - writing to the blockchain 14 | 15 | ## Installation 16 | 17 | Install the template using [thirdweb create](https://portal.thirdweb.com/cli/create) 18 | 19 | ```bash 20 | npx thirdweb create app --react-native 21 | ``` 22 | 23 | ## Get started 24 | 25 | 1. Install dependencies 26 | 27 | ```bash 28 | yarn install 29 | ``` 30 | 31 | 2. Get your thirdweb client id 32 | 33 | Rename the `.env.example` file to `.env` and paste in your thirdweb client id. 34 | 35 | You can obtain a free client id from the [thirdweb dashboard](https://thirdweb.com/dashboard/settings). 36 | 37 | 3. Prebuild the ios and android directories 38 | 39 | 40 | > [!IMPORTANT] 41 | > The thirdweb SDK uses native modules, which means it cannot run on expo GO. You must build the ios and android apps to link the native modules. 42 | 43 | ```bash 44 | npx expo prebuild 45 | ``` 46 | 47 | This will create the `ios` and `android` directories. 48 | 49 | 4. Start the app 50 | 51 | ```bash 52 | yarn ios 53 | ``` 54 | 55 | or 56 | 57 | ```bash 58 | yarn android 59 | ``` 60 | 61 | To run this app, you'll need either: 62 | 63 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 64 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) 65 | 66 | ## Troubleshooting 67 | 68 | ### OpenSSL Error on Xcode 16 69 | 70 | If using xcode 16, you may encounter a OpenSSL error when trying to build the app. This is because xcode 16 requires a newer version of OpenSSL than the one specified in the current app.json. 71 | 72 | To fix this, change the version of OpenSSL specified in the `app.json` file to `3.3.2000`. 73 | 74 | - Open the `app.json` file 75 | - Find the `ios` > `extraPods` section 76 | - Set `"version": "3.3.2000"` for the `OpenSSL-Universal` pod 77 | - Save the file 78 | 79 | Then run `npx expo prebuild` to update the native modules with the new OpenSSL version and run the app again. 80 | 81 | ## Additional Resources 82 | 83 | - [Documentation](https://portal.thirdweb.com/typescript/v5) 84 | - [Templates](https://thirdweb.com/templates) 85 | - [YouTube](https://www.youtube.com/c/thirdweb) 86 | 87 | ## Support 88 | 89 | For help or feedback, please [visit our support site](https://thirdweb.com/support) 90 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "thirdweb playground", 4 | "slug": "thirdweb-playground", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "com.thirdweb.demo", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true, 17 | "bundleIdentifier": "com.thirdweb.demo", 18 | "associatedDomains": [ 19 | "webcredentials:thirdweb.com", 20 | "applinks:thirdweb.com" 21 | ] 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/images/adaptive-icon.png", 26 | "backgroundColor": "#ffffff" 27 | }, 28 | "package": "com.thirdweb.demo", 29 | "intentFilters": [ 30 | { 31 | "autoVerify": true, 32 | "action": "VIEW", 33 | "data": { 34 | "scheme": "https", 35 | "host": "thirdweb.com" 36 | }, 37 | "category": ["BROWSABLE", "DEFAULT"] 38 | } 39 | ] 40 | }, 41 | "web": { 42 | "bundler": "metro", 43 | "output": "static", 44 | "favicon": "./assets/images/favicon.png" 45 | }, 46 | "plugins": [ 47 | "expo-router", 48 | [ 49 | "expo-build-properties", 50 | { 51 | "android": { 52 | "minSdkVersion": 26 53 | }, 54 | "ios": { 55 | "extraPods": [ 56 | { 57 | "name": "OpenSSL-Universal", 58 | "configurations": ["Release", "Debug"], 59 | "modular_headers": true, 60 | "version": "3.3.2000" 61 | } 62 | ] 63 | } 64 | } 65 | ] 66 | ], 67 | "experiments": { 68 | "typedRoutes": true 69 | }, 70 | "extra": { 71 | "router": { 72 | "origin": false 73 | }, 74 | "eas": { 75 | "projectId": "d1d7acaf-b44a-4425-af35-620596448499" 76 | } 77 | }, 78 | "owner": "thirdweb" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { TabBarIcon } from "@/components/navigation/TabBarIcon"; 2 | import { Colors } from "@/constants/Colors"; 3 | import { useColorScheme } from "@/hooks/useColorScheme"; 4 | import { Tabs } from "expo-router"; 5 | import React from "react"; 6 | 7 | export default function TabLayout() { 8 | const colorScheme = useColorScheme(); 9 | 10 | return ( 11 | 17 | ( 22 | 26 | ), 27 | }} 28 | /> 29 | ( 34 | 38 | ), 39 | }} 40 | /> 41 | ( 46 | 50 | ), 51 | }} 52 | /> 53 | ( 58 | 62 | ), 63 | }} 64 | /> 65 | 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /app/(tabs)/buy.tsx: -------------------------------------------------------------------------------- 1 | import AsyncStorage from "@react-native-async-storage/async-storage"; 2 | import { openURL } from "expo-linking"; 3 | import { Image, Linking, StyleSheet, View, useColorScheme } from "react-native"; 4 | import { ParallaxScrollView } from "../../components/ParallaxScrollView"; 5 | import { ThemedButton } from "../../components/ThemedButton"; 6 | import { ThemedText } from "../../components/ThemedText"; 7 | import { ThemedView } from "../../components/ThemedView"; 8 | import { Colors } from "../../constants/Colors"; 9 | import { client } from "../../constants/thirdweb"; 10 | import { useThemeColor } from "../../hooks/useThemeColor"; 11 | 12 | export default function BuyScreen() { 13 | return ( 14 | 21 | } 22 | > 23 | 24 | Buy with crypto or fiat 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | function BuySection() { 32 | const borderColor = useThemeColor( 33 | { light: Colors.light.border, dark: Colors.dark.border }, 34 | "border", 35 | ); 36 | return ( 37 | 38 | 39 | thirdweb hoodie (Large) 40 | 41 | 47 | 48 | Price: $2.00 49 | 50 | 51 | { 54 | const url = await makeUrl(); 55 | const mmUrl = new URL( 56 | `https://metamask.app.link/dapp/${url.toString()}`, 57 | ); 58 | openURL(mmUrl.toString()); 59 | }} 60 | /> 61 | { 64 | const url = encodeURIComponent((await makeUrl()).toString()); 65 | const mmUrl = new URL( 66 | `https://phantom.app/ul/browse/${url}?ref=${url}`, 67 | ); 68 | openURL(mmUrl.toString()); 69 | }} 70 | /> 71 | 72 | 73 | ); 74 | } 75 | 76 | async function makeUrl() { 77 | const authToken = await AsyncStorage.getItem( 78 | `walletToken-${client.clientId}`, 79 | ); 80 | const url = new URL("https://thirdweb.com/pay"); 81 | url.searchParams.set("clientId", client.clientId); 82 | url.searchParams.set("chainId", "8453"); 83 | url.searchParams.set( 84 | "tokenAddress", 85 | "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", 86 | ); 87 | url.searchParams.set( 88 | "recipientAddress", 89 | "0x2247d5d238d0f9d37184d8332aE0289d1aD9991b", 90 | ); 91 | url.searchParams.set("amount", "2000000"); 92 | url.searchParams.set("redirectUri", "com.thirdweb.demo://"); 93 | url.searchParams.set("theme", "light"); 94 | url.searchParams.set("name", "thirdweb hoodie"); 95 | url.searchParams.set("preferredWallet", "io.metamask"); 96 | url.searchParams.set( 97 | "image", 98 | "https://playground.thirdweb.com/drip-hoodie.png", 99 | ); 100 | if (authToken) { 101 | url.searchParams.set("authCookie", authToken); 102 | url.searchParams.set("walletId", "inApp"); 103 | url.searchParams.set("authProvider", "google"); 104 | } 105 | return url; 106 | } 107 | 108 | const styles = StyleSheet.create({ 109 | titleContainer: { 110 | flexDirection: "row", 111 | alignItems: "center", 112 | gap: 8, 113 | }, 114 | stepContainer: { 115 | flexDirection: "column", 116 | gap: 16, 117 | marginBottom: 8, 118 | padding: 16, 119 | borderRadius: 16, 120 | borderWidth: 1, 121 | }, 122 | reactLogo: { 123 | height: "100%", 124 | width: "100%", 125 | bottom: 0, 126 | left: 0, 127 | position: "absolute", 128 | }, 129 | buyImage: { 130 | width: "100%", 131 | height: 200, 132 | }, 133 | }); 134 | -------------------------------------------------------------------------------- /app/(tabs)/index.tsx: -------------------------------------------------------------------------------- 1 | import { Image, StyleSheet, View, useColorScheme } from "react-native"; 2 | 3 | import { ParallaxScrollView } from "@/components/ParallaxScrollView"; 4 | import { ThemedButton } from "@/components/ThemedButton"; 5 | import { ThemedText } from "@/components/ThemedText"; 6 | import { ThemedView } from "@/components/ThemedView"; 7 | import { chain, client } from "@/constants/thirdweb"; 8 | import { useEffect, useState } from "react"; 9 | import { createAuth } from "thirdweb/auth"; 10 | import { baseSepolia, ethereum } from "thirdweb/chains"; 11 | import { 12 | ConnectButton, 13 | ConnectEmbed, 14 | lightTheme, 15 | useActiveAccount, 16 | useActiveWallet, 17 | useConnect, 18 | useDisconnect, 19 | } from "thirdweb/react"; 20 | import { shortenAddress } from "thirdweb/utils"; 21 | import { createWallet } from "thirdweb/wallets"; 22 | import { 23 | getUserEmail, 24 | hasStoredPasskey, 25 | inAppWallet, 26 | } from "thirdweb/wallets/in-app"; 27 | 28 | const wallets = [ 29 | inAppWallet({ 30 | auth: { 31 | options: [ 32 | "google", 33 | "facebook", 34 | "discord", 35 | "telegram", 36 | "email", 37 | "phone", 38 | "passkey", 39 | ], 40 | passkeyDomain: "thirdweb.com", 41 | }, 42 | smartAccount: { 43 | chain: baseSepolia, 44 | sponsorGas: true, 45 | }, 46 | }), 47 | createWallet("io.metamask"), 48 | createWallet("com.coinbase.wallet", { 49 | appMetadata: { 50 | name: "Thirdweb RN Demo", 51 | }, 52 | mobileConfig: { 53 | callbackURL: "com.thirdweb.demo://", 54 | }, 55 | walletConfig: { 56 | options: "smartWalletOnly", 57 | }, 58 | }), 59 | createWallet("me.rainbow"), 60 | createWallet("com.trustwallet.app"), 61 | createWallet("io.zerion.wallet"), 62 | ]; 63 | 64 | const thirdwebAuth = createAuth({ 65 | domain: "localhost:3000", 66 | client, 67 | }); 68 | 69 | // fake login state, this should be returned from the backend 70 | let isLoggedIn = false; 71 | 72 | export default function HomeScreen() { 73 | const account = useActiveAccount(); 74 | const theme = useColorScheme(); 75 | return ( 76 | 83 | } 84 | > 85 | 86 | Connecting Wallets 87 | 88 | 89 | {``} 90 | 91 | Configurable button + modal, handles both connection and connected 92 | state. Example below has Smart Accounts + sponsored transactions 93 | enabled. 94 | 95 | 96 | 102 | 103 | {`Themed `} 104 | 105 | Styled the Connect Button to match your app. 106 | 107 | 108 | 139 | 140 | 141 | {``} 142 | 143 | Embeddable connection component in any screen. Example below is 144 | configured with a specific list of EOAs + SIWE. 145 | 146 | 147 | setTimeout(resolve, 2000)); 156 | const verifiedPayload = await thirdwebAuth.verifyPayload(params); 157 | isLoggedIn = verifiedPayload.valid; 158 | }, 159 | async doLogout() { 160 | isLoggedIn = false; 161 | }, 162 | async getLoginPayload(params) { 163 | return thirdwebAuth.generatePayload(params); 164 | }, 165 | async isLoggedIn(address) { 166 | return isLoggedIn; 167 | }, 168 | }} 169 | /> 170 | {account && ( 171 | 172 | ConnectEmbed does not render when connected, use the `onConnect` prop 173 | to navigate to a new screen instead. 174 | 175 | )} 176 | 177 | 178 | {`useConnect()`} 179 | 180 | Hooks to build your own UI. Example below connects to a smart Google 181 | account or metamask EOA. 182 | 183 | 184 | 185 | 186 | ); 187 | } 188 | 189 | const CustomConnectUI = () => { 190 | const wallet = useActiveWallet(); 191 | const account = useActiveAccount(); 192 | const [email, setEmail] = useState(); 193 | const { disconnect } = useDisconnect(); 194 | useEffect(() => { 195 | if (wallet && wallet.id === "inApp") { 196 | getUserEmail({ client }).then(setEmail); 197 | } 198 | }, [wallet]); 199 | 200 | return wallet && account ? ( 201 | 202 | Connected as {shortenAddress(account.address)} 203 | {email && {email}} 204 | 205 | disconnect(wallet)} title="Disconnect" /> 206 | 207 | ) : ( 208 | <> 209 | 210 | 211 | 212 | 213 | ); 214 | }; 215 | 216 | const ConnectWithGoogle = () => { 217 | const { connect, isConnecting } = useConnect(); 218 | return ( 219 | { 224 | connect(async () => { 225 | const w = inAppWallet({ 226 | smartAccount: { 227 | chain, 228 | sponsorGas: true, 229 | }, 230 | }); 231 | await w.connect({ 232 | client, 233 | strategy: "google", 234 | }); 235 | return w; 236 | }); 237 | }} 238 | /> 239 | ); 240 | }; 241 | 242 | const ConnectWithMetaMask = () => { 243 | const { connect, isConnecting } = useConnect(); 244 | return ( 245 | { 251 | connect(async () => { 252 | const w = createWallet("io.metamask"); 253 | await w.connect({ 254 | client, 255 | }); 256 | return w; 257 | }); 258 | }} 259 | /> 260 | ); 261 | }; 262 | 263 | const ConnectWithPasskey = () => { 264 | const { connect } = useConnect(); 265 | return ( 266 | { 269 | connect(async () => { 270 | const hasPasskey = await hasStoredPasskey(client); 271 | const w = inAppWallet({ 272 | auth: { 273 | options: ["passkey"], 274 | passkeyDomain: "thirdweb.com", 275 | }, 276 | }); 277 | await w.connect({ 278 | client, 279 | strategy: "passkey", 280 | type: hasPasskey ? "sign-in" : "sign-up", 281 | }); 282 | return w; 283 | }); 284 | }} 285 | /> 286 | ); 287 | }; 288 | 289 | const styles = StyleSheet.create({ 290 | titleContainer: { 291 | flexDirection: "row", 292 | alignItems: "center", 293 | gap: 8, 294 | }, 295 | stepContainer: { 296 | gap: 8, 297 | marginBottom: 8, 298 | }, 299 | reactLogo: { 300 | height: "100%", 301 | width: "100%", 302 | bottom: 0, 303 | left: 0, 304 | position: "absolute", 305 | }, 306 | rowContainer: { 307 | flexDirection: "row", 308 | flexWrap: "wrap", 309 | gap: 24, 310 | justifyContent: "space-evenly", 311 | }, 312 | tableContainer: { 313 | width: "100%", 314 | }, 315 | tableRow: { 316 | flexDirection: "row", 317 | justifyContent: "space-between", 318 | marginBottom: 4, 319 | }, 320 | leftColumn: { 321 | flex: 1, 322 | textAlign: "left", 323 | }, 324 | rightColumn: { 325 | flex: 1, 326 | textAlign: "right", 327 | }, 328 | }); 329 | -------------------------------------------------------------------------------- /app/(tabs)/read.tsx: -------------------------------------------------------------------------------- 1 | import { ParallaxScrollView } from "@/components/ParallaxScrollView"; 2 | import { SocialProfilesList } from "@/components/SocialProfileCard"; 3 | import { ThemedText } from "@/components/ThemedText"; 4 | import { ThemedView } from "@/components/ThemedView"; 5 | import { client, contract, usdcContract } from "@/constants/thirdweb"; 6 | import { ActivityIndicator, Image, StyleSheet, View } from "react-native"; 7 | import { toTokens } from "thirdweb"; 8 | import { transferEvent } from "thirdweb/extensions/erc20"; 9 | import { totalSupply } from "thirdweb/extensions/erc721"; 10 | import { useContractEvents, useReadContract } from "thirdweb/react"; 11 | import { shortenAddress } from "thirdweb/utils"; 12 | 13 | export default function ReadScreen() { 14 | return ( 15 | 22 | } 23 | > 24 | 25 | Read onchain data 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | function SocialSection() { 35 | return ( 36 | 37 | 38 | {`useSocialProfiles()`} 39 | 40 | Fetch all known social profiles for any wallet address. 41 | 42 | 43 | 47 | 48 | ); 49 | } 50 | 51 | function ReadSection() { 52 | const nameQuery = useReadContract({ 53 | contract, 54 | method: "function name() returns (string)", 55 | }); 56 | const supplyQuery = useReadContract(totalSupply, { 57 | contract, 58 | }); 59 | 60 | return ( 61 | 62 | 63 | useReadContract() 64 | 65 | Hook to read contract data, with auto refetching. 66 | 67 | 68 | 69 | 70 | Contract name:{" "} 71 | {nameQuery.data}{" "} 72 | 73 | 74 | Supply:{" "} 75 | 76 | {supplyQuery.data?.toString()} 77 | {" "} 78 | 79 | 80 | 81 | ); 82 | } 83 | 84 | function EventsSection() { 85 | const eventsQuery = useContractEvents({ 86 | contract: usdcContract, 87 | events: [transferEvent()], 88 | blockRange: 10, 89 | }); 90 | 91 | return ( 92 | 93 | 94 | useContractEvents() 95 | 96 | Hook to subscribe to live contract events. 97 | 98 | 99 | 100 | 101 | Live USDC transfers 102 | 103 | {eventsQuery.isLoading && } 104 | {eventsQuery.data 105 | ?.slice(-10) 106 | ?.reverse() 107 | ?.map((event, i) => { 108 | return ( 109 | 110 | 111 | {shortenAddress(event.args.from)} sent{" "} 112 | 113 | {toTokens(event.args.value, 6)} USDC 114 | {" "} 115 | 116 | 117 | ); 118 | })} 119 | 120 | ); 121 | } 122 | 123 | const styles = StyleSheet.create({ 124 | titleContainer: { 125 | flexDirection: "row", 126 | alignItems: "center", 127 | gap: 8, 128 | }, 129 | stepContainer: { 130 | gap: 8, 131 | marginBottom: 8, 132 | }, 133 | reactLogo: { 134 | height: "100%", 135 | width: "100%", 136 | bottom: 0, 137 | left: 0, 138 | position: "absolute", 139 | }, 140 | }); 141 | -------------------------------------------------------------------------------- /app/(tabs)/write.tsx: -------------------------------------------------------------------------------- 1 | import { Image, Linking, StyleSheet, View } from "react-native"; 2 | 3 | import { ParallaxScrollView } from "@/components/ParallaxScrollView"; 4 | import { ThemedButton } from "@/components/ThemedButton"; 5 | import { ThemedText } from "@/components/ThemedText"; 6 | import { ThemedView } from "@/components/ThemedView"; 7 | import { client, contract } from "@/constants/thirdweb"; 8 | import { Link } from "expo-router"; 9 | import { balanceOf, claimTo, getNFT } from "thirdweb/extensions/erc721"; 10 | import { 11 | useActiveAccount, 12 | useReadContract, 13 | useSendAndConfirmTransaction, 14 | } from "thirdweb/react"; 15 | import { resolveScheme } from "thirdweb/storage"; 16 | import { shortenAddress } from "thirdweb/utils"; 17 | 18 | export default function WriteScreen() { 19 | return ( 20 | 27 | } 28 | > 29 | 30 | Transactions 31 | 32 | 33 | useSendTransaction() 34 | 35 | Hook to submit transactions onchain from the connected wallet. 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | function WriteSection() { 45 | const account = useActiveAccount(); 46 | const sendMutation = useSendAndConfirmTransaction(); 47 | const balanceQuery = useReadContract(balanceOf, { 48 | contract, 49 | owner: account?.address!, 50 | queryOptions: { enabled: !!account }, 51 | }); 52 | const nftQuery = useReadContract(getNFT, { 53 | contract, 54 | tokenId: 1n, 55 | }); 56 | 57 | const mint = async () => { 58 | if (!account) return; 59 | sendMutation.mutate( 60 | claimTo({ 61 | contract, 62 | quantity: 1n, 63 | to: account.address, 64 | }), 65 | ); 66 | }; 67 | 68 | return ( 69 | <> 70 | {account ? ( 71 | <> 72 | 73 | 74 | Wallet:{" "} 75 | 76 | {shortenAddress(account.address)} 77 | 78 | 79 | 80 | NFTs owned:{" "} 81 | 82 | {balanceQuery.data?.toString()} 83 | 84 | 85 | 86 | {nftQuery.data && ( 87 | 90 | 100 | 101 | )} 102 | 108 | {sendMutation.error && ( 109 | 110 | {sendMutation.error.message} 111 | 112 | )} 113 | 114 | ) : ( 115 | <> 116 | 117 | 118 | Connect 119 | {" "} 120 | a wallet to perform transactions 121 | 122 | 123 | )} 124 | 125 | ); 126 | } 127 | 128 | const styles = StyleSheet.create({ 129 | titleContainer: { 130 | flexDirection: "row", 131 | alignItems: "center", 132 | gap: 8, 133 | }, 134 | stepContainer: { 135 | gap: 8, 136 | marginBottom: 8, 137 | }, 138 | reactLogo: { 139 | height: "100%", 140 | width: "100%", 141 | bottom: 0, 142 | left: 0, 143 | position: "absolute", 144 | }, 145 | }); 146 | -------------------------------------------------------------------------------- /app/+html.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollViewStyleReset } from 'expo-router/html'; 2 | import { type PropsWithChildren } from 'react'; 3 | 4 | /** 5 | * This file is web-only and used to configure the root HTML for every web page during static rendering. 6 | * The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs. 7 | */ 8 | export default function Root({ children }: PropsWithChildren) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | {/* 17 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. 18 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. 19 | */} 20 | 21 | 22 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} 23 |