├── .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 | }
--------------------------------------------------------------------------------