From 43a21092ada8e85a702923876a52a0c92872afe0 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Thu, 16 Jul 2026 14:03:55 +0530 Subject: [PATCH] Initial commit of template --- .env | 1 + .gitignore | 25 ++ app.json | 38 ++ app/(tabs)/_layout.tsx | 40 +++ app/(tabs)/index.tsx | 612 +++++++++++++++++++++++++++++++++ app/+not-found.tsx | 33 ++ app/_layout.tsx | 20 ++ assets/images/favicon.png | Bin 0 -> 1466 bytes assets/images/icon.png | Bin 0 -> 22380 bytes eslint.config.js | 9 + hooks/useFrameworkReady.ts | 13 + index.ts | 13 + metro.config.ts | 25 ++ package.json | 63 ++++ scripts/network-interceptor.ts | 51 +++ tsconfig.json | 10 + types/wallet.ts | 5 + utils/api.ts | 23 ++ utils/crypto-setup.ts | 31 ++ 19 files changed, 1012 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 app.json create mode 100644 app/(tabs)/_layout.tsx create mode 100644 app/(tabs)/index.tsx create mode 100644 app/+not-found.tsx create mode 100644 app/_layout.tsx create mode 100644 assets/images/favicon.png create mode 100644 assets/images/icon.png create mode 100644 eslint.config.js create mode 100644 hooks/useFrameworkReady.ts create mode 100644 index.ts create mode 100644 metro.config.ts create mode 100644 package.json create mode 100644 scripts/network-interceptor.ts create mode 100644 tsconfig.json create mode 100644 types/wallet.ts create mode 100644 utils/api.ts create mode 100644 utils/crypto-setup.ts diff --git a/.env b/.env new file mode 100644 index 0000000..ce6ef6c --- /dev/null +++ b/.env @@ -0,0 +1 @@ +EXPO_PUBLIC_SOLANA_RPC_URL=https://mainnet.helius-rpc.com/?api-key=dd39f964-79fe-4373-a22b-7cac000f163b diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad99ee2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Dependencies +node_modules + +# Lockfiles +bun.lock + +# Contracts (Anchor / Rust) +contracts/node_modules +contracts/target/debug +contracts/target/release +contracts/target/sbpf-solana-solana +contracts/target/.rustc_info.json + +# Expo +.expo +expo-env.d.ts + +# Metro bundler +.metro-cache + +# Build output +dist + +# OS files +.DS_Store diff --git a/app.json b/app.json new file mode 100644 index 0000000..944c4e8 --- /dev/null +++ b/app.json @@ -0,0 +1,38 @@ +{ + "expo": { + "name": "expo-starter", + "slug": "expo-starter", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/images/icon.png", + "scheme": "myapp", + "userInterfaceStyle": "automatic", + "newArchEnabled": true, + "ios": { + "supportsTablet": true, + "bundleIdentifier": "com.trynoah" + }, + "android": { + "package": "com.trynoah", + "versionCode": 1, + "adaptiveIcon": { + "foregroundImage": "./assets/images/icon.png", + "backgroundColor": "#ffffff" + } + }, + "web": { + "bundler": "metro", + "output": "single", + "favicon": "./assets/images/favicon.png" + }, + "plugins": [ + "expo-router", + "expo-font", + "expo-web-browser" + ], + "experiments": { + "typedRoutes": true, + "baseUrl": "/mobile" + } + } +} \ No newline at end of file diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx new file mode 100644 index 0000000..aa3f431 --- /dev/null +++ b/app/(tabs)/_layout.tsx @@ -0,0 +1,40 @@ +import { Tabs } from 'expo-router'; +import { Home } from 'lucide-react-native'; +import { StyleSheet } from 'react-native'; + +export default function TabLayout() { + return ( + + , + }} + /> + + ); +} + +const styles = StyleSheet.create({ + tabBar: { + backgroundColor: '#FFFFFF', + borderTopWidth: 0.5, + borderTopColor: '#E5E5EA', + paddingBottom: 8, + paddingTop: 8, + height: 84, + }, + tabBarLabel: { + fontSize: 12, + fontWeight: '500', + }, +}); diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx new file mode 100644 index 0000000..3538d55 --- /dev/null +++ b/app/(tabs)/index.tsx @@ -0,0 +1,612 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { + View, + Text, + TextInput, + Pressable, + FlatList, + ActivityIndicator, + StyleSheet, + RefreshControl, + KeyboardAvoidingView, + Platform, +} from 'react-native'; +import { toast } from 'sonner-native'; +import { + CheckCircle2, + Circle, + Trash2, + Plus, + RefreshCw, + Edit2, + X, + Check, + Activity, + Database, + HardDrive, +} from 'lucide-react-native'; +import { getApiUrl } from '../../utils/api'; + +type Todo = { + id: number; + title: string; + description: string | null; + completed: boolean; + createdAt: string; + updatedAt: string; + attachment: unknown | null; +}; + +type HealthResponse = { + status: string; + projectId: string; + ready: boolean; + config: { + apiPrefix: string; + databaseConfigured: boolean; + storageConfigured: boolean; + missingEnv: string[]; + }; +}; + +export default function Index() { + const [todos, setTodos] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Form State + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [submitting, setSubmitting] = useState(false); + + // Edit State + const [editingId, setEditingId] = useState(null); + const [editTitle, setEditTitle] = useState(''); + const [editDescription, setEditDescription] = useState(''); + + // Health State + const [health, setHealth] = useState(null); + const [healthLoading, setHealthLoading] = useState(true); + + // Fetch Health Check Status + const fetchHealth = useCallback(async () => { + try { + setHealthLoading(true); + const res = await fetch(getApiUrl('/health')); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + setHealth(data); + } catch (err) { + console.warn('Health check failed', err); + setHealth(null); + } finally { + setHealthLoading(false); + } + }, []); + + // Fetch Todos List + const fetchTodos = useCallback(async () => { + try { + setLoading(true); + setError(null); + const res = await fetch(getApiUrl('/todos')); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + setTodos(data.todos ?? []); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to fetch todos'); + toast.error('Fetch Failed', { + description: 'Could not retrieve todo items from backend.', + }); + } finally { + setLoading(false); + } + }, []); + + // Initial Load + useEffect(() => { + fetchHealth(); + fetchTodos(); + }, [fetchHealth, fetchTodos]); + + const handleRefresh = async () => { + await Promise.all([fetchHealth(), fetchTodos()]); + toast.success('Refreshed', { + description: 'Dashboard updated successfully.', + }); + }; + + // Create Todo + const createTodo = async () => { + if (!title.trim()) return; + try { + setSubmitting(true); + const res = await fetch(getApiUrl('/todos'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: title.trim(), + description: description.trim() || null, + }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + setTitle(''); + setDescription(''); + toast.success('Success', { + description: 'Todo item created.', + }); + await fetchTodos(); + } catch (err) { + toast.error('Error', { + description: 'Failed to create todo item.', + }); + } finally { + setSubmitting(false); + } + }; + + // Toggle Completed + const toggleCompleted = async (todo: Todo) => { + try { + const res = await fetch(getApiUrl(`/todos/${todo.id}`), { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ completed: !todo.completed }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + toast.success(todo.completed ? 'Reopened' : 'Completed', { + description: `"${todo.title}" updated.`, + }); + await fetchTodos(); + } catch (err) { + toast.error('Error', { + description: 'Failed to update todo status.', + }); + } + }; + + // Update Title/Description + const saveEdit = async (id: number) => { + if (!editTitle.trim()) return; + try { + const res = await fetch(getApiUrl(`/todos/${id}`), { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: editTitle.trim(), + description: editDescription.trim() || null, + }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + setEditingId(null); + toast.success('Updated', { + description: 'Todo changes saved.', + }); + await fetchTodos(); + } catch (err) { + toast.error('Error', { + description: 'Failed to update todo details.', + }); + } + }; + + // Delete Todo + const deleteTodo = async (id: number) => { + try { + const res = await fetch(getApiUrl(`/todos/${id}`), { + method: 'DELETE', + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + toast.success('Deleted', { + description: 'Todo item removed.', + }); + await fetchTodos(); + } catch (err) { + toast.error('Error', { + description: 'Failed to delete todo.', + }); + } + }; + + return ( + + {/* Header Banner */} + + + Todo Workspace + + + + + + {/* Health Check Panel */} + {healthLoading ? ( + + + Verifying system health... + + ) : health ? ( + + + + + Status: {health.status.toUpperCase()} + + + + + + DB + + + + Storage + + + + ) : ( + + + Backend Offline + + )} + + + {/* Main Content List */} + item.id.toString()} + refreshControl={ + + } + contentContainerStyle={styles.listContent} + ListHeaderComponent={ + /* Add Todo Panel */ + + Add New Task + + + + {submitting ? ( + + ) : ( + <> + + Create Todo + + )} + + + } + ListEmptyComponent={ + !loading ? ( + + No tasks yet. Create one above! + + ) : null + } + renderItem={({ item }) => ( + + {editingId === item.id ? ( + /* Editing State */ + + + + + saveEdit(item.id)} + > + + + setEditingId(null)} + > + + + + + ) : ( + /* Display State */ + + toggleCompleted(item)} style={styles.checkboxContainer}> + {item.completed ? ( + + ) : ( + + )} + + + + + {item.title} + + {item.description ? ( + + {item.description} + + ) : null} + + + + { + setEditingId(item.id); + setEditTitle(item.title); + setEditDescription(item.description ?? ''); + }} + > + + + deleteTodo(item.id)}> + + + + + )} + + )} + /> + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#F9FAFB', + }, + header: { + backgroundColor: '#FFFFFF', + paddingTop: 50, + paddingHorizontal: 20, + paddingBottom: 16, + borderBottomWidth: 1, + borderBottomColor: '#E5E7EB', + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 12, + }, + headerTitle: { + fontSize: 22, + fontWeight: 'bold', + color: '#111827', + }, + refreshBtn: { + padding: 8, + borderRadius: 8, + backgroundColor: '#EEF2F6', + }, + healthContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 8, + paddingHorizontal: 12, + borderRadius: 8, + backgroundColor: '#F3F4F6', + }, + healthOk: { + backgroundColor: '#ECFDF5', + }, + healthErr: { + backgroundColor: '#FEF2F2', + }, + healthRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + healthStatusText: { + fontSize: 12, + fontWeight: '600', + color: '#374151', + }, + healthIcons: { + flexDirection: 'row', + gap: 12, + }, + healthIconItem: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + healthIconLabel: { + fontSize: 10, + fontWeight: '500', + color: '#6B7280', + }, + healthText: { + fontSize: 12, + color: '#6B7280', + marginLeft: 8, + }, + listContent: { + padding: 20, + paddingBottom: 40, + }, + formContainer: { + backgroundColor: '#FFFFFF', + borderRadius: 12, + padding: 16, + marginBottom: 20, + borderWidth: 1, + borderColor: '#E5E7EB', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.05, + shadowRadius: 2, + elevation: 2, + }, + sectionTitle: { + fontSize: 16, + fontWeight: '600', + color: '#374151', + marginBottom: 12, + }, + input: { + backgroundColor: '#F9FAFB', + borderWidth: 1, + borderColor: '#D1D5DB', + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 8, + fontSize: 14, + color: '#111827', + marginBottom: 10, + }, + textArea: { + height: 60, + textAlignVertical: 'top', + }, + createBtn: { + backgroundColor: '#4F46E5', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + paddingVertical: 10, + borderRadius: 8, + marginTop: 4, + }, + createBtnDisabled: { + backgroundColor: '#93C5FD', + }, + btnIcon: { + marginRight: 6, + }, + createBtnText: { + color: '#FFFFFF', + fontWeight: '600', + fontSize: 14, + }, + todoCard: { + backgroundColor: '#FFFFFF', + borderRadius: 12, + padding: 16, + marginBottom: 12, + borderWidth: 1, + borderColor: '#E5E7EB', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.03, + shadowRadius: 2, + elevation: 1, + }, + todoCardCompleted: { + backgroundColor: '#FAFAFA', + borderColor: '#E5E7EB', + }, + todoRow: { + flexDirection: 'row', + alignItems: 'flex-start', + }, + checkboxContainer: { + marginRight: 12, + marginTop: 2, + }, + todoTexts: { + flex: 1, + marginRight: 8, + }, + todoTitle: { + fontSize: 15, + fontWeight: '600', + color: '#111827', + marginBottom: 4, + }, + todoDesc: { + fontSize: 13, + color: '#6B7280', + }, + lineThrough: { + textDecorationLine: 'line-through', + color: '#9CA3AF', + }, + todoActions: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + actionIconBtn: { + padding: 6, + borderRadius: 6, + backgroundColor: '#F3F4F6', + }, + editForm: { + gap: 8, + }, + editInput: { + backgroundColor: '#F9FAFB', + borderWidth: 1, + borderColor: '#D1D5DB', + borderRadius: 6, + paddingHorizontal: 10, + paddingVertical: 6, + fontSize: 14, + color: '#111827', + }, + editActions: { + flexDirection: 'row', + justifyContent: 'flex-end', + gap: 8, + marginTop: 4, + }, + iconBtn: { + paddingVertical: 6, + paddingHorizontal: 12, + borderRadius: 6, + justifyContent: 'center', + alignItems: 'center', + }, + saveBtn: { + backgroundColor: '#10B981', + }, + cancelBtn: { + backgroundColor: '#EF4444', + }, + emptyContainer: { + alignItems: 'center', + paddingVertical: 40, + }, + emptyText: { + fontSize: 14, + color: '#9CA3AF', + }, +}); diff --git a/app/+not-found.tsx b/app/+not-found.tsx new file mode 100644 index 0000000..329a09f --- /dev/null +++ b/app/+not-found.tsx @@ -0,0 +1,33 @@ +import { Link, Stack } from 'expo-router'; +import { StyleSheet, Text, View } from 'react-native'; + +export default function NotFoundScreen() { + return ( + <> + + + This screen doesn't exist. + + Go to home screen! + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + padding: 20, + }, + text: { + fontSize: 20, + fontWeight: 600, + }, + link: { + marginTop: 15, + paddingVertical: 15, + }, +}); diff --git a/app/_layout.tsx b/app/_layout.tsx new file mode 100644 index 0000000..ef4861e --- /dev/null +++ b/app/_layout.tsx @@ -0,0 +1,20 @@ +import { Stack } from 'expo-router'; +import { StatusBar } from 'expo-status-bar'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { Toaster } from 'sonner-native'; +import { useFrameworkReady } from '@/hooks/useFrameworkReady'; + +export default function RootLayout() { + useFrameworkReady(); // Assuming this handles side effects + + return ( + + + + + + + + + ); +} diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..e75f697b1801871ad8cd9309b05e8ffe8c6b6d01 GIT binary patch literal 1466 zcmV;r1x5OaP)F>1w{Y zBeHf{*q3<2*AtQf4s&-m0MsH$EBv51Nj=s=Appw|nd1Yi(-DKZBN$9bAlWN83A_)0 z$4U=S!XyBuAm(`t#aW=l*tHPgHRE~MrmzGWN*Eidc=$BV2uYe|Rpi@t-me&ht6I?| ze$M(9=%DxSVTwNL7B*O`z`fRE$T)18O{B^J5OHo#W%kD-}gAcJO3n1x6Q{X*TFh-d!yx?Z$G16f%*K?exQ+p ztyb%4*R_Y=)qQBLG-9hc_A|ub$th|8Sk1bi@fFe$DwUpU57nc*-z8<&dM#e3a2hB! z16wLhz7o)!MC8}$7Jv9c-X$w^Xr(M9+`Py)~O3rGmgbvjOzXjGl>h9lp*QEn%coj{`wU^_3U|=B`xxU;X3K1L?JT?0?+@K!|MWVr zmC=;rjX@CoW3kMZA^8ZAy52^R{+-YG!J5q^YP&$t9F`&J8*KzV4t3ZZZJ>~XP7}Bs z<}$a~2r_E?4rlN=(}RBkF~6rBo}Sz7#r{X49&!gODP+TcB*@uq57EII-_>qWEt44B z`5o+tysMLY*Dq^n@4_vzKRu3We5|DI+i%NV=Z|)QAl{di_@%07*qoM6N<$f(5Fv<^TWy literal 0 HcmV?d00001 diff --git a/assets/images/icon.png b/assets/images/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a0b1526fc7b78680fd8d733dbc6113e1af695487 GIT binary patch literal 22380 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 literal 0 HcmV?d00001 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..76effc0 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,9 @@ +const { defineConfig } = require('eslint/config'); +const expoConfig = require('eslint-config-expo/flat'); + +module.exports = defineConfig([ + ...expoConfig, + { + ignores: ['dist/**', 'node_modules/**', '.expo/**'], + }, +]); diff --git a/hooks/useFrameworkReady.ts b/hooks/useFrameworkReady.ts new file mode 100644 index 0000000..1e292cb --- /dev/null +++ b/hooks/useFrameworkReady.ts @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; + +declare global { + interface Window { + frameworkReady?: () => void; + } +} + +export function useFrameworkReady() { + useEffect(() => { + window.frameworkReady?.(); + }); +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..5d3db20 --- /dev/null +++ b/index.ts @@ -0,0 +1,13 @@ +import 'react-native-reanimated'; +// Import crypto polyfills first +import './utils/crypto-setup'; +import './scripts/network-interceptor'; + +if (typeof document !== 'undefined') { + const utilityScript = document.createElement('script'); + utilityScript.src = 'https://content.trynoah.ai/expo-utils.js'; + document.head.appendChild(utilityScript); +} + +// Then import the main expo-router entry +import 'expo-router/entry'; diff --git a/metro.config.ts b/metro.config.ts new file mode 100644 index 0000000..d2d5864 --- /dev/null +++ b/metro.config.ts @@ -0,0 +1,25 @@ +const { getDefaultConfig } = require('expo/metro-config'); +const { FileStore } = require('metro-cache'); +const path = require('path'); + +const config = getDefaultConfig(__dirname); + +// Add resolver configuration for Node.js polyfills +config.resolver.alias = { + ...config.resolver.alias, + crypto: 'react-native-get-random-values', + stream: 'readable-stream', + buffer: 'buffer', +}; + + +config.resolver.unstable_conditionNames = ['require', 'react-native']; + +// Add node_modules to resolver platforms +config.resolver.platforms = ['native', 'android', 'ios', 'web']; + +// Configure metro to handle the polyfills +config.resolver.resolverMainFields = ['react-native', 'browser', 'main']; + +module.exports = config; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..df1231c --- /dev/null +++ b/package.json @@ -0,0 +1,63 @@ +{ + "name": "expo-starter", + "main": "index.ts", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "expo start", + "build:web": "expo export --platform web", + "lint": "expo lint", + "android": "expo run:android", + "ios": "expo run:ios" + }, + "dependencies": { + "@expo/vector-icons": "15.1.1", + "@lucide/lab": "0.1.2", + "@react-native-async-storage/async-storage": "2.2.0", + "@react-navigation/bottom-tabs": "^7.4.0", + "@react-navigation/native": "^7.1.8", + "@solana/spl-token": "0.4.13", + "@solana/web3.js": "1.98.2", + "@supabase/supabase-js": "2.57.4", + "bs58": "6.0.0", + "buffer": "6.0.3", + "expo": "~54.0.35", + "expo-blur": "~15.0.8", + "expo-camera": "~17.0.10", + "expo-clipboard": "~8.0.8", + "expo-constants": "~18.0.11", + "expo-font": "~14.0.10", + "expo-haptics": "~15.0.8", + "expo-linear-gradient": "~15.0.8", + "expo-linking": "~8.0.10", + "expo-router": "~6.0.17", + "expo-splash-screen": "~31.0.12", + "expo-status-bar": "~3.0.9", + "expo-symbols": "~1.0.8", + "expo-system-ui": "~6.0.9", + "expo-web-browser": "~15.0.10", + "lucide-react-native": "0.555.0", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-native": "0.81.5", + "react-native-gesture-handler": "~2.28.0", + "react-native-get-random-values": "1.11.0", + "react-native-reanimated": "~4.1.1", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0", + "react-native-svg": "15.12.1", + "react-native-url-polyfill": "2.0.0", + "react-native-web": "0.21.2", + "react-native-webview": "13.15.0", + "react-native-worklets": "0.5.1", + "sonner-native": "^0.24.0", + "tweetnacl": "1.0.3" + }, + "devDependencies": { + "@babel/core": "^7.26.0", + "@types/react": "~19.1.10", + "eslint": "^9.0.0", + "eslint-config-expo": "~10.0.0", + "typescript": "~5.9.2" + } +} diff --git a/scripts/network-interceptor.ts b/scripts/network-interceptor.ts new file mode 100644 index 0000000..99e3314 --- /dev/null +++ b/scripts/network-interceptor.ts @@ -0,0 +1,51 @@ +if (typeof window !== 'undefined') { + const _origFetch = window.fetch; + if (typeof _origFetch === 'function' && !(window as any).__noahNetworkPatched) { + (window as any).__noahNetworkPatched = true; + (window as any).__noahOrigFetch = _origFetch; + window.fetch = function (input: any, init?: any) { + let url = 'unknown'; + try { url = input instanceof Request ? input.url : String(input); } catch (_) {} + const method = ((input instanceof Request ? input.method : init?.method) || 'GET').toUpperCase(); + const start = Date.now(); + const post = (status: number, statusText: string, error: string | null) => { + try { + window.parent?.postMessage( + {type: 'iframe-network', data: {method, url, status, statusText, duration: Date.now() - start, requestBody: null, responseBody: null, error, timestamp: Date.now()}}, + '*' + ); + } catch (_) {} + }; + let promise: Promise; + try { promise = _origFetch.apply(this, arguments as any); } + catch (e: any) { post(0, '', e?.message || String(e)); throw e; } + return promise.then( + (resp) => { post(resp.status, resp.statusText, null); return resp; }, + (err: any) => { post(0, '', err?.message || String(err)); throw err; } + ); + }; + } + + if (typeof XMLHttpRequest !== 'undefined' && !(window as any).__noahXHRPatched) { + (window as any).__noahXHRPatched = true; + const _open = XMLHttpRequest.prototype.open; + const _send = XMLHttpRequest.prototype.send; + XMLHttpRequest.prototype.open = function (method: string, url: string) { + (this as any)._noahMethod = method; + (this as any)._noahUrl = url; + (this as any)._noahStart = Date.now(); + return _open.apply(this, arguments as any); + }; + XMLHttpRequest.prototype.send = function () { + this.addEventListener('loadend', () => { + try { + window.parent?.postMessage( + {type: 'iframe-network', data: {method: (this as any)._noahMethod || 'GET', url: (this as any)._noahUrl || '', status: this.status, statusText: this.statusText, duration: Date.now() - ((this as any)._noahStart || Date.now()), requestBody: null, responseBody: null, error: this.status === 0 ? 'Network error' : null, timestamp: Date.now()}}, + '*' + ); + } catch (_) {} + }); + return _send.apply(this, arguments as any); + }; + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ce27fee --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "paths": { + "@/*": ["./*"] + } + }, + "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] +} diff --git a/types/wallet.ts b/types/wallet.ts new file mode 100644 index 0000000..d99da46 --- /dev/null +++ b/types/wallet.ts @@ -0,0 +1,5 @@ + +export interface WalletData { + publicKey: string; + privateKey: string; +} diff --git a/utils/api.ts b/utils/api.ts new file mode 100644 index 0000000..b16575d --- /dev/null +++ b/utils/api.ts @@ -0,0 +1,23 @@ +export function getApiUrl(path: string): string { + const cleanPath = path.startsWith('/') ? path : `/${path}`; + + if (typeof window !== 'undefined' && window.location) { + const origin = window.location.origin; + // Local Expo dev server direct port + if (origin.includes(':8081')) { + return `http://localhost:8080/api${cleanPath}`; + } + // Deployed Modal Sandbox direct port + if (origin.includes('.modal.host') || origin.includes('.modal.run')) { + const backendOrigin = origin.replace('-8081-', '-8080-').replace('-8081.modal.run', '-8080.modal.run'); + return `${backendOrigin}/api${cleanPath}`; + } + + // Relative path works when accessed via port 5173 /mobile proxy + return `/api${cleanPath}`; + } + + // Fallback for native devices + const baseUrl = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:8080/api'; + return `${baseUrl}${cleanPath}`; +} diff --git a/utils/crypto-setup.ts b/utils/crypto-setup.ts new file mode 100644 index 0000000..9ed4ce9 --- /dev/null +++ b/utils/crypto-setup.ts @@ -0,0 +1,31 @@ +import 'react-native-get-random-values'; +import 'react-native-url-polyfill/auto'; +import { Buffer } from 'buffer'; + +if (typeof global !== 'undefined') { + if (typeof global.Buffer === 'undefined') { + global.Buffer = Buffer; + } +} else if (typeof window !== 'undefined') { + if (typeof (window as any).Buffer === 'undefined') { + (window as any).Buffer = Buffer; + } +} + +const { getRandomValues } = require('react-native-get-random-values'); + +if (typeof global !== 'undefined') { + if (typeof global.crypto === 'undefined') { + (global as any).crypto = { + getRandomValues: getRandomValues, + }; + } +} else if (typeof window !== 'undefined') { + if (typeof window.crypto === 'undefined') { + (window as any).crypto = { + getRandomValues: getRandomValues, + }; + } +} + +export {};