RS monogramRussellΒ Schmidt
Lightbox image, just a zoomed in version of the last picture. Hit Escape to exit and return to the last page.

Building a React Native Driver App with InControl API

Prerequisites

  • React Native development environment setup
  • Node.js and npm/yarn installed
  • Access to InControl API credentials (Client ID and Client Secret)
  • Basic knowledge of GraphQL and React Native

Tutorial Overview

We'll create a driver app that enables:

  • Remote start/stop of charging sessions
  • Real-time session monitoring
  • Push notifications for session completion
  • Charging station discovery

Step 1: Project Setup

Initialize React Native Project

npx react-native init InControlDriverApp
cd InControlDriverApp

Install Required Dependencies

npm install @apollo/client graphql react-native-push-notification
npm install @react-native-async-storage/async-storage
npm install react-native-permissions
npm install @react-native-community/netinfo
npm install react-native-maps
npm install @react-native-community/geolocation

For iOS, also run:

cd ios && pod install && cd ..

Step 2: API Configuration

Set up Apollo Client

Create src/apollo/client.js:

import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import AsyncStorage from '@react-native-async-storage/async-storage';

const httpLink = createHttpLink({
  uri: 'https://your-instance.inchargeus.net/api/graphql', // Replace with your instance
});

const authLink = setContext(async (_, { headers }) => {
  const token = await AsyncStorage.getItem('apiToken');
  
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
      'Content-Type': 'application/json',
    }
  }
});

export const client = new ApolloClient({
  link: from([authLink, httpLink]),
  cache: new InMemoryCache(),
  defaultOptions: {
    watchQuery: {
      errorPolicy: 'all'
    }
  }
});

Authentication Service

Create src/services/authService.js:

import AsyncStorage from '@react-native-async-storage/async-storage';

export class AuthService {
  static async authenticateWithCredentials(clientId, clientSecret) {
    try {
      const credentials = `${clientId}:${clientSecret}`;
      const encodedCredentials = btoa(credentials); // Base64 encoding
      
      await AsyncStorage.setItem('apiToken', encodedCredentials);
      return true;
    } catch (error) {
      console.error('Authentication failed:', error);
      return false;
    }
  }

  static async getToken() {
    return await AsyncStorage.getItem('apiToken');
  }

  static async logout() {
    await AsyncStorage.removeItem('apiToken');
  }
}

Step 3: Location Service

Set up Location Tracking

Create src/services/locationService.js:

import Geolocation from '@react-native-community/geolocation';
import { PermissionsAndroid, Platform, Alert } from 'react-native';

export class LocationService {
  static async requestLocationPermission() {
    if (Platform.OS === 'android') {
      try {
        const granted = await PermissionsAndroid.request(
          PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
          {
            title: 'Location Permission',
            message: 'This app needs access to your location to find nearby charging stations.',
            buttonNeutral: 'Ask Me Later',
            buttonNegative: 'Cancel',
            buttonPositive: 'OK',
          }
        );
        return granted === PermissionsAndroid.RESULTS.GRANTED;
      } catch (err) {
        console.warn(err);
        return false;
      }
    }
    return true; // iOS permissions are handled by react-native-maps
  }

  static getCurrentPosition() {
    return new Promise((resolve, reject) => {
      Geolocation.getCurrentPosition(
        (position) => {
          resolve({
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
          });
        },
        (error) => {
          console.error('Location error:', error);
          reject(error);
        },
        {
          enableHighAccuracy: true,
          timeout: 15000,
          maximumAge: 10000,
        }
      );
    });
  }

  static watchPosition(callback) {
    return Geolocation.watchPosition(
      (position) => {
        callback({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
        });
      },
      (error) => {
        console.error('Location watch error:', error);
      },
      {
        enableHighAccuracy: true,
        distanceFilter: 10, // Update every 10 meters
      }
    );
  }

  static clearWatch(watchId) {
    Geolocation.clearWatch(watchId);
  }

