Skip to content

Mobile Client

The Bifrost Mobile Client is a cross-platform application built with React Native and Expo, providing VPN management on iOS and Android devices.

  • Home Screen: VPN connection status and quick toggle
  • Servers Screen: Server selection with latency indicators
  • Stats Screen: Real-time traffic statistics and connection details
  • Settings Screen: Configuration management and preferences
  • Node.js 18+
  • npm or yarn
  • Expo CLI
  • For iOS: Xcode 14+ (macOS only)
  • For Android: Android Studio with SDK
Terminal window
# Clone repository
git clone https://github.com/rennerdo30/bifrost-proxy.git
cd bifrost-proxy/mobile
# Install dependencies
npm install
# Start development server
npx expo start
Terminal window
# iOS Simulator (macOS only)
npx expo run:ios
# Android Emulator
npx expo run:android
# On physical device
npx expo start --tunnel
# Then scan QR code with Expo Go app

Using EAS Build (recommended):

Terminal window
# Install EAS CLI
npm install -g eas-cli
# Login to Expo account
eas login
# Build for iOS
eas build --platform ios
# Build for Android
eas build --platform android
# Build for both
eas build --platform all

The main dashboard showing connection status:

┌─────────────────────────────────────┐
│ Bifrost VPN │
├─────────────────────────────────────┤
│ │
│ ┌───────────────┐ │
│ │ │ │
│ │ 🔒 │ │
│ │ Connected │ │
│ │ │ │
│ └───────────────┘ │
│ │
│ [ Disconnect ] │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ ↑ Sent │ │ ↓ Received │ │
│ │ 1.2 GB │ │ 3.4 GB │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ Server: us-west.example.com │
│ Connected: 2h 34m │
│ │
├─────────────────────────────────────┤
│ 🏠 Home │ 📡 Servers │ 📊 Stats │ ⚙️ │
└─────────────────────────────────────┘

Server selection with status indicators:

┌─────────────────────────────────────┐
│ Available Servers │
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────┐│
│ │ 🟢 US West ✓ 45ms ││
│ │ us-west.example.com ││
│ │ HTTP ││
│ └─────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────┐│
│ │ 🟢 Europe 120ms ││
│ │ eu.example.com ││
│ │ HTTP ││
│ └─────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────┐│
│ │ 🟡 Asia Pacific 200ms ││
│ │ asia.example.com ││
│ │ SOCKS5 ││
│ └─────────────────────────────────┘│
│ │
│ ┌─────────────────────────────────┐│
│ │ 🔴 Development offline ││
│ │ dev.example.com ││
│ │ HTTP ││
│ └─────────────────────────────────┘│
│ │
├─────────────────────────────────────┤
│ 🏠 Home │ 📡 Servers │ 📊 Stats │ ⚙️ │
└─────────────────────────────────────┘

Status indicators:

  • 🟢 Online - Server available
  • 🟡 Busy - High load
  • 🔴 Offline - Server unavailable

Detailed connection statistics:

┌─────────────────────────────────────┐
│ Connection Stats │
├─────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ ↑ Sent │ │ ↓ Received │ │
│ │ 1.2 GB │ │ 3.4 GB │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ Current Session │
│ ───────────────────────────────── │
│ Duration 2h 34m │
│ Total Data 4.6 GB │
│ Status Connected │
│ │
│ Connection Details │
│ ───────────────────────────────── │
│ Protocol WireGuard │
│ Encryption ChaCha20-Poly1305 │
│ Port 51820 │
│ MTU 1420 │
│ │
│ Network │
│ ───────────────────────────────── │
│ Local IP 10.0.0.2 │
│ Gateway 10.0.0.1 │
│ DNS 1.1.1.1, 8.8.8.8 │
│ Interface bifrost0 │
│ │
├─────────────────────────────────────┤
│ 🏠 Home │ 📡 Servers │ 📊 Stats │ ⚙️ │
└─────────────────────────────────────┘

Configuration and preferences:

┌─────────────────────────────────────┐
│ Settings │
├─────────────────────────────────────┤
│ │
│ Connection │
│ ───────────────────────────────── │
│ Auto-connect [ 🔘 ] │
│ Connect on app launch │
│ │
│ Kill Switch [🔘 ] │
│ Block traffic if VPN disconnects │
│ │
│ Split Tunneling [ 🔘 ] │
│ Exclude certain apps │
│ [Configure →] │
│ │
│ Server │
│ ───────────────────────────────── │
│ ┌─────────────────────────────────┐│
│ │ proxy.example.com:7080 [Save] ││
│ └─────────────────────────────────┘│
│ │
│ Notifications │
│ ───────────────────────────────── │
│ Connection Alerts [🔘 ] │
│ Notify on connect/disconnect │
│ │
│ Data & Privacy │
│ ───────────────────────────────── │
│ [ Clear Cached Data ] │
│ │
│ About │
│ ───────────────────────────────── │
│ Version 1.0.0 │
│ Server Status Connected │
│ │
├─────────────────────────────────────┤
│ 🏠 Home │ 📡 Servers │ 📊 Stats │ ⚙️ │
└─────────────────────────────────────┘

