JSPM

@trackthatride/react-native-map

1.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • 0
  • Score
    100M100P100Q25057F
  • License MIT

React Native components for real-time tracking maps

Package Exports

  • @trackthatride/react-native-map

Readme

@trackthatride/react-native-map

React Native SDK for building mobile delivery tracking applications. Includes components and hooks optimized for iOS and Android with background mode support, battery optimization, and native map integration.

Installation

npm install @trackthatride/react-native-map

Required Dependencies

npm install react-native-maps

iOS Setup

  1. Install CocoaPods dependencies:
cd ios && pod install && cd ..
  1. Add location permissions to ios/YourApp/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show delivery tracking</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need your location to provide real-time tracking updates</string>
  1. Enable Google Maps (optional, for Google Maps instead of Apple Maps):

Add to ios/Podfile:

# Uncomment the next line to use Google Maps
# pod 'GoogleMaps'

Android Setup

  1. Add Google Maps API key to android/app/src/main/AndroidManifest.xml:
<application>
  <meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="YOUR_GOOGLE_MAPS_API_KEY"/>
</application>
  1. Add permissions to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  1. Update android/build.gradle:
buildscript {
    ext {
        googlePlayServicesVersion = "18.0.0"
        androidMapsUtilsVersion = "2.3.0"
    }
}

Quick Start

import { TrackingMap, TrackingInfo } from '@trackthatride/react-native-map';
import { View } from 'react-native';

function TrackingScreen() {
  const [trackingState, setTrackingState] = useState(null);

  return (
    <View style={{ flex: 1 }}>
      <TrackingMap
        trackingCode="TR123456789"
        apiBaseUrl="https://your-app.replit.app"
        onStateChange={setTrackingState}
        style={{ flex: 1 }}
      />
      
      {trackingState && (
        <TrackingInfo
          state={trackingState}
          showDriverDetails
          showETA
        />
      )}
    </View>
  );
}

Components

TrackingMap

Native map component with real-time tracking, optimized for mobile devices.

Props

interface TrackingMapProps {
  trackingCode: string;                    // Required: Tracking code to display
  apiBaseUrl?: string;                     // Optional: API base URL
  style?: ViewStyle;                       // Optional: Container style
  initialRegion?: Region;                  // Optional: Initial map region
  mapType?: 'standard' | 'satellite' | 'hybrid' | 'terrain';
  enableRealtime?: boolean;                // Optional: Enable SSE (default: true)
  showRoute?: boolean;                     // Optional: Show route (default: true)
  showsUserLocation?: boolean;             // Optional: Show user location (default: false)
  followsUserLocation?: boolean;           // Optional: Follow user (default: false)
  markers?: {                              // Optional: Custom markers
    pickup?: { image?: any; title?: string; description?: string };
    delivery?: { image?: any; title?: string; description?: string };
    driver?: { image?: any; title?: string; description?: string };
  };
  onStateChange?: (state) => void;         // Optional: State change callback
  onMapReady?: () => void;                 // Optional: Map ready callback
  onError?: (error: Error) => void;        // Optional: Error callback
  onMarkerPress?: (markerId, coord) => void; // Optional: Marker press callback
  mapViewProps?: Partial<MapViewProps>;    // Optional: Additional MapView props
}

Basic Usage

import { TrackingMap } from '@trackthatride/react-native-map';

<TrackingMap
  trackingCode="TR123456789"
  apiBaseUrl="https://your-app.replit.app"
  style={{ flex: 1 }}
/>

Custom Region

<TrackingMap
  trackingCode="TR123456789"
  initialRegion={{
    latitude: 37.78825,
    longitude: -122.4324,
    latitudeDelta: 0.0922,
    longitudeDelta: 0.0421,
  }}
  mapType="hybrid"
/>

Custom Markers