  static calculateDistance(lat1, lon1, lat2, lon2) {
    const R = 6371; // Radius of the Earth in kilometers
    const dLat = (lat2 - lat1) * Math.PI / 180;
    const dLon = (lon2 - lon1) * Math.PI / 180;
    const a = 
      Math.sin(dLat/2) * Math.sin(dLat/2) +
      Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * 
      Math.sin(dLon/2) * Math.sin(dLon/2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    const distance = R * c; // Distance in kilometers
    return distance;
  }
}

Step 4: GraphQL Queries and Mutations

Define GraphQL Operations

Create src/graphql/operations.js:

import { gql } from '@apollo/client';

// Query to get available charging stations
export const GET_CHARGING_STATIONS = gql`
  query GetChargingStations($filter: ChargingStationFilter) {
    chargingStations(filter: $filter) {
      id
      name
      location {
        latitude
        longitude
        address
      }
      connectors {
        id
        type
        status
        power
      }
      availability
      status
    }
  }
`;

// Query to get active sessions for a driver
export const GET_ACTIVE_SESSIONS = gql`
  query GetActiveSessions($driverId: ID!) {
    sessions(filter: { driverId: $driverId, status: ACTIVE }) {
      id
      status
      startTime
      estimatedEndTime
      energyDelivered
      chargingStation {
        id
        name
        location {
          address
        }
      }
      connector {
        id
        type
        power
      }
    }
  }
`;

// Mutation to start a charging session
export const START_CHARGING_SESSION = gql`
  mutation StartChargingSession($input: StartSessionInput!) {
    startChargingSession(input: $input) {
      success
      session {
        id
        status
        startTime
        chargingStation {
          name
        }
      }
      errors {
        message
        code
      }
    }
  }
`;

// Mutation to stop a charging session
export const STOP_CHARGING_SESSION = gql`
  mutation StopChargingSession($sessionId: ID!) {
    stopChargingSession(sessionId: $sessionId) {
      success
      session {
        id
        status
        endTime
        totalEnergyDelivered
        totalCost
      }
      errors {
        message
        code
      }
    }
  }
`;

// Subscription for real-time session updates
export const SESSION_UPDATES = gql`
  subscription SessionUpdates($sessionId: ID!) {
    sessionUpdated(sessionId: $sessionId) {
      id
      status
      energyDelivered
      estimatedTimeRemaining
      currentPower
    }
  }
`;

Step 4: React Native Components

Main App Component

Create src/App.js:

import React, { useEffect, useState } from 'react';
import { ApolloProvider } from '@apollo/client';
import { client } from './apollo/client';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { AuthService } from './services/authService';
import LoginScreen from './screens/LoginScreen';
import DashboardScreen from './screens/DashboardScreen';
import ChargingStationsScreen from './screens/ChargingStationsScreen';
import StationMapScreen from './screens/StationMapScreen';
import { setupPushNotifications } from './services/notificationService';

const Stack = createStackNavigator();

export default function App() {
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    checkAuthStatus();
    setupPushNotifications();
  }, []);

  const checkAuthStatus = async () => {
    const token = await AuthService.getToken();
    setIsAuthenticated(!!token);
    setLoading(false);
  };

  if (loading) {
    return null; // Add loading spinner here
  }

  return (
    <ApolloProvider client={client}>
      <NavigationContainer>
        <Stack.Navigator>
          {!isAuthenticated ? (
            <Stack.Screen 
              name="Login" 
              component={LoginScreen}
              options={{ title: 'InControl Driver' }}
            />
          ) : (
            <>
              <Stack.Screen 
                name="Dashboard" 
                component={DashboardScreen}
                options={{ title: 'Dashboard' }}
              />
              <Stack.Screen 
                name="StationMap" 
                component={StationMapScreen}
                options={{ title: 'Charging Stations Map' }}
              />
              <Stack.Screen 
                name="Stations" 
                component={ChargingStationsScreen}
                options={{ title: 'Charging Stations' }}
              />
            </>
          )}
        </Stack.Navigator>
      </NavigationContainer>
    </ApolloProvider>
  );
}

Login Screen

Create src/screens/LoginScreen.js:

import React, { useState } from 'react';
import {
  View,
  Text,
  TextInput,
  TouchableOpacity,
  StyleSheet,
  Alert,
} from 'react-native';
import { AuthService } from '../services/authService';

export default function LoginScreen({ navigation }) {
  const [clientId, setClientId] = useState('');
  const [clientSecret, setClientSecret] = useState('');
  const [loading, setLoading] = useState(false);

  const handleLogin = async () => {
    if (!clientId || !clientSecret) {
      Alert.alert('Error', 'Please enter both Client ID and Client Secret');
      return;
    }

    setLoading(true);
    const success = await AuthService.authenticateWithCredentials(clientId, clientSecret);
    
    if (success) {
      navigation.replace('Dashboard');
    } else {
      Alert.alert('Error', 'Authentication failed. Please check your credentials.');
    }
    setLoading(false);
  };

  return (
    <View style={styles.container}>
      <Text style={styles.title}>InControl Driver App</Text>
      
      <TextInput
        style={styles.input}
        placeholder="Client ID"
        value={clientId}
        onChangeText={setClientId}
        autoCapitalize="none"
      />
      
      <TextInput
        style={styles.input}
        placeholder="Client Secret"
        value={clientSecret}
        onChangeText={setClientSecret}
        secureTextEntry
        autoCapitalize="none"
      />
      
      <TouchableOpacity 
        style={styles.button} 
        onPress={handleLogin}
        disabled={loading}
      >
        <Text style={styles.buttonText}>
          {loading ? 'Logging in...' : 'Login'}
        </Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    justifyContent: 'center',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    textAlign: 'center',
    marginBottom: 40,
  },
  input: {
    borderWidth: 1,
    borderColor: '#ddd',
    padding: 15,
    marginVertical: 10,
    borderRadius: 8,
  },
  button: {
    backgroundColor: '#007AFF',
    padding: 15,
    borderRadius: 8,
    marginTop: 20,
  },
  buttonText: {
    color: 'white',
    textAlign: 'center',
    fontWeight: 'bold',
  },
});

Enhanced Dashboard Screen

Create src/screens/DashboardScreen.js:

import React from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  ScrollView,
} from 'react-native';
import { useQuery } from '@apollo/client';
import { GET_ACTIVE_SESSIONS } from '../graphql/operations';
import ActiveSessionCard from '../components/ActiveSessionCard';

