├── .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 | 
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 |
24 | {/* Add any additional elements that you want globally available on web... */}
25 |
26 | {children}
27 |
28 | );
29 | }
30 |
31 | const responsiveBackground = `
32 | body {
33 | background-color: #fff;
34 | }
35 | @media (prefers-color-scheme: dark) {
36 | body {
37 | background-color: #000;
38 | }
39 | }`;
40 |
--------------------------------------------------------------------------------
/app/+native-intent.tsx:
--------------------------------------------------------------------------------
1 | export function redirectSystemPath(options: { path: string }) {
2 | console.log("redirectSystemPath", options);
3 | // redirect all system links to the root path
4 | return "/";
5 | }
6 |
--------------------------------------------------------------------------------
/app/+not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Link, Stack } from "expo-router";
2 | import { StyleSheet } from "react-native";
3 |
4 | import { ThemedText } from "@/components/ThemedText";
5 | import { ThemedView } from "@/components/ThemedView";
6 |
7 | export default function NotFoundScreen() {
8 | return (
9 | <>
10 |
11 |
12 | This screen doesn't exist.
13 |
14 | Go to home screen!
15 |
16 |
17 | >
18 | );
19 | }
20 |
21 | const styles = StyleSheet.create({
22 | container: {
23 | flex: 1,
24 | alignItems: "center",
25 | justifyContent: "center",
26 | padding: 20,
27 | },
28 | link: {
29 | marginTop: 15,
30 | paddingVertical: 15,
31 | },
32 | });
33 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | DarkTheme,
3 | DefaultTheme,
4 | ThemeProvider,
5 | } from "@react-navigation/native";
6 | import { useFonts } from "expo-font";
7 | import { Stack } from "expo-router";
8 | import * as SplashScreen from "expo-splash-screen";
9 | import { useEffect } from "react";
10 | import { ThirdwebProvider } from "thirdweb/react";
11 |
12 | import { useColorScheme } from "@/hooks/useColorScheme";
13 | import { StatusBar } from "react-native";
14 | import { Colors } from "../constants/Colors";
15 |
16 | // Prevent the splash screen from auto-hiding before asset loading is complete.
17 | SplashScreen.preventAutoHideAsync();
18 |
19 | export default function RootLayout() {
20 | const colorScheme = useColorScheme();
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 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/apple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/apple.png
--------------------------------------------------------------------------------
/assets/images/facebook.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/facebook.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/google.png
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/assets/images/react-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/react-banner.png
--------------------------------------------------------------------------------
/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/react-logo.png
--------------------------------------------------------------------------------
/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/splash.png
--------------------------------------------------------------------------------
/assets/images/thirdweb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/thirdweb.png
--------------------------------------------------------------------------------
/assets/images/title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thirdweb-example/expo-starter/82848918c2ee35722fffc4b9e475ba68f64aaa4e/assets/images/title.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ["babel-preset-expo"],
5 | plugins: [
6 | "react-native-reanimated/plugin",
7 | ],
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/components/Collapsible.tsx:
--------------------------------------------------------------------------------
1 | import Ionicons from "@expo/vector-icons/Ionicons";
2 | import { type PropsWithChildren, useState } from "react";
3 | import { StyleSheet, TouchableOpacity, useColorScheme } from "react-native";
4 |
5 | import { ThemedText } from "@/components/ThemedText";
6 | import { ThemedView } from "@/components/ThemedView";
7 | import { Colors } from "@/constants/Colors";
8 |
9 | export function Collapsible({
10 | children,
11 | title,
12 | }: PropsWithChildren & { title: string }) {
13 | const [isOpen, setIsOpen] = useState(false);
14 | const theme = useColorScheme() ?? "light";
15 |
16 | return (
17 |
18 | setIsOpen((value) => !value)}
21 | activeOpacity={0.8}
22 | >
23 |
28 | {title}
29 |
30 | {isOpen && {children}}
31 |
32 | );
33 | }
34 |
35 | const styles = StyleSheet.create({
36 | heading: {
37 | flexDirection: "row",
38 | alignItems: "center",
39 | gap: 6,
40 | },
41 | content: {
42 | marginTop: 6,
43 | marginLeft: 24,
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/components/ExternalLink.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "expo-router";
2 | import { openBrowserAsync } from "expo-web-browser";
3 | import type { ComponentProps } from "react";
4 | import { Platform } from "react-native";
5 |
6 | type Props = Omit, "href"> & { href: string };
7 |
8 | export function ExternalLink({ href, ...rest }: Props) {
9 | return (
10 | {
15 | if (Platform.OS !== "web") {
16 | // Prevent the default behavior of linking to the default browser on native.
17 | event.preventDefault();
18 | // Open the link in an in-app browser.
19 | await openBrowserAsync(href);
20 | }
21 | }}
22 | />
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/HelloWave.tsx:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from "react-native";
2 | import Animated, {
3 | useSharedValue,
4 | useAnimatedStyle,
5 | withTiming,
6 | withRepeat,
7 | withSequence,
8 | } from "react-native-reanimated";
9 |
10 | import { ThemedText } from "@/components/ThemedText";
11 |
12 | export function HelloWave() {
13 | const rotationAnimation = useSharedValue(0);
14 |
15 | rotationAnimation.value = withRepeat(
16 | withSequence(
17 | withTiming(25, { duration: 150 }),
18 | withTiming(0, { duration: 150 }),
19 | ),
20 | 4, // Run the animation 4 times
21 | );
22 |
23 | const animatedStyle = useAnimatedStyle(() => ({
24 | transform: [{ rotate: `${rotationAnimation.value}deg` }],
25 | }));
26 |
27 | return (
28 |
29 | 👋
30 |
31 | );
32 | }
33 |
34 | const styles = StyleSheet.create({
35 | text: {
36 | fontSize: 28,
37 | lineHeight: 32,
38 | marginTop: -6,
39 | },
40 | });
41 |
--------------------------------------------------------------------------------
/components/ParallaxScrollView.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren, ReactElement } from "react";
2 | import { StyleSheet, useColorScheme } from "react-native";
3 | import Animated, {
4 | interpolate,
5 | useAnimatedRef,
6 | useAnimatedStyle,
7 | useScrollViewOffset,
8 | } from "react-native-reanimated";
9 |
10 | import { ThemedView } from "@/components/ThemedView";
11 |
12 | const HEADER_HEIGHT = 200;
13 |
14 | type Props = PropsWithChildren<{
15 | headerImage: ReactElement;
16 | headerBackgroundColor: { dark: string; light: string };
17 | }>;
18 |
19 | export function ParallaxScrollView({
20 | children,
21 | headerImage,
22 | headerBackgroundColor,
23 | }: Props) {
24 | const colorScheme = useColorScheme() ?? "light";
25 | const scrollRef = useAnimatedRef();
26 | const scrollOffset = useScrollViewOffset(scrollRef);
27 |
28 | const headerAnimatedStyle = useAnimatedStyle(() => {
29 | return {
30 | transform: [
31 | {
32 | translateY: interpolate(
33 | scrollOffset.value,
34 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
35 | [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75],
36 | ),
37 | },
38 | {
39 | scale: interpolate(
40 | scrollOffset.value,
41 | [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
42 | [2, 1, 1],
43 | ),
44 | },
45 | ],
46 | };
47 | });
48 |
49 | return (
50 |
51 |
52 |
59 | {headerImage}
60 |
61 | {children}
62 |
63 |
64 | );
65 | }
66 |
67 | const styles = StyleSheet.create({
68 | container: {
69 | flex: 1,
70 | },
71 | header: {
72 | height: HEADER_HEIGHT,
73 | overflow: "hidden",
74 | },
75 | content: {
76 | flex: 1,
77 | padding: 32,
78 | gap: 16,
79 | overflow: "hidden",
80 | },
81 | });
82 |
--------------------------------------------------------------------------------
/components/SocialProfileCard.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { View, StyleSheet, Image } from "react-native";
3 | import { ThemedText } from "@/components/ThemedText";
4 | import { useSocialProfiles } from "thirdweb/react";
5 | import { ThirdwebClient } from "thirdweb";
6 | import { resolveScheme } from "thirdweb/storage";
7 | import { SocialProfile, FarcasterProfile, getSocialProfiles } from "thirdweb/social";
8 | import { ThemedView } from "./ThemedView";
9 |
10 | interface SocialProfileCardProps {
11 | address: string | undefined;
12 | client: ThirdwebClient;
13 | }
14 |
15 | export function SocialProfilesList({
16 | client,
17 | address,
18 | }: SocialProfileCardProps) {
19 | const profiles = useSocialProfiles({
20 | client,
21 | address,
22 | });
23 |
24 | if (profiles.isLoading) {
25 | return Loading...;
26 | }
27 |
28 | return profiles.data?.length ? (
29 | profiles.data?.map((profile) => )
30 | ) : (
31 | No social profiles found
32 | );
33 | }
34 |
35 | export function SocialProfileCard({
36 | profile,
37 | client,
38 | }: {
39 | profile: SocialProfile | undefined;
40 | client: ThirdwebClient;
41 | }) {
42 | if (!profile) return null;
43 | return (
44 |
45 |
46 | {profile.avatar && (
47 |
48 | )}
49 |
50 | {profile.name}
51 | {profile.bio || "-"}
52 | {profile.type}
53 |
54 |
55 |
56 | );
57 | }
58 |
59 | const styles = StyleSheet.create({
60 | card: {
61 | borderRadius: 8,
62 | padding: 16,
63 | marginVertical: 8,
64 | shadowColor: "#000",
65 | shadowOffset: {
66 | width: 0,
67 | height: 2,
68 | },
69 | shadowOpacity: 0.23,
70 | shadowRadius: 2.62,
71 | elevation: 4,
72 | },
73 | contentContainer: {
74 | flexDirection: "row",
75 | alignItems: "center",
76 | },
77 | avatar: {
78 | width: 60,
79 | height: 60,
80 | borderRadius: 30,
81 | marginRight: 16,
82 | },
83 | tableContainer: {
84 | flex: 1,
85 | flexDirection: "column",
86 | alignItems: "flex-start",
87 | },
88 | });
89 |
--------------------------------------------------------------------------------
/components/ThemedButton.tsx:
--------------------------------------------------------------------------------
1 | import { useThemeColor } from "@/hooks/useThemeColor";
2 | import {
3 | type PressableProps,
4 | StyleSheet,
5 | TouchableOpacity,
6 | ActivityIndicator,
7 | } from "react-native";
8 | import { ThemedText } from "./ThemedText";
9 |
10 | export type ThemedButtonProps = {
11 | lightColor?: string;
12 | darkColor?: string;
13 | onPress?: PressableProps["onPress"];
14 | title: string;
15 | loading?: boolean;
16 | loadingTitle?: string;
17 | variant?: "primary" | "secondary";
18 | };
19 |
20 | export function ThemedButton(props: ThemedButtonProps) {
21 | const variant = props.variant ?? "primary";
22 | const bg = useThemeColor(
23 | { light: props.lightColor, dark: props.darkColor },
24 | "tint",
25 | );
26 | const textInverted = useThemeColor(
27 | { light: props.lightColor, dark: props.darkColor },
28 | "textInverted",
29 | );
30 | const text = useThemeColor(
31 | { light: props.lightColor, dark: props.darkColor },
32 | "text",
33 | );
34 | const textColor = variant == "secondary" ? text : textInverted;
35 | return (
36 | {
48 | props.onPress?.(e);
49 | }}
50 | >
51 | {props.loading && (
52 |
53 | )}
54 |
55 | {props.loading ? props.loadingTitle : props.title}
56 |
57 |
58 | );
59 | }
60 |
61 | const styles = StyleSheet.create({
62 | button: {
63 | flex: 1,
64 | flexDirection: "row",
65 | gap: 8,
66 | padding: 12,
67 | borderRadius: 100,
68 | justifyContent: "center",
69 | alignItems: "center",
70 | },
71 | });
72 |
--------------------------------------------------------------------------------
/components/ThemedInput.tsx:
--------------------------------------------------------------------------------
1 | import { useThemeColor } from "@/hooks/useThemeColor";
2 | import React from "react";
3 | import {
4 | StyleSheet,
5 | TextInputProps,
6 | TextInput,
7 | TouchableOpacity,
8 | ActivityIndicator,
9 | } from "react-native";
10 | import { ThemedView } from "./ThemedView";
11 | import Ionicons from "@expo/vector-icons/Ionicons";
12 |
13 | export type ThemedInputProps = {
14 | lightColor?: string;
15 | darkColor?: string;
16 | onSubmit?: (value: string) => void;
17 | isSubmitting?: boolean;
18 | } & TextInputProps;
19 |
20 | export function ThemedInput(props: ThemedInputProps) {
21 | const [val, setVal] = React.useState("");
22 | const onSubmit = props.onSubmit;
23 | const borderColor = useThemeColor(
24 | { light: props.lightColor, dark: props.darkColor },
25 | "border",
26 | );
27 | const primaryText = useThemeColor(
28 | { light: props.lightColor, dark: props.darkColor },
29 | "text",
30 | );
31 | const secondaryText = useThemeColor(
32 | { light: props.lightColor, dark: props.darkColor },
33 | "icon",
34 | );
35 | return (
36 |
47 |
54 | {onSubmit && (
55 | onSubmit(val)}
57 | disabled={props.isSubmitting}
58 | style={{
59 | paddingVertical: 12,
60 | paddingHorizontal: 16,
61 | }}
62 | >
63 | {props.isSubmitting ? (
64 |
65 | ) : (
66 |
71 | )}
72 |
73 | )}
74 |
75 | );
76 | }
77 |
78 | const styles = StyleSheet.create({
79 | input: {
80 | flex: 1,
81 | flexDirection: "row",
82 | gap: 8,
83 | padding: 12,
84 | justifyContent: "center",
85 | alignItems: "center",
86 | },
87 | });
88 |
--------------------------------------------------------------------------------
/components/ThemedText.tsx:
--------------------------------------------------------------------------------
1 | import { Text, type TextProps, StyleSheet } from "react-native";
2 |
3 | import { useThemeColor } from "@/hooks/useThemeColor";
4 |
5 | export type ThemedTextProps = TextProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | type?:
9 | | "default"
10 | | "title"
11 | | "defaultSemiBold"
12 | | "subtitle"
13 | | "link"
14 | | "subtext";
15 | };
16 |
17 | export function ThemedText({
18 | style,
19 | lightColor,
20 | darkColor,
21 | type = "default",
22 | ...rest
23 | }: ThemedTextProps) {
24 | const color = useThemeColor({ light: lightColor, dark: darkColor }, "text");
25 | const subTextColor = useThemeColor(
26 | { light: lightColor, dark: darkColor },
27 | "subtext",
28 | );
29 |
30 | return (
31 |
44 | );
45 | }
46 |
47 | const styles = StyleSheet.create({
48 | default: {
49 | fontSize: 16,
50 | lineHeight: 24,
51 | },
52 | subtext: {
53 | fontSize: 14,
54 | lineHeight: 20,
55 | },
56 | defaultSemiBold: {
57 | fontSize: 16,
58 | lineHeight: 24,
59 | fontWeight: "600",
60 | },
61 | title: {
62 | fontSize: 32,
63 | fontWeight: "bold",
64 | lineHeight: 32,
65 | },
66 | subtitle: {
67 | fontSize: 20,
68 | fontWeight: "bold",
69 | },
70 | link: {
71 | lineHeight: 30,
72 | fontSize: 16,
73 | color: "#0a7ea4",
74 | },
75 | });
76 |
--------------------------------------------------------------------------------
/components/ThemedView.tsx:
--------------------------------------------------------------------------------
1 | import { View, type ViewProps } from "react-native";
2 |
3 | import { useThemeColor } from "@/hooks/useThemeColor";
4 |
5 | export type ThemedViewProps = ViewProps & {
6 | lightColor?: string;
7 | darkColor?: string;
8 | };
9 |
10 | export function ThemedView({
11 | style,
12 | lightColor,
13 | darkColor,
14 | ...otherProps
15 | }: ThemedViewProps) {
16 | const backgroundColor = useThemeColor(
17 | { light: lightColor, dark: darkColor },
18 | "background",
19 | );
20 |
21 | return ;
22 | }
23 |
--------------------------------------------------------------------------------
/components/__tests__/ThemedText-test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import { ThemedText } from '../ThemedText';
5 |
6 | it(`renders correctly`, () => {
7 | const tree = renderer.create(Snapshot test!).toJSON();
8 |
9 | expect(tree).toMatchSnapshot();
10 | });
11 |
--------------------------------------------------------------------------------
/components/__tests__/__snapshots__/ThemedText-test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders correctly 1`] = `
4 |
22 | Snapshot test!
23 |
24 | `;
25 |
--------------------------------------------------------------------------------
/components/navigation/TabBarIcon.tsx:
--------------------------------------------------------------------------------
1 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
2 |
3 | import Ionicons from '@expo/vector-icons/Ionicons';
4 | import { type IconProps } from '@expo/vector-icons/build/createIconSet';
5 | import { type ComponentProps } from 'react';
6 |
7 | export function TabBarIcon({ style, ...rest }: IconProps['name']>) {
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/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 | const tintColorLight = "#3366aa";
7 | const tintColorDark = "#3366aa";
8 |
9 | export const Colors = {
10 | light: {
11 | text: "#11181C",
12 | subtext: "#687076",
13 | textInverted: "#fff",
14 | background: "#fff",
15 | tint: tintColorLight,
16 | icon: "#687076",
17 | tabIconDefault: "#687076",
18 | tabIconSelected: tintColorLight,
19 | border: "#ECEDEE",
20 | },
21 | dark: {
22 | text: "#ECEDEE",
23 | subtext: "#878792",
24 | textInverted: "#fff",
25 | background: "#151718",
26 | tint: tintColorDark,
27 | icon: "#9BA1A6",
28 | tabIconDefault: "#9BA1A6",
29 | tabIconSelected: tintColorDark,
30 | border: "#333333",
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/constants/thirdweb.ts:
--------------------------------------------------------------------------------
1 | import { createThirdwebClient, getContract } from "thirdweb";
2 | import { base, baseSepolia } from "thirdweb/chains";
3 |
4 | const clientId = process.env.EXPO_PUBLIC_THIRDWEB_CLIENT_ID!;
5 |
6 | if (!clientId) {
7 | throw new Error(
8 | "Missing EXPO_PUBLIC_THIRDWEB_CLIENT_ID - make sure to set it in your .env file",
9 | );
10 | }
11 |
12 | export const client = createThirdwebClient({
13 | clientId,
14 | });
15 |
16 | export const chain = base;
17 |
18 | export const contract = getContract({
19 | client,
20 | address: "0x82e50a6BF13A70366eDFC871f8FB8a428C43Dc03",
21 | chain: baseSepolia,
22 | });
23 |
24 | export const usdcContract = getContract({
25 | address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
26 | chain: base,
27 | client,
28 | });
29 |
--------------------------------------------------------------------------------
/eas.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": {
3 | "version": ">= 9.1.0"
4 | },
5 | "build": {
6 | "local": {
7 | "android": {
8 | "buildType": "apk"
9 | },
10 | "ios": {
11 | "simulator": true
12 | }
13 | },
14 | "development": {
15 | "developmentClient": true,
16 | "distribution": "internal"
17 | },
18 | "preview": {
19 | "distribution": "internal"
20 | },
21 | "production": {}
22 | },
23 | "submit": {
24 | "production": {}
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.ts:
--------------------------------------------------------------------------------
1 | export { useColorScheme } from 'react-native';
2 |
--------------------------------------------------------------------------------
/hooks/useColorScheme.web.ts:
--------------------------------------------------------------------------------
1 | // NOTE: The default React Native styling doesn't support server rendering.
2 | // Server rendered styles should not change between the first render of the HTML
3 | // and the first render on the client. Typically, web developers will use CSS media queries
4 | // to render different styles on the client and server, these aren't directly supported in React Native
5 | // but can be achieved using a styling library like Nativewind.
6 | export function useColorScheme() {
7 | return 'light';
8 | }
9 |
--------------------------------------------------------------------------------
/hooks/useThemeColor.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Learn more about light and dark modes:
3 | * https://docs.expo.dev/guides/color-schemes/
4 | */
5 |
6 | import { useColorScheme } from 'react-native';
7 |
8 | import { Colors } from '@/constants/Colors';
9 |
10 | export function useThemeColor(
11 | props: { light?: string; dark?: string },
12 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark
13 | ) {
14 | const theme = useColorScheme() ?? 'light';
15 | const colorFromProps = props[theme];
16 |
17 | if (colorFromProps) {
18 | return colorFromProps;
19 | } else {
20 | return Colors[theme][colorName];
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import { Platform } from "react-native";
2 |
3 | if (Platform.OS !== "web") {
4 | import("@thirdweb-dev/react-native-adapter");
5 | }
6 | import "react-native-reanimated";
7 | import "expo-router/entry";
8 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | // Learn more https://docs.expo.io/guides/customizing-metro
2 | const { getDefaultConfig } = require("expo/metro-config");
3 |
4 | /** @type {import('expo/metro-config').MetroConfig} */
5 | const config = getDefaultConfig(__dirname);
6 |
7 | module.exports = config;
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "expo-starter",
3 | "main": "./index.js",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "reset-project": "node ./scripts/reset-project.js",
8 | "android": "expo run:android",
9 | "ios": "expo run:ios",
10 | "test": "jest --watchAll",
11 | "lint": "expo lint"
12 | },
13 | "jest": {
14 | "preset": "jest-expo"
15 | },
16 | "dependencies": {
17 | "@coinbase/wallet-mobile-sdk": "^1.0.13",
18 | "@expo/vector-icons": "^14.1.0",
19 | "@react-native-async-storage/async-storage": "2.1.2",
20 | "@react-native-community/netinfo": "11.4.1",
21 | "@react-navigation/native": "^7.1.6",
22 | "@thirdweb-dev/react-native-adapter": "^1.5.4",
23 | "@walletconnect/react-native-compat": "^2.20.1",
24 | "amazon-cognito-identity-js": "^6.3.15",
25 | "ethers": "^6.13.7",
26 | "expo": "53.0.9",
27 | "expo-application": "6.1.4",
28 | "expo-build-properties": "0.14.6",
29 | "expo-constants": "~17.1.4",
30 | "expo-dev-client": "5.1.7",
31 | "expo-font": "13.3.0",
32 | "expo-linking": "7.1.4",
33 | "expo-router": "5.0.3",
34 | "expo-splash-screen": "~0.30.7",
35 | "expo-status-bar": "~2.2.3",
36 | "expo-system-ui": "~5.0.6",
37 | "expo-web-browser": "~14.1.6",
38 | "react": "19.0.0",
39 | "react-dom": "19.0.0",
40 | "react-native": "0.79.2",
41 | "react-native-aes-gcm-crypto": "0.2.2",
42 | "react-native-gesture-handler": "2.25.0",
43 | "react-native-get-random-values": "1.11.0",
44 | "react-native-mmkv": "^3.2.0",
45 | "react-native-passkey": "^3.0.0",
46 | "react-native-quick-crypto": "0.7.13",
47 | "react-native-reanimated": "3.17.5",
48 | "react-native-safe-area-context": "5.4.0",
49 | "react-native-screens": "4.10.0",
50 | "react-native-svg": "15.11.2",
51 | "react-native-web": "0.20.0",
52 | "thirdweb": "5.96.7"
53 | },
54 | "devDependencies": {
55 | "@babel/core": "^7.27.1",
56 | "@types/jest": "^29.5.12",
57 | "@types/react": "~19.1.2",
58 | "@types/react-test-renderer": "^19.1.0",
59 | "expo-atlas": "^0.4.0",
60 | "jest": "^29.2.1",
61 | "jest-expo": "~53.0.2",
62 | "react-test-renderer": "19.1.0",
63 | "typescript": "~5.8.3"
64 | },
65 | "private": true
66 | }
67 |
--------------------------------------------------------------------------------
/scripts/reset-project.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * This script is used to reset the project to a blank state.
5 | * It moves the /app directory to /app-example and creates a new /app directory with an index.tsx and _layout.tsx file.
6 | * You can remove the `reset-project` script from package.json and safely delete this file after running it.
7 | */
8 |
9 | const fs = require('fs');
10 | const path = require('path');
11 |
12 | const root = process.cwd();
13 | const oldDirPath = path.join(root, 'app');
14 | const newDirPath = path.join(root, 'app-example');
15 | const newAppDirPath = path.join(root, 'app');
16 |
17 | const indexContent = `import { Text, View } from "react-native";
18 |
19 | export default function Index() {
20 | return (
21 |
28 | Edit app/index.tsx to edit this screen.
29 |
30 | );
31 | }
32 | `;
33 |
34 | const layoutContent = `import { Stack } from "expo-router";
35 |
36 | export default function RootLayout() {
37 | return (
38 |
39 |
40 |
41 | );
42 | }
43 | `;
44 |
45 | fs.rename(oldDirPath, newDirPath, (error) => {
46 | if (error) {
47 | return console.error(`Error renaming directory: ${error}`);
48 | }
49 | console.log('/app moved to /app-example.');
50 |
51 | fs.mkdir(newAppDirPath, { recursive: true }, (error) => {
52 | if (error) {
53 | return console.error(`Error creating new app directory: ${error}`);
54 | }
55 | console.log('New /app directory created.');
56 |
57 | const indexPath = path.join(newAppDirPath, 'index.tsx');
58 | fs.writeFile(indexPath, indexContent, (error) => {
59 | if (error) {
60 | return console.error(`Error creating index.tsx: ${error}`);
61 | }
62 | console.log('app/index.tsx created.');
63 |
64 | const layoutPath = path.join(newAppDirPath, '_layout.tsx');
65 | fs.writeFile(layoutPath, layoutContent, (error) => {
66 | if (error) {
67 | return console.error(`Error creating _layout.tsx: ${error}`);
68 | }
69 | console.log('app/_layout.tsx created.');
70 | });
71 | });
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/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 | ]
17 | }
18 |
--------------------------------------------------------------------------------