<TrackingMap
  trackingCode="TR123456789"
  markers={{
    pickup: {
      image: require('./assets/pickup-marker.png'),
      title: 'Pickup Location',
    },
    delivery: {
      image: require('./assets/delivery-marker.png'),
      title: 'Delivery Location',
    },
    driver: {
      image: require('./assets/car-marker.png'),
      title: 'Driver',
    },
  }}
/>

Event Handling

<TrackingMap
  trackingCode="TR123456789"
  onStateChange={(state) => {
    console.log('Ride status:', state.ride?.status);
  }}
  onMarkerPress={(markerId, coordinate) => {
    console.log(`Marker ${markerId} pressed at:`, coordinate);
  }}
  onError={(error) => {
    Alert.alert('Error', error.message);
  }}
  onMapReady={() => {
    console.log('Map is ready');
  }}
/>

TrackingInfo

Display formatted tracking information with native styling.

Props

interface TrackingInfoProps {
  state: TrackingSessionState;             // Required: Current tracking state
  style?: ViewStyle;                       // Optional: Container style
  showDriverDetails?: boolean;             // Optional: Show driver info (default: true)
  showETA?: boolean;                       // Optional: Show ETA (default: true)
  showConnectionStatus?: boolean;          // Optional: Show status (default: false)
  colors?: {                               // Optional: Color scheme
    background?: string;
    text?: string;
    accent?: string;
    success?: string;
    warning?: string;
    error?: string;
  };
  formatters?: {                           // Optional: Custom formatters
    time?: (timestamp: string) => string;
    status?: (status: string) => string;
    eta?: (eta: string) => string;
  };
  textStyles?: {                           // Optional: Text styles
    title?: TextStyle;
    subtitle?: TextStyle;
    body?: TextStyle;
    caption?: TextStyle;
  };
}

Basic Usage

<TrackingInfo
  state={trackingState}
  showDriverDetails
  showETA
  showConnectionStatus
/>

Custom Colors

<TrackingInfo
  state={trackingState}
  colors={{
    background: '#FFFFFF',
    text: '#000000',
    accent: '#007AFF',
    success: '#34C759',
    warning: '#FF9500',
    error: '#FF3B30',
  }}
/>

Custom Formatting