The mobile client connects to a Bifrost client running on your network. Configure the server address in Settings.

  1. Start the Bifrost client on your computer/server
  2. Note the API address (default: http://192.168.x.x:7383)
  3. Enter this address in the mobile app Settings

The mobile app uses these API endpoints:

EndpointDescription
GET /api/v1/statusClient status
GET /api/v1/vpn/statusVPN connection status
POST /api/v1/vpn/enableEnable VPN
POST /api/v1/vpn/disableDisable VPN
GET /api/v1/serversAvailable servers
POST /api/v1/servers/{id}/selectSelect server
GET /api/v1/configGet configuration
PUT /api/v1/configUpdate configuration
mobile/
├── src/
│ ├── components/ # Reusable UI components
│ ├── screens/ # Screen components
│ │ ├── HomeScreen.tsx
│ │ ├── ServersScreen.tsx
│ │ ├── StatsScreen.tsx
│ │ └── SettingsScreen.tsx
│ ├── services/ # API and utilities
│ │ └── api.ts
│ └── App.tsx # Main app component
├── app.json # Expo configuration
├── package.json
└── tsconfig.json
  • React Native: Cross-platform UI framework
  • Expo: Development and build tooling
  • React Query: Data fetching and caching
  • React Navigation: Screen navigation
  1. Create screen component in src/screens/
  2. Add to navigation in App.tsx
  3. Add tab bar icon if needed

The app uses React Query for data fetching:

import { useQuery, useMutation } from '@tanstack/react-query'
import { api } from '../services/api'
function MyScreen() {
const { data, isLoading, error } = useQuery({
queryKey: ['vpn-status'],
queryFn: api.getVPNStatus,
refetchInterval: 3000,
})
const mutation = useMutation({
mutationFn: api.enableVPN,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['vpn-status'] })
},
})
// ...
}

The Bifrost Mobile Client is designed with accessibility in mind, supporting both VoiceOver (iOS) and TalkBack (Android) screen readers.

FeatureSupport
Screen ReaderFull support
Accessibility LabelsAll interactive elements
Accessibility RolesSemantic roles for buttons, tabs, switches
Accessibility StatesDisabled, selected, checked states
Accessibility HintsContext-aware action hints
  1. Enable VoiceOver: Settings > Accessibility > VoiceOver
  2. Navigate using swipe gestures
  3. Double-tap to activate
  4. Use two-finger scroll for lists

Home Screen:

  • Connect/Disconnect button announces: “Connect to VPN, button” or “Disconnect from VPN, button”
  • Status text announces: “Connection status: Protected” or similar
  • Error alerts are announced immediately with role “alert”
  • Upload/Download cards announce: “Upload: 1.2 GB” and “Download: 3.4 GB”

Servers Screen:

  • Server items announce full context: “Server Name, address, protocol, status, selected/not selected”
  • Offline servers announce disabled state
  • Pull-to-refresh announces “Pull to refresh server list”

Stats Screen:

  • All stat rows announce label and value pairs: “Duration: 2h 34m”
  • StatusCard components announce: “Total Sent: 1.2 GB”, “Total Received: 3.4 GB”

Settings Screen:

  • Switch controls announce their label and current state
  • Configure button announces: “Configure split tunneling, button”
  • Text input announces: “Server address, Enter the server address in host:port format”

Tab Navigation:

  • Tab icons announce: “Home tab, selected” or “Settings tab”
  • Uses proper tab role with selected state
  1. Enable TalkBack: Settings > Accessibility > TalkBack
  2. Navigate by swiping left/right
  3. Double-tap to activate
  4. Use two-finger scroll for content

All behaviors match VoiceOver testing above with Android-specific variations:

  • importantForAccessibility="no" hides decorative elements
  • importantForAccessibility="no-hide-descendants" groups child elements
  • Swipe gestures map to Android TalkBack conventions
  • Focus indicators appear on currently selected element

The following accessibility props are used throughout the app:

// Button example
<TouchableOpacity
accessibilityRole="button"
accessibilityLabel="Connect to VPN"
accessibilityHint="Double tap to establish VPN connection"
accessibilityState={{ disabled: isLoading }}
>
// Switch example
<Switch
accessibilityLabel="Auto-connect toggle"
accessibilityHint="When enabled, the app will connect automatically on launch"
accessibilityState={{ checked: isEnabled }}
/>
// Grouped content example
<View
accessible={true}
accessibilityRole="summary"
accessibilityLabel={`${title}: ${value}`}
>
<Text importantForAccessibility="no">{title}</Text>
<Text importantForAccessibility="no">{value}</Text>
</View>
  1. Emoji Icons: Tab bar uses emoji icons which are hidden from screen readers; proper labels are provided instead
  2. Pull-to-Refresh: Custom accessibility labels describe the refresh action
  3. Dynamic Content: Status updates may not be immediately announced; users should navigate to the status area to hear updates
  4. Error States: Error messages use the alert role for immediate screen reader announcement
  1. Verify client is running and API is enabled
  2. Check network connectivity (same network)
  3. Ensure correct IP address and port
  4. Check firewall settings on client machine
  1. Clear app data and reinstall
  2. Check for conflicting VPN apps
  3. Update to latest version
  1. Increase refetch intervals in settings
  2. Disable notifications if not needed
  3. Disconnect when not in use
  1. Check network connection
  2. Verify client is running
  3. Pull down to refresh manually