export default function DashboardScreen({ navigation }) {
  const driverId = 'current-driver-id'; // Get from user context
  
  const { data, loading, error, refetch } = useQuery(GET_ACTIVE_SESSIONS, {
    variables: { driverId },
    pollInterval: 30000, // Poll every 30 seconds
  });

  return (
    <ScrollView style={styles.container}>
      <Text style={styles.title}>Dashboard</Text>
      
      <View style={styles.buttonRow}>
        <TouchableOpacity 
          style={[styles.actionButton, styles.mapButton]}
          onPress={() => navigation.navigate('StationMap')}
        >
          <Text style={styles.buttonText}>πŸ—ΊοΈ Station Map</Text>
        </TouchableOpacity>
        
        <TouchableOpacity 
          style={[styles.actionButton, styles.listButton]}
          onPress={() => navigation.navigate('Stations')}
        >
          <Text style={styles.buttonText}>πŸ“‹ Station List</Text>
        </TouchableOpacity>
      </View>

      <View style={styles.section}>
        <Text style={styles.sectionTitle}>Active Sessions</Text>
        {loading ? (
          <Text>Loading sessions...</Text>
        ) : error ? (
          <Text>Error loading sessions</Text>
        ) : data?.sessions?.length > 0 ? (
          data.sessions.map(session => (
            <ActiveSessionCard 
              key={session.id} 
              session={session}
              onRefresh={refetch}
            />
          ))
        ) : (
          <Text style={styles.noSessions}>No active charging sessions</Text>
        )}
      </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  buttonRow: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    marginBottom: 20,
  },
  actionButton: {
    flex: 1,
    padding: 15,
    borderRadius: 8,
    marginHorizontal: 5,
  },
  mapButton: {
    backgroundColor: '#34C759',
  },
  listButton: {
    backgroundColor: '#007AFF',
  },
  buttonText: {
    color: 'white',
    textAlign: 'center',
    fontWeight: 'bold',
  },
  section: {
    marginBottom: 20,
  },
  sectionTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 10,
  },
  noSessions: {
    textAlign: 'center',
    color: '#666',
    fontStyle: 'italic',
  },
});

Active Session Card Component

Create src/components/ActiveSessionCard.js:

import React, { useEffect } from 'react';
import {
  View,
  Text,
  TouchableOpacity,
  StyleSheet,
  Alert,
} from 'react-native';
import { useMutation, useSubscription } from '@apollo/client';
import { STOP_CHARGING_SESSION, SESSION_UPDATES } from '../graphql/operations';
import { showNotification } from '../services/notificationService';

export default function ActiveSessionCard({ session, onRefresh }) {
  const [stopSession, { loading: stopping }] = useMutation(STOP_CHARGING_SESSION);
  
  // Subscribe to real-time updates
  const { data: updateData } = useSubscription(SESSION_UPDATES, {
    variables: { sessionId: session.id }
  });

  useEffect(() => {
    if (updateData?.sessionUpdated?.status === 'COMPLETED') {
      showNotification({
        title: 'Charging Complete',
        message: `Your charging session at ${session.chargingStation.name} has completed.`,
      });
      onRefresh();
    }
  }, [updateData]);

  const handleStopSession = () => {
    Alert.alert(
      'Stop Charging',
      'Are you sure you want to stop this charging session?',
      [
        { text: 'Cancel', style: 'cancel' },
        { 
          text: 'Stop', 
          style: 'destructive',
          onPress: async () => {
            try {
              const result = await stopSession({
                variables: { sessionId: session.id }
              });
              
              if (result.data?.stopChargingSession?.success) {
                Alert.alert('Success', 'Charging session stopped successfully');
                onRefresh();
              } else {
                Alert.alert('Error', 'Failed to stop charging session');
              }
            } catch (error) {
              Alert.alert('Error', 'Network error occurred');
            }
          }
        }
      ]
    );
  };

  const currentSession = updateData?.sessionUpdated || session;

  return (
    <View style={styles.card}>
      <Text style={styles.stationName}>
        {session.chargingStation.name}
      </Text>
      <Text style={styles.address}>
        {session.chargingStation.location.address}
      </Text>
      
      <View style={styles.details}>
        <Text>Status: {currentSession.status}</Text>
        <Text>Energy: {currentSession.energyDelivered} kWh</Text>
        {currentSession.currentPower && (
          <Text>Power: {currentSession.currentPower} kW</Text>
        )}
        {currentSession.estimatedTimeRemaining && (
          <Text>Time Remaining: {currentSession.estimatedTimeRemaining} min</Text>
        )}
      </View>

      <TouchableOpacity 
        style={styles.stopButton}
        onPress={handleStopSession}
        disabled={stopping}
      >
        <Text style={styles.stopButtonText}>
          {stopping ? 'Stopping...' : 'Stop Charging'}
        </Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    backgroundColor: 'white',
    padding: 15,
    borderRadius: 8,
    marginBottom: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  stationName: {
    fontSize: 16,
    fontWeight: 'bold',
  },
  address: {
    color: '#666',
    marginBottom: 10,
  },
  details: {
    marginBottom: 15,
  },
  stopButton: {
    backgroundColor: '#FF3B30',
    padding: 12,
    borderRadius: 6,
  },
  stopButtonText: {
    color: 'white',
    textAlign: 'center',
    fontWeight: 'bold',
  },
});

Dedicated Station Map Screen

Create src/screens/StationMapScreen.js:

import React, { useState, useEffect } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  Alert,
  Dimensions,
  ScrollView,
} from 'react-native';
import MapView, { Marker, PROVIDER_GOOGLE, Circle } from 'react-native-maps';
import { useQuery, useMutation } from '@apollo/client';
import { GET_CHARGING_STATIONS, START_CHARGING_SESSION } from '../graphql/operations';
import { LocationService } from '../services/locationService';

const { width, height } = Dimensions.get('window');

export default function StationMapScreen({ navigation }) {
  const [userLocation, setUserLocation] = useState(null);
  const [selectedStation, setSelectedStation] = useState(null);
  const [searchRadius, setSearchRadius] = useState(5000); // 5km default
  const [mapRegion, setMapRegion] = useState({
    latitude: 37.78825,
    longitude: -122.4324,
    latitudeDelta: 0.0922,
    longitudeDelta: 0.0421,
  });

  const { data, loading, error, refetch } = useQuery(GET_CHARGING_STATIONS, {
    variables: {
      filter: {
        availability: 'AVAILABLE',
        ...(userLocation && {
          location: {
            latitude: userLocation.latitude,
            longitude: userLocation.longitude,
            radius: searchRadius,
          }
        })
      }
    }
  });

  const [startSession, { loading: starting }] = useMutation(START_CHARGING_SESSION);

  useEffect(() => {
    initializeLocation();
  }, []);

  useEffect(() => {
    // Watch for location changes
    let watchId;
    if (userLocation) {
      watchId = LocationService.watchPosition((newLocation) => {
        setUserLocation(newLocation);
        refetch();
      });
    }
    
    return () => {
      if (watchId) {
        LocationService.clearWatch(watchId);
      }
    };
  }, [userLocation, refetch]);

  const initializeLocation = async () => {
    const hasPermission = await LocationService.requestLocationPermission();
    
    if (hasPermission) {
      try {
        const location = await LocationService.getCurrentPosition();
        setUserLocation(location);
        setMapRegion({
          ...location,
          latitudeDelta: 0.02,
          longitudeDelta: 0.02,
        });
        refetch();
      } catch (error) {
        Alert.alert('Location Error', 'Could not get your current location');
      }
    } else {
      Alert.alert('Permission Denied', 'Location permission is required to find nearby stations');
    }
  };

  const handleStartCharging = async (stationId, connectorId) => {
    try {
      const result = await startSession({
        variables: {
          input: {
            chargingStationId: stationId,
            connectorId: connectorId,
            driverId: 'current-driver-id',
          }
        }
      });

      if (result.data?.startChargingSession?.success) {
        Alert.alert('Success', 'Charging session started successfully', [
          { 
            text: 'OK', 
            onPress: () => {
              setSelectedStation(null);
              navigation.navigate('Dashboard');
            }
          }
        ]);
      } else {
        const error = result.data?.startChargingSession?.errors?.[0]?.message;
        Alert.alert('Error', error || 'Failed to start charging session');
      }
    } catch (error) {
      Alert.alert('Error', 'Network error occurred');
    }
  };

  const getMarkerColor = (station) => {
    const availableConnectors = station.connectors.filter(c => c.status === 'AVAILABLE').length;
    if (availableConnectors === 0) return '#FF3B30';
    if (availableConnectors <= 2) return '#FF9500';
    return '#34C759';
  };

  const getDistanceText = (station) => {
    if (!userLocation) return '';
    const distance = LocationService.calculateDistance(
      userLocation.latitude,
      userLocation.longitude,
      station.location.latitude,
      station.location.longitude
    );
    return distance < 1 ? `${Math.round(distance * 1000)}m` : `${distance.toFixed(1)}km`;
  };

  const adjustSearchRadius = (newRadius) => {
    setSearchRadius(newRadius);
    // Update map region to show the new search area
    if (userLocation) {
      const latitudeDelta = (newRadius / 111320) * 2.5; // Rough conversion
      const longitudeDelta = latitudeDelta;
      setMapRegion({
        ...userLocation,
        latitudeDelta,
        longitudeDelta,
      });
    }
    refetch();
  };

  if (loading && !data) {
    return (
      <View style={styles.centered}>
        <Text>Loading charging stations...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.centered}>
        <Text>Error loading stations</Text>
        <TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
          <Text style={styles.retryText}>Retry</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      {/* Search Radius Controls */}
      <View style={styles.radiusControls}>
        <ScrollView horizontal showsHorizontalScrollIndicator={false}>
          {[1000, 2000, 5000, 10000, 20000].map(radius => (
            <TouchableOpacity
              key={radius}
              style={[
                styles.radiusButton,
                searchRadius === radius && styles.activeRadiusButton
              ]}
              onPress={() => adjustSearchRadius(radius)}
            >
              <Text style={[
                styles.radiusButtonText,
                searchRadius === radius && styles.activeRadiusButtonText
              ]}>
                {radius < 1000 ? `${radius}m` : `${radius/1000}km`}
              </Text>
            </TouchableOpacity>
          ))}
        </ScrollView>
      </View>

      <MapView
        provider={PROVIDER_GOOGLE}
        style={styles.map}
        region={mapRegion}
        showsUserLocation={true}
        showsMyLocationButton={true}
        onRegionChangeComplete={setMapRegion}
      >
        {/* Search radius circle */}
        {userLocation && (
          <Circle
            center={userLocation}
            radius={searchRadius}
            strokeColor="rgba(0, 122, 255, 0.5)"
            fillColor="rgba(0, 122, 255, 0.1)"
            strokeWidth={2}
          />
        )}

        {/* Charging Station Markers */}
        {data?.chargingStations?.map(station => (
          <Marker
            key={station.id}
            coordinate={{
              latitude: station.location.latitude,
              longitude: station.location.longitude,
            }}
            title={station.name}
            description={`${station.connectors.filter(c => c.status === 'AVAILABLE').length} available`}
            pinColor={getMarkerColor(station)}
            onPress={() => setSelectedStation(station)}
          />
        ))}
      </MapView>

      {/* Station Details Bottom Sheet */}
      {selectedStation && (
        <View style={styles.bottomSheet}>
          <ScrollView style={styles.bottomSheetContent}>
            <View style={styles.bottomSheetHeader}>
              <Text style={styles.stationName}>{selectedStation.name}</Text>
              <TouchableOpacity
                style={styles.closeButton}
                onPress={() => setSelectedStation(null)}
              >
                <Text style={styles.closeButtonText}>Γ—</Text>
              </TouchableOpacity>
            </View>
            
            <Text style={styles.address}>{selectedStation.location.address}</Text>
            
            {userLocation && (
              <Text style={styles.distance}>
                πŸ“ {getDistanceText(selectedStation)} away
              </Text>
            )}
            
            <Text style={styles.status}>
              Status: <Text style={styles.statusValue}>{selectedStation.status}</Text>
            </Text>
            
            <View style={styles.connectorsSection}>
              <Text style={styles.connectorsTitle}>
                Available Connectors ({selectedStation.connectors.filter(c => c.status === 'AVAILABLE').length}):
              </Text>
              
              {selectedStation.connectors
                .filter(connector => connector.status === 'AVAILABLE')
                .map(connector => (
                  <TouchableOpacity
                    key={connector.id}
                    style={styles.connectorButton}
                    onPress={() => handleStartCharging(selectedStation.id, connector.id)}
                    disabled={starting}
                  >
                    <Text style={styles.connectorText}>
                      πŸ”Œ {connector.type} - {connector.power}kW
                    </Text>
                    {starting && <Text style={styles.startingText}>Starting...</Text>}
                  </TouchableOpacity>
                ))
              }
              
              {selectedStation.connectors.filter(c => c.status === 'AVAILABLE').length === 0 && (
                <View style={styles.noConnectorsContainer}>
                  <Text style={styles.noConnectors}>⚠️ No available connectors</Text>
                  <Text style={styles.noConnectorsSubtext}>All connectors are currently in use</Text>
                </View>
              )}
            </View>

            {/* Navigation Button */}
            <TouchableOpacity
              style={styles.navigationButton}
              onPress={() => {
                // This would typically open the native maps app
                Alert.alert('Navigation', 'This would open your preferred navigation app');
              }}
            >
              <Text style={styles.navigationButtonText}>πŸ—ΊοΈ Get Directions</Text>
            </TouchableOpacity>
          </ScrollView>
        </View>
      )}

      {/* Floating Action Buttons */}
      <View style={styles.floatingButtons}>
        {userLocation && (
          <TouchableOpacity
            style={styles.centerButton}
            onPress={() => setMapRegion({
              ...userLocation,
              latitudeDelta: 0.02,
              longitudeDelta: 0.02,
            })}
          >
            <Text style={styles.centerButtonText}>πŸ“</Text>
          </TouchableOpacity>
        )}
        
        <TouchableOpacity
          style={styles.refreshButton}
          onPress={() => refetch()}
        >
          <Text style={styles.refreshButtonText}>πŸ”„</Text>
        </TouchableOpacity>
      </View>

      {/* Stats Bar */}
      <View style={styles.statsBar}>
        <Text style={styles.statsText}>
          Found {data?.chargingStations?.length || 0} stations within {searchRadius < 1000 ? `${searchRadius}m` : `${searchRadius/1000}km`}
        </Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  centered: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  map: {
    flex: 1,
  },
  radiusControls: {
    position: 'absolute',
    top: 10,
    left: 0,
    right: 0,
    zIndex: 1,
    paddingHorizontal: 15,
  },
  radiusButton: {
    backgroundColor: 'white',
    paddingHorizontal: 15,
    paddingVertical: 8,
    borderRadius: 20,
    marginHorizontal: 5,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  activeRadiusButton: {
    backgroundColor: '#007AFF',
  },
  radiusButtonText: {
    fontWeight: 'bold',
    color: '#333',
  },
  activeRadiusButtonText: {
    color: 'white',
  },
  retryButton: {
    backgroundColor: '#007AFF',
    padding: 15,
    borderRadius: 8,
    marginTop: 20,
  },
  retryText: {
    color: 'white',
    fontWeight: 'bold',
  },
  bottomSheet: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: 'white',
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    maxHeight: height * 0.6,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: -2 },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  bottomSheetContent: {
    padding: 20,
  },
  bottomSheetHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 10,
  },
  stationName: {
    fontSize: 20,
    fontWeight: 'bold',
    flex: 1,
    color: '#333',
  },
  closeButton: {
    width: 30,
    height: 30,
    borderRadius: 15,
    backgroundColor: '#f0f0f0',
    justifyContent: 'center',
    alignItems: 'center',
  },
  closeButtonText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#666',
  },
  address: {
    color: '#666',
    marginBottom: 8,
    fontSize: 14,
  },
  distance: {
    color: '#007AFF',
    fontWeight: 'bold',
    marginBottom: 8,
    fontSize: 14,
  },
  status: {
    marginBottom: 15,
    fontSize: 14,
  },
  statusValue: {
    fontWeight: 'bold',
    color: '#34C759',
  },
  connectorsSection: {
    marginBottom: 15,
  },
  connectorsTitle: {
    fontWeight: 'bold',
    marginBottom: 10,
    fontSize: 16,
    color: '#333',
  },
  connectorButton: {
    backgroundColor: '#007AFF',
    padding: 15,
    borderRadius: 8,
    marginVertical: 3,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  connectorText: {
    color: 'white',
    fontWeight: 'bold',
    flex: 1,
  },
  startingText: {
    color: 'white',
    fontStyle: 'italic',
  },
  noConnectorsContainer: {
    padding: 20,
    alignItems: 'center',
  },
  noConnectors: {
    textAlign: 'center',
    color: '#FF3B30',
    fontWeight: 'bold',
    fontSize: 16,
  },
  noConnectorsSubtext: {
    textAlign: 'center',
    color: '#666',
    fontSize: 12,
    marginTop: 5,
  },
  navigationButton: {
    backgroundColor: '#34C759',
    padding: 15,
    borderRadius: 8,
    marginTop: 10,
  },
  navigationButtonText: {
    color: 'white',
    textAlign: 'center',
    fontWeight: 'bold',
    fontSize: 16,
  },
  floatingButtons: {
    position: 'absolute',
    right: 20,
    top: 80,
  },
  centerButton: {
    width: 50,
    height: 50,
    borderRadius: 25,
    backgroundColor: 'white',
    justifyContent: 'center',
    alignItems: 'center',
    marginBottom: 10,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  centerButtonText: {
    fontSize: 24,
  },
  refreshButton: {
    width: 50,
    height: 50,
    borderRadius: 25,
    backgroundColor: 'white',
    justifyContent: 'center',
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  refreshButtonText: {
    fontSize: 20,
  },
  statsBar: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: 'rgba(0, 0, 0, 0.8)',
    padding: 10,
  },
  statsText: {
    color: 'white',
    textAlign: 'center',
    fontSize: 12,
  },
});