<TrackingInfo
  state={trackingState}
  formatters={{
    time: (timestamp) => {
      const date = new Date(timestamp);
      return date.toLocaleTimeString('en-US', {
        hour: '2-digit',
        minute: '2-digit',
      });
    },
    eta: (eta) => {
      const minutes = Math.ceil(
        (new Date(eta) - new Date()) / 60000
      );
      return minutes < 60
        ? `${minutes} min`
        : `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
    },
  }}
/>

Hooks

useTrackingSession

React Native hook with mobile-specific optimizations including background mode support.

API

function useTrackingSession(config?: TrackingSessionConfig): {
  state: TrackingSessionState | null;
  isConnected: boolean;
  connectionStatus: ConnectionStatus;
  connect: (trackingCode: string) => Promise<void>;
  disconnect: () => void;
  refresh: () => Promise<void>;
  error: Error | null;
  isBackground: boolean;  // Mobile-specific: background state
}

Basic Usage

import { useTrackingSession } from '@trackthatride/react-native-map';
import { useEffect } from 'react';

function TrackingScreen() {
  const {
    state,
    isConnected,
    connect,
    disconnect,
    error,
    isBackground,
  } = useTrackingSession({
    baseUrl: 'https://your-app.replit.app',
  });

  useEffect(() => {
    connect('TR123456789');
    return () => disconnect();
  }, []);

  if (error) {
    return <Text>Error: {error.message}</Text>;
  }

  return (
    <View>
      <Text>Status: {state?.ride?.status}</Text>
      <Text>Background: {isBackground ? 'Yes' : 'No'}</Text>
    </View>
  );
}

Background Mode Handling

The hook automatically handles background mode:

function TrackingScreen() {
  const { state, isBackground } = useTrackingSession({
    baseUrl: 'https://your-app.replit.app',
  });

  useEffect(() => {
    connect('TR123456789');
  }, []);

  // Hook automatically:
  // - Disconnects SSE when app goes to background
  // - Switches to 30-second polling in background
  // - Reconnects SSE when app comes to foreground

  return (
    <View>
      {isBackground && (
        <Text>Running in background mode (polling every 30s)</Text>
      )}
      <Text>Status: {state?.ride?.status}</Text>
    </View>
  );
}

Complete Examples

Full Tracking Screen

import React, { useState, useEffect } from 'react';
import {
  View,
  StyleSheet,
  SafeAreaView,
  StatusBar,
  Alert,
} from 'react-native';
import {
  TrackingMap,
  TrackingInfo,
} from '@trackthatride/react-native-map';

function FullTrackingScreen({ route }) {
  const { trackingCode } = route.params;
  const [trackingState, setTrackingState] = useState(null);

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />
      
      <View style={styles.mapContainer}>
        <TrackingMap
          trackingCode={trackingCode}
          apiBaseUrl="https://your-app.replit.app"
          onStateChange={setTrackingState}
          onError={(error) => {
            Alert.alert('Error', error.message);
          }}
          style={styles.map}
          enableRealtime
          showRoute
        />
      </View>

      <View style={styles.infoContainer}>
        {trackingState ? (
          <TrackingInfo
            state={trackingState}
            showDriverDetails
            showETA
            showConnectionStatus
            style={styles.info}
          />
        ) : (
          <Text style={styles.loading}>Loading tracking information...</Text>
        )}
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#FFFFFF',
  },
  mapContainer: {
    flex: 2,
  },
  map: {
    flex: 1,
  },
  infoContainer: {
    flex: 1,
  },
  info: {
    flex: 1,
  },
  loading: {
    padding: 20,
    textAlign: 'center',
    color: '#666',
  },
});

export default FullTrackingScreen;

Custom Tracking Dashboard

import React, { useEffect } from 'react';
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  ScrollView,
} from 'react-native';
import { useTrackingSession } from '@trackthatride/react-native-map';

function TrackingDashboard({ trackingCode }) {
  const {
    state,
    connect,
    disconnect,
    refresh,
    error,
    connectionStatus,
    isBackground,
  } = useTrackingSession({
    baseUrl: 'https://your-app.replit.app',
  });

  useEffect(() => {
    connect(trackingCode);
    return () => disconnect();
  }, [trackingCode]);

  const handleRefresh = async () => {
    try {
      await refresh();
    } catch (err) {
      Alert.alert('Refresh Failed', err.message);
    }
  };

  if (error) {
    return (
      <View style={styles.error}>
        <Text style={styles.errorText}>Error: {error.message}</Text>
        <TouchableOpacity onPress={handleRefresh} style={styles.button}>
          <Text style={styles.buttonText}>Retry</Text>
        </TouchableOpacity>
      </View>
    );
  }

  if (!state?.ride) {
    return (
      <View style={styles.loading}>
        <Text>Loading...</Text>
      </View>
    );
  }

  const { ride, driver, estimatedArrival } = state;

  return (
    <ScrollView style={styles.container}>
      {/* Connection Status */}
      <View style={styles.statusBar}>
        <View style={[
          styles.statusDot,
          { backgroundColor: getStatusColor(connectionStatus) }
        ]} />
        <Text style={styles.statusText}>
          {connectionStatus} {isBackground && '(background)'}
        </Text>
      </View>

      {/* Tracking Code */}
      <View style={styles.card}>
        <Text style={styles.title}>{ride.tracking_code}</Text>
        <View style={[
          styles.badge,
          { backgroundColor: getRideStatusColor(ride.status) }
        ]}>
          <Text style={styles.badgeText}>{ride.status.toUpperCase()}</Text>
        </View>
      </View>

      {/* Customer Info */}
      <View style={styles.card}>
        <Text style={styles.cardTitle}>Customer</Text>
        <Text style={styles.cardText}>{ride.customer_name}</Text>
        <Text style={styles.cardSubtext}>{ride.phone_number}</Text>
      </View>

      {/* Addresses */}
      <View style={styles.card}>
        <Text style={styles.cardTitle}>Pickup</Text>
        <Text style={styles.cardText}>{ride.pickup_address}</Text>
        
        <Text style={[styles.cardTitle, styles.marginTop]}>Delivery</Text>
        <Text style={styles.cardText}>{ride.delivery_address}</Text>
      </View>

      {/* Driver Info */}
      {driver && (
        <View style={styles.card}>
          <Text style={styles.cardTitle}>Driver</Text>
          <Text style={styles.cardText}>
            {driver.firstName} {driver.lastName}
          </Text>
          {driver.mobileNumber && (
            <Text style={styles.cardSubtext}>{driver.mobileNumber}</Text>
          )}
          {driver.vehicleType && driver.vehiclePlate && (
            <Text style={styles.cardSubtext}>
              {driver.vehicleType}{driver.vehiclePlate}
            </Text>
          )}
        </View>
      )}

      {/* ETA */}
      {estimatedArrival && (
        <View style={styles.card}>
          <Text style={styles.cardTitle}>Estimated Arrival</Text>
          <Text style={styles.etaText}>
            {formatETA(estimatedArrival)}
          </Text>
        </View>
      )}

      {/* Refresh Button */}
      <TouchableOpacity onPress={handleRefresh} style={styles.refreshButton}>
        <Text style={styles.refreshButtonText}>Refresh</Text>
      </TouchableOpacity>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#F5F5F5',
  },
  statusBar: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 12,
    backgroundColor: '#FFFFFF',
    borderBottomWidth: 1,
    borderBottomColor: '#E0E0E0',
  },
  statusDot: {
    width: 10,
    height: 10,
    borderRadius: 5,
    marginRight: 8,
  },
  statusText: {
    fontSize: 12,
    color: '#666',
  },
  card: {
    backgroundColor: '#FFFFFF',
    padding: 16,
    marginTop: 12,
    marginHorizontal: 12,
    borderRadius: 8,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 8,
  },
  badge: {
    alignSelf: 'flex-start',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 16,
    marginTop: 8,
  },
  badgeText: {
    fontSize: 12,
    fontWeight: '600',
    color: '#FFFFFF',
  },
  cardTitle: {
    fontSize: 16,
    fontWeight: '600',
    color: '#333',
    marginBottom: 8,
  },
  cardText: {
    fontSize: 16,
    color: '#333',
  },
  cardSubtext: {
    fontSize: 14,
    color: '#666',
    marginTop: 4,
  },
  marginTop: {
    marginTop: 16,
  },
  etaText: {
    fontSize: 20,
    fontWeight: '600',
    color: '#007AFF',
  },
  refreshButton: {
    backgroundColor: '#007AFF',
    padding: 16,
    margin: 12,
    borderRadius: 8,
    alignItems: 'center',
  },
  refreshButtonText: {
    color: '#FFFFFF',
    fontSize: 16,
    fontWeight: '600',
  },
  error: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
  },
  errorText: {
    fontSize: 16,
    color: '#FF3B30',
    marginBottom: 20,
    textAlign: 'center',
  },
  loading: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

const getStatusColor = (status) => {
  const colors = {
    connected: '#34C759',
    connecting: '#FF9500',
    reconnecting: '#FF9500',
    disconnected: '#FF3B30',
  };
  return colors[status] || '#999';
};

const getRideStatusColor = (status) => {
  const colors = {
    pending: '#FF9500',
    assigned: '#007AFF',
    in_transit: '#34C759',
    delivered: '#34C759',
    cancelled: '#FF3B30',
  };
  return colors[status] || '#999';
};

const formatETA = (eta) => {
  const minutes = Math.ceil((new Date(eta) - new Date()) / 60000);
  if (minutes <= 0) return 'Arrived';
  if (minutes < 60) return `${minutes} min`;
  return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
};

export default TrackingDashboard;

Tracking with Navigation

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import TrackingListScreen from './screens/TrackingListScreen';
import TrackingDetailScreen from './screens/TrackingDetailScreen';

const Stack = createStackNavigator();

function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="TrackingList"
          component={TrackingListScreen}
          options={{ title: 'My Deliveries' }}
        />
        <Stack.Screen
          name="TrackingDetail"
          component={TrackingDetailScreen}
          options={{ title: 'Delivery Tracking' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

// TrackingListScreen.tsx
function TrackingListScreen({ navigation }) {
  const trackingCodes = ['TR123456789', 'TR987654321', 'TR555555555'];

  return (
    <ScrollView>
      {trackingCodes.map(code => (
        <TouchableOpacity
          key={code}
          style={styles.listItem}
          onPress={() => navigation.navigate('TrackingDetail', { trackingCode: code })}
        >
          <Text style={styles.listItemText}>{code}</Text>
        </TouchableOpacity>
      ))}
    </ScrollView>
  );
}

// TrackingDetailScreen.tsx
function TrackingDetailScreen({ route }) {
  const { trackingCode } = route.params;
  const [state, setState] = useState(null);

  return (
    <View style={{ flex: 1 }}>
      <TrackingMap
        trackingCode={trackingCode}
        onStateChange={setState}
        style={{ flex: 1 }}
      />
      {state && <TrackingInfo state={state} />}
    </View>
  );
}

Mobile-Specific Features

Background Mode Support

The SDK automatically optimizes for battery life when the app is in the background:

  • Foreground: Real-time SSE updates
  • Background: Polling every 30 seconds
  • Automatic switching: Handles AppState changes
// Automatic background handling
const { isBackground } = useTrackingSession({
  baseUrl: 'https://your-app.replit.app',
});

// Show UI indicator
{isBackground && (
  <Text>Background Mode (reduced updates)</Text>
)}

Location Permissions

Request location permissions for enhanced tracking:

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

async function requestLocationPermission() {
  if (Platform.OS === 'android') {
    const granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
    );
    return granted === PermissionsAndroid.RESULTS.GRANTED;
  }
  return true; // iOS permissions handled via Info.plist
}

// Use in component
useEffect(() => {
  requestLocationPermission().then(granted => {
    if (granted) {
      setShowsUserLocation(true);
    }
  });
}, []);

Deep Linking

Support deep links for tracking codes:

// App.tsx
import { Linking } from 'react-native';

useEffect(() => {
  const handleDeepLink = ({ url }) => {
    // trackme://track/TR123456789
    const trackingCode = url.split('/').pop();
    navigation.navigate('TrackingDetail', { trackingCode });
  };

  Linking.addEventListener('url', handleDeepLink);
  
  // Handle app launched via deep link
  Linking.getInitialURL().then(url => {
    if (url) {
      handleDeepLink({ url });
    }
  });

  return () => {
    Linking.removeEventListener('url', handleDeepLink);
  };
}, []);

Push Notifications

Integrate with push notifications for tracking updates:

import messaging from '@react-native-firebase/messaging';

useEffect(() => {
  const unsubscribe = messaging().onMessage(async remoteMessage => {
    if (remoteMessage.data?.trackingCode) {
      // Update tracking display
      await refresh();
    }
  });

  return unsubscribe;
}, []);

Performance Optimization

Memoization

import { useMemo, useCallback } from 'react';

function TrackingScreen() {
  const handleStateChange = useCallback((state) => {
    setTrackingState(state);
  }, []);

  const markers = useMemo(() => ({
    pickup: {
      image: require('./assets/pickup.png'),
      title: 'Pickup',
    },
    delivery: {
      image: require('./assets/delivery.png'),
      title: 'Delivery',
    },
  }), []);

  return (
    <TrackingMap
      trackingCode="TR123456789"
      onStateChange={handleStateChange}
      markers={markers}
    />
  );
}

Image Optimization

// Use optimized marker images
const markers = {
  pickup: {
    image: require('./assets/markers/pickup@2x.png'),
  },
  delivery: {
    image: require('./assets/markers/delivery@2x.png'),
  },
};

Lazy Loading

import React, { lazy, Suspense } from 'react';

const TrackingMap = lazy(() => import('@trackthatride/react-native-map'));

function App() {
  return (
    <Suspense fallback={<ActivityIndicator />}>
      <TrackingMap trackingCode="TR123456789" />
    </Suspense>
  );
}

Best Practices

  1. Handle Platform Differences: Use Platform.OS for platform-specific code.

  2. Request Permissions: Always request location permissions before using location features.

  3. Implement Error Boundaries: Wrap tracking components in error boundaries.

  4. Optimize for Battery: The hook handles this automatically, but be aware of background behavior.

  5. Test on Real Devices: Always test location and map features on physical devices.

  6. Handle Network Issues: Implement proper error handling for offline scenarios.

  7. Use SafeAreaView: Always wrap in SafeAreaView for notch/island devices.

Troubleshooting

Map Not Displaying

iOS:

  • Verify CocoaPods are installed
  • Check Info.plist has location permissions
  • For Google Maps, verify API key is configured

Android:

  • Check Google Maps API key in AndroidManifest.xml
  • Verify permissions are declared
  • Ensure Google Play Services is available

Location Not Working

  • Check permissions in device settings
  • Verify Info.plist (iOS) / AndroidManifest.xml (Android) configuration
  • Test on physical device (simulator location may be unreliable)

Background Updates Not Working

  • Verify background modes are enabled (iOS)
  • Check battery optimization settings (Android)
  • Review logs for background task errors

Memory Issues

  • Ensure destroy() is called or hook cleanup runs
  • Avoid creating multiple sessions simultaneously
  • Check for memory leaks with React DevTools Profiler

Platform-Specific Notes

iOS

  • Requires location permissions in Info.plist
  • Background location requires additional capabilities
  • Simulator may not accurately reflect real device behavior
  • MapKit is used by default (free), Google Maps is optional

Android

  • Requires Google Maps API key
  • Uses Google Maps by default
  • Battery optimization may affect background updates
  • Test on multiple Android versions (API 21+)

TypeScript Support

Full TypeScript support with all React Native types:

import {
  TrackingMap,
  TrackingInfo,
  useTrackingSession,
  TrackingMapProps,
  TrackingInfoProps,
  UseTrackingSessionResult,
  TrackingSessionState,
} from '@trackthatride/react-native-map';

// All types from react-native-maps are also available
import type { Region, LatLng, MapViewProps } from 'react-native-maps';

Testing

Unit Tests

import { renderHook } from '@testing-library/react-hooks';
import { useTrackingSession } from '@trackthatride/react-native-map';

describe('useTrackingSession', () => {
  it('should connect to tracking code', async () => {
    const { result } = renderHook(() =>
      useTrackingSession({ baseUrl: 'https://test.com' })
    );

    await act(async () => {
      await result.current.connect('TR123456789');
    });

    expect(result.current.isConnected).toBe(true);
  });
});

Integration Tests

import { render, waitFor } from '@testing-library/react-native';
import { TrackingMap } from '@trackthatride/react-native-map';

describe('TrackingMap', () => {
  it('should render map and markers', async () => {
    const { getByTestId } = render(
      <TrackingMap
        trackingCode="TR123456789"
        apiBaseUrl="https://test.com"
      />
    );

    await waitFor(() => {
      expect(getByTestId('tracking-map')).toBeTruthy();
    });
  });
});

License

MIT

Support

For support, please contact Track That Ride support or visit our documentation at /docs on your Track That Ride instance.

Additional Resources