Up and Running
Understanding the project structure, layout, provider hierarchy, and data management.
Project Structure
The native app follows a standard Expo Router structure. Here is a breakdown of the key files and directories to help you navigate and contribute.
apps/native/app/_layout.tsx
This file is the root layout of the application. It initializes the app, manages the splash screen, and sets up the Zustand store providers for state management.
export default function RootLayout() {
return (
<PrivyProvider
appId={Config.PRIVY_APP_ID}
clientId={Config.PRIVY_CLIENT_ID}
>
<ThemeStoreProvider>
<BiometricStoreProvider>
<NotificationsStoreProvider>
<AuthStoreProvider>
<DataStoreProvider>
<NetworkStoreProvider>
<SafeAreaProvider>
<RootNavigator />
<Toast />
</SafeAreaProvider>
</NetworkStoreProvider>
</DataStoreProvider>
</AuthStoreProvider>
</NotificationsStoreProvider>
</BiometricStoreProvider>
</ThemeStoreProvider>
</PrivyProvider>
);
}
Store Provider Hierarchy
The Zustand store providers are nested in a specific order to ensure proper initialization and functionality:
PrivyProvider: Wraps the application to provide authentication and wallet functionalityThemeStoreProvider: Initializes the theme store with device color scheme detection and automatic theme switchingBiometricStoreProvider: Manages biometric authentication state and device capability detectionNotificationsStoreProvider: Handles push notification permissions and token registrationAuthStoreProvider: Manages authentication state, wallet management, and biometric authenticationDataStoreProvider: Handles database connections, encryption, and sync statusNetworkStoreProvider: Monitors network connectivity and internet reachabilitySafeAreaProvider: Handles safe area insets for notches and system UIRootNavigator: Contains the Expo Router stack with authentication guardsToast: Global toast notification system
graph TD
PrivyProvider --> ThemeStoreProvider
ThemeStoreProvider --> BiometricStoreProvider
BiometricStoreProvider --> NotificationsStoreProvider
NotificationsStoreProvider --> AuthStoreProvider
AuthStoreProvider --> DataStoreProvider
DataStoreProvider --> NetworkStoreProvider
NetworkStoreProvider --> SafeAreaProvider
SafeAreaProvider --> RootNavigator
State Management with Zustand
The app uses Zustand for lightweight, scalable state management across all features:
Key Benefits
- Type-Safe: Full TypeScript support with proper type inference
- Minimal Boilerplate: No reducers, actions, or dispatch needed
- Reactive: Automatic UI updates when state changes
- Persistent: Stores persist data across app restarts where needed
- Composable: Stores can subscribe to each other and share state
- DevTools Ready: Built-in support for debugging and time-travel
Store Architecture
Each store is organized by domain and provides:
- State: Current values and computed properties
- Actions: Functions to update state
- Selectors: Memoized computed values for performance
- Effects: Side effects and async operations
Available Stores
Theme Store (useThemeStore)
Manages color schemes, theme colors, and UI theming:
import { useThemeColors, useIsDark, useToggleColorScheme } from '@/stores';
function MyComponent() {
const colors = useThemeColors();
const isDark = useIsDark();
const toggleTheme = useToggleColorScheme();
return (
<View style={{ backgroundColor: colors.background }}>
<Text style={{ color: colors.foreground }}>
Theme: {isDark ? 'Dark' : 'Light'}
</Text>
<Button title="Toggle Theme" onPress={toggleTheme} />
</View>
);
}
Auth Store (useAuthStore)
Handles wallets, authentication, and biometric features:
import { useMovementWallets, useActiveWallet, useAuthReady } from '@/stores';
function WalletComponent() {
const wallets = useMovementWallets();
const activeWallet = useActiveWallet();
const isReady = useAuthReady();
if (!isReady) return <LoadingSpinner />;
return (
<View>
{wallets.map(wallet => (
<WalletCard key={wallet.address} wallet={wallet} />
))}
</View>
);
}
Data Store (useDataStore)
Manages database connections and synchronization:
import { useDatabaseReady, useSyncStatus } from '@/stores';
function DataComponent() {
const isReady = useDatabaseReady();
const syncStatus = useSyncStatus();
return (
<View>
<Text>Database: {isReady ? 'Ready' : 'Initializing'}</Text>
<Text>Sync: {syncStatus}</Text>
</View>
);
}
Network Store (useNetworkStore)
Tracks connectivity and network status:
import { useIsConnected, useIsOnline } from '@/stores';
function NetworkComponent() {
const isConnected = useIsConnected();
const isOnline = useIsOnline();
return (
<View>
<Text>Connected: {isConnected ? 'Yes' : 'No'}</Text>
<Text>Online: {isOnline ? 'Yes' : 'No'}</Text>
</View>
);
}
Biometric Store (useBiometricStore)
Manages biometric authentication (Touch ID, Face ID) and device capabilities:
import { useBiometricEnabled, useBiometricAvailable, useBiometricEnrolled } from '@/stores';
function BiometricSettings() {
const isEnabled = useBiometricEnabled();
const isAvailable = useBiometricAvailable();
const isEnrolled = useBiometricEnrolled();
return (
<View>
<Text>Biometrics Available: {isAvailable ? 'Yes' : 'No'}</Text>
<Text>Biometrics Enrolled: {isEnrolled ? 'Yes' : 'No'}</Text>
<Text>Biometrics Enabled: {isEnabled ? 'Yes' : 'No'}</Text>
</View>
);
}
Notifications Store (useNotificationsStore)
Handles push notification permissions and backend registration:
import { useNotificationsEnabled, useNotificationsGranted, usePushToken } from '@/stores';
function NotificationsSettings() {
const isEnabled = useNotificationsEnabled();
const isGranted = useNotificationsGranted();
const pushToken = usePushToken();
return (
<View>
<Text>Notifications Enabled: {isEnabled ? 'Yes' : 'No'}</Text>
<Text>Permissions Granted: {isGranted ? 'Yes' : 'No'}</Text>
<Text>Push Token: {pushToken ? 'Registered' : 'Not registered'}</Text>
</View>
);
}
Toast Store (useToastStore)
Manages global notifications and alerts:
import { useShowToast } from '@/stores';
function NotificationButton() {
const showToast = useShowToast();
const handlePress = () => {
showToast({
message: 'Operation completed!',
type: 'success'
});
};
return <Button title="Show Success" onPress={handlePress} />;
}
Data Management with TanStack Query
The app uses TanStack Query (React Query) for efficient data fetching and state management:
Key Benefits
- Smart Caching: Blockchain data is cached to reduce redundant network requests
- Background Refetching: Data automatically refreshes in the background to stay up-to-date
- Optimistic Updates: UI updates immediately while changes sync with the blockchain
- Persistent Cache: Query cache is persisted to AsyncStorage for offline access
- Automatic Retries: Failed requests are automatically retried with exponential backoff
Configuration
The QueryProvider in apps/native/src/providers/query-provider.tsx configures:
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 24 hours
retry: 2,
},
},
});
This ensures blockchain data is cached for 5 minutes before being considered stale, and persisted for 24 hours.
Theming System
The app uses a centralized theming system that provides consistent styling and automatic light/dark mode support.
Basic Usage with useThemeColors()
The simplest way to access theme colors in your components:
import { View, Text, StyleSheet } from 'react-native';
import { useThemeColors } from '@/stores';
export default function MyComponent() {
const colors = useThemeColors();
const styles = StyleSheet.create({
container: {
backgroundColor: colors.background,
borderColor: colors.border,
borderWidth: 1,
},
text: {
color: colors.foreground,
},
});
return (
<View style={styles.container}>
<Text style={styles.text}>Hello World</Text>
</View>
);
}
Advanced Usage with Theme Selectors
Access full theme state including theme switching:
import { View, Text, Button, StyleSheet } from 'react-native';
import { useThemeColors, useIsDark, useToggleColorScheme } from '@/stores';
export default function SettingsScreen() {
const colors = useThemeColors();
const isDark = useIsDark();
const toggleTheme = useToggleColorScheme();
const styles = StyleSheet.create({
container: {
backgroundColor: colors.background,
},
text: {
color: colors.foreground,
},
});
return (
<View style={styles.container}>
<Text style={styles.text}>
Current theme: {isDark ? 'Dark' : 'Light'}
</Text>
<Button title="Toggle Theme" onPress={toggleTheme} />
</View>
);
}
Available Colors
The theme provides semantic color tokens:
- Base:
background,foreground - Components:
card,cardForeground,popover,popoverForeground - Actions:
primary,primaryForeground,secondary,secondaryForeground - States:
muted,mutedForeground,accent,accentForeground,destructive,destructiveForeground - UI Elements:
border,input,ring - Charts:
chart1throughchart5
Design Tokens
Import spacing, radius, fonts, and font sizes for consistent styling:
import { useThemeColors } from '@/stores';
import { Spacing, Radius, Fonts, FontSizes } from '@/constants/Colors';
const colors = useThemeColors();
const styles = StyleSheet.create({
card: {
backgroundColor: colors.card,
borderRadius: Radius['2xl'], // 8
padding: Spacing.md, // 16
margin: Spacing.sm, // 8
},
title: {
color: colors.cardForeground,
fontFamily: Fonts.semibold, // 'Inter_600SemiBold'
fontSize: FontSizes.xl, // 20
marginBottom: Spacing.sm,
},
});
Available Design Tokens:
- Spacing:
xs(4),sm(8),md(16),lg(24),xl(32),2xl(48),3xl(64) - Radius:
sm(0),md(0),lg(0),xl(4),2xl(8),3xl(12),4xl(16) - Fonts:
regular,medium,semibold - FontSizes:
xs(12),sm(14),md(16),lg(18),xl(20),2xl(24),3xl(30),4xl(36)
Best Practices
- Always use
useColors()oruseTheme()- Never hardcode colors - Create StyleSheet inside component - Styles need to update when theme changes
- Use semantic color names - Use
colors.primaryinstead of specific color values - Leverage design tokens - Use
Spacing,Radius,Fontsfor consistency - Test both themes - Always verify your UI works in light and dark mode
Working with Assets
Creating New Assets
- Add Images: Place your image files (PNG, JPG, SVG) into
apps/native/assets/images/. - Usage: Import them directly in your components or reference them via
require.
import { Image } from 'expo-image';
<Image source={require('@/assets/images/my-icon.png')} style={{ width: 50, height: 50 }} />
For icons, we typically use the assets/images directory or an icon library compatible with React Native.
Blockchain Interaction Hooks
The app provides several hooks for interacting with Move contracts:
useGig() - Standard Gig Management
Located in apps/native/src/hooks/use-gig.ts, this hook provides functions for creating, applying to, and managing standard gigs.
Key Functions:
createGig(description, budget, contractIpfsHash)- Create a new gigapplyBySigning(gigAddress)- Apply to a gig by signingassignApplicant(gigAddress, applicantAddress)- Assign a workercompleteGig(gigAddress)- Complete and release paymentgetAllGigs()- Fetch all gigs from the contractgetUserRating(userAddress)- Get a user's average rating
Example:
const { createGig, getAllGigs } = useGig();
// Create a new gig
await createGig("Build a website", 1000, "ipfs://...");
// Fetch all gigs
const gigs = await getAllGigs();
useLocationBasedGig() - Location-Based Gigs
Located in apps/native/src/hooks/use-location-based-gig.ts, this hook manages location-based gig opportunities.
Key Functions:
createLocationGig(description, totalAmount, maxOpportunities, lat, long)- Create a location gigclaimCompletion(gigAddress, evidenceHash)- Claim completion of a location taskgetAllLocationGigs()- Fetch all location-based gigs
Example:
const { createLocationGig, getAllLocationGigs } = useLocationBasedGig();
// Create a location-based gig
await createLocationGig(
"Deliver package",
500,
10,
-33.8688,
151.2093
);
// Fetch all location gigs
const locationGigs = await getAllLocationGigs();
These hooks abstract the complexity of blockchain interactions and provide a clean API for the UI layer.