Enhanced Charging Stations Screen with Map

Create src/screens/ChargingStationsScreen.js:

import React, { useState, useEffect } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  Alert,
  Dimensions,
  ScrollView,
} from 'react-native';
import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps';
import { useQuery, useMutation } from '@apollo/client';
import { GET_CHARGING_STATIONS, START_CHARGING_SESSION } from '../graphql/operations';
import { LocationService } from '../services/locationService';

const { width, height } = Dimensions.get('window');

export default function ChargingStationsScreen() {
  const [userLocation, setUserLocation] = useState(null);
  const [selectedStation, setSelectedStation] = useState(null);
  const [mapRegion, setMapRegion] = useState({
    latitude: 37.78825,
    longitude: -122.4324,
    latitudeDelta: 0.0922,
    longitudeDelta: 0.0421,
  });

  const { data, loading, error, refetch } = useQuery(GET_CHARGING_STATIONS, {
    variables: {
      filter: {
        availability: 'AVAILABLE',
        // Add radius filter based on user location
        ...(userLocation && {
          location: {
            latitude: userLocation.latitude,
            longitude: userLocation.longitude,
            radius: 10000, // 10km radius
          }
        })
      }
    }
  });

  const [startSession, { loading: starting }] = useMutation(START_CHARGING_SESSION);

  useEffect(() => {
    initializeLocation();
  }, []);

  const initializeLocation = async () => {
    const hasPermission = await LocationService.requestLocationPermission();
    
    if (hasPermission) {
      try {
        const location = await LocationService.getCurrentPosition();
        setUserLocation(location);
        setMapRegion({
          ...location,
          latitudeDelta: 0.01,
          longitudeDelta: 0.01,
        });
        
        // Refetch stations with new location
        refetch();
      } catch (error) {
        Alert.alert('Location Error', 'Could not get your current location');
      }
    } else {
      Alert.alert('Permission Denied', 'Location permission is required to find nearby stations');
    }
  };

  const handleStartCharging = async (stationId, connectorId) => {
    try {
      const result = await startSession({
        variables: {
          input: {
            chargingStationId: stationId,
            connectorId: connectorId,
            driverId: 'current-driver-id',
          }
        }
      });

      if (result.data?.startChargingSession?.success) {
        Alert.alert('Success', 'Charging session started successfully');
        setSelectedStation(null);
      } else {
        const error = result.data?.startChargingSession?.errors?.[0]?.message;
        Alert.alert('Error', error || 'Failed to start charging session');
      }
    } catch (error) {
      Alert.alert('Error', 'Network error occurred');
    }
  };

  const getMarkerColor = (station) => {
    const availableConnectors = station.connectors.filter(c => c.status === 'AVAILABLE').length;
    if (availableConnectors === 0) return '#FF3B30'; // Red for no availability
    if (availableConnectors <= 2) return '#FF9500'; // Orange for limited availability
    return '#34C759'; // Green for good availability
  };

  const getDistanceText = (station) => {
    if (!userLocation) return '';
    const distance = LocationService.calculateDistance(
      userLocation.latitude,
      userLocation.longitude,
      station.location.latitude,
      station.location.longitude
    );
    return distance < 1 ? `${Math.round(distance * 1000)}m` : `${distance.toFixed(1)}km`;
  };

  if (loading && !data) {
    return (
      <View style={styles.centered}>
        <Text>Loading charging stations...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View style={styles.centered}>
        <Text>Error loading stations</Text>
        <TouchableOpacity style={styles.retryButton} onPress={() => refetch()}>
          <Text style={styles.retryText}>Retry</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <MapView
        provider={PROVIDER_GOOGLE}
        style={styles.map}
        region={mapRegion}
        showsUserLocation={true}
        showsMyLocationButton={true}
        onRegionChangeComplete={setMapRegion}
      >
        {/* User Location Marker */}
        {userLocation && (
          <Marker
            coordinate={userLocation}
            title="Your Location"
            pinColor="blue"
          />
        )}

        {/* Charging Station Markers */}
        {data?.chargingStations?.map(station => (
          <Marker
            key={station.id}
            coordinate={{
              latitude: station.location.latitude,
              longitude: station.location.longitude,
            }}
            title={station.name}
            description={`${station.connectors.filter(c => c.status === 'AVAILABLE').length} available connectors`}
            pinColor={getMarkerColor(station)}
            onPress={() => setSelectedStation(station)}
          />
        ))}
      </MapView>

      {/* Station Details Bottom Sheet */}
      {selectedStation && (
        <View style={styles.bottomSheet}>
          <ScrollView style={styles.bottomSheetContent}>
            <View style={styles.bottomSheetHeader}>
              <Text style={styles.stationName}>{selectedStation.name}</Text>
              <TouchableOpacity
                style={styles.closeButton}
                onPress={() => setSelectedStation(null)}
              >
                <Text style={styles.closeButtonText}>Γ—</Text>
              </TouchableOpacity>
            </View>
            
            <Text style={styles.address}>{selectedStation.location.address}</Text>
            
            {userLocation && (
              <Text style={styles.distance}>
                Distance: {getDistanceText(selectedStation)}
              </Text>
            )}
            
            <Text style={styles.status}>Status: {selectedStation.status}</Text>
            
            <View style={styles.connectorsSection}>
              <Text style={styles.connectorsTitle}>Available Connectors:</Text>
              {selectedStation.connectors
                .filter(connector => connector.status === 'AVAILABLE')
                .map(connector => (
                  <TouchableOpacity
                    key={connector.id}
                    style={styles.connectorButton}
                    onPress={() => handleStartCharging(selectedStation.id, connector.id)}
                    disabled={starting}
                  >
                    <Text style={styles.connectorText}>
                      {connector.type} - {connector.power}kW
                    </Text>
                  </TouchableOpacity>
                ))
              }
              
              {selectedStation.connectors.filter(c => c.status === 'AVAILABLE').length === 0 && (
                <Text style={styles.noConnectors}>No available connectors</Text>
              )}
            </View>
          </ScrollView>
        </View>
      )}

      {/* Floating Action Button to Center on User */}
      {userLocation && (
        <TouchableOpacity
          style={styles.centerButton}
          onPress={() => setMapRegion({
            ...userLocation,
            latitudeDelta: 0.01,
            longitudeDelta: 0.01,
          })}
        >
          <Text style={styles.centerButtonText}>πŸ“</Text>
        </TouchableOpacity>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  centered: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  map: {
    flex: 1,
  },
  retryButton: {
    backgroundColor: '#007AFF',
    padding: 15,
    borderRadius: 8,
    marginTop: 20,
  },
  retryText: {
    color: 'white',
    fontWeight: 'bold',
  },
  bottomSheet: {
    position: 'absolute',
    bottom: 0,
    left: 0,
    right: 0,
    backgroundColor: 'white',
    borderTopLeftRadius: 20,
    borderTopRightRadius: 20,
    maxHeight: height * 0.5,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: -2 },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  bottomSheetContent: {
    padding: 20,
  },
  bottomSheetHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 10,
  },
  stationName: {
    fontSize: 20,
    fontWeight: 'bold',
    flex: 1,
  },
  closeButton: {
    width: 30,
    height: 30,
    borderRadius: 15,
    backgroundColor: '#f0f0f0',
    justifyContent: 'center',
    alignItems: 'center',
  },
  closeButtonText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#666',
  },
  address: {
    color: '#666',
    marginBottom: 5,
  },
  distance: {
    color: '#007AFF',
    fontWeight: 'bold',
    marginBottom: 5,
  },
  status: {
    marginBottom: 15,
  },
  connectorsSection: {
    marginBottom: 20,
  },
  connectorsTitle: {
    fontWeight: 'bold',
    marginBottom: 10,
    fontSize: 16,
  },
  connectorButton: {
    backgroundColor: '#007AFF',
    padding: 12,
    borderRadius: 8,
    marginVertical: 3,
  },
  connectorText: {
    color: 'white',
    textAlign: 'center',
    fontWeight: 'bold',
  },
  noConnectors: {
    textAlign: 'center',
    color: '#666',
    fontStyle: 'italic',
    padding: 20,
  },
  centerButton: {
    position: 'absolute',
    top: 60,
    right: 20,
    width: 50,
    height: 50,
    borderRadius: 25,
    backgroundColor: 'white',
    justifyContent: 'center',
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  centerButtonText: {
    fontSize: 24,
  },
});

Step 5: Push Notification Service

Create src/services/notificationService.js:

import PushNotification from 'react-native-push-notification';
import { Platform } from 'react-native';

export const setupPushNotifications = () => {
  PushNotification.configure({
    onNotification: function(notification) {
      console.log('Notification received:', notification);
    },
    requestPermissions: Platform.OS === 'ios',
  });

  // Create notification channels for Android
  if (Platform.OS === 'android') {
    PushNotification.createChannel(
      {
        channelId: 'charging-sessions',
        channelName: 'Charging Sessions',
        channelDescription: 'Notifications for charging session updates',
        soundName: 'default',
        importance: 4,
        vibrate: true,
      },
      (created) => console.log(`Channel created: ${created}`)
    );
  }
};

export const showNotification = ({ title, message, data = {} }) => {
  PushNotification.localNotification({
    channelId: 'charging-sessions',
    title,
    message,
    userInfo: data,
    playSound: true,
    soundName: 'default',
    vibrate: true,
  });
};

Step 6: App Configuration

Update App.js (Root)

import React from 'react';
import App from './src/App';

export default App;

Configure Permissions

Android (android/app/src/main/AndroidManifest.xml)

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- Add Google Maps API key -->
<application>
  <meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="YOUR_GOOGLE_MAPS_API_KEY_HERE"/>
</application>

iOS (ios/YourApp/Info.plist)

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access to find nearby charging stations</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs location access to find nearby charging stations</string>

Google Maps Setup

  1. Get Google Maps API Key:

    • Go to Google Cloud Console
    • Enable Maps SDK for Android/iOS
    • Create API key and restrict it appropriately
  2. Configure for iOS (ios/Podfile):

# Add to your Podfile
pod 'GoogleMaps'
pod 'Google-Maps-iOS-Utils'
  1. Run pod install:
cd ios && pod install && cd ..

Step 7: Running and Testing

Start the Development Server

npm start

Run on Device/Emulator

# iOS
npm run ios

# Android
npm run android

Key Features Implemented

  1. Authentication: Secure API key-based authentication
  2. Real-time Updates: GraphQL subscriptions for live session monitoring
  3. Remote Control: Start and stop charging sessions remotely
  4. Push Notifications: Local notifications for session completion
  5. Interactive Map: Native map with user location and charging station markers
  6. Location Services: GPS tracking and distance calculations
  7. Station Discovery: Browse and filter available charging stations by radius
  8. Session Management: View active sessions with real-time status
  9. Visual Indicators: Color-coded markers showing station availability
  10. Search Radius Control: Adjustable search area from 1km to 20km

Best Practices

  1. Error Handling: Comprehensive error handling for network issues
  2. Loading States: User-friendly loading indicators
  3. Offline Support: Cache important data for offline viewing
  4. Security: Secure storage of API credentials
  5. Performance: Efficient polling and subscription management
  6. Location Privacy: Request permissions appropriately and handle denials gracefully
  7. Map Performance: Optimize marker rendering for large datasets
  8. Battery Optimization: Efficient location tracking with appropriate distance filters

Map-Specific Features

Color-Coded Markers

  • 🟒 Green: Good availability (3+ connectors)
  • 🟠 Orange: Limited availability (1-2 connectors)
  • πŸ”΄ Red: No availability (0 connectors)

Location Features

  • User Location Tracking: Real-time GPS positioning
  • Distance Calculations: Accurate distance measurements to stations
  • Search Radius: Adjustable from 1km to 20km
  • Auto-refresh: Location updates trigger station data refresh

Map Controls

  • Center Button: Quick return to user location
  • Refresh Button: Manual data refresh
  • Radius Picker: Easy search area adjustment
  • Stats Bar: Shows number of stations found

Next Steps

  1. Add route planning to selected charging stations
  2. Implement real-time availability updates on map
  3. Add clustering for better performance with many markers
  4. Enhance offline support with cached map tiles
  5. Implement advanced filtering (connector type, power level, pricing)
  6. Add navigation integration with native maps apps
  7. Include augmented reality features for station identification
  8. Add charging history and analytics dashboard
  9. Implement favorite stations and trip planning
  10. Add voice guidance and accessibility features

Troubleshooting Map Issues

Common Problems and Solutions

  1. Map not showing:

    • Verify Google Maps API key is configured
    • Check that Maps SDK is enabled in Google Cloud Console
  2. Location not working:

    • Ensure location permissions are granted
    • Check that location services are enabled on device
  3. Markers not appearing:

    • Verify GraphQL queries return valid coordinates
    • Check that latitude/longitude values are valid numbers
  4. Performance issues:

    • Implement marker clustering for large datasets
    • Use appropriate map region bounds
    • Optimize GraphQL queries with proper filtering

This tutorial provides a solid foundation for building a production-ready driver app with the InControl API. Remember to test thoroughly and handle edge cases for the best user experience.