Refactor example app and add location tracking functionality with tests

This commit is contained in:
2026-02-03 23:06:09 +05:30
parent fb56521d84
commit d1e383baf5
9 changed files with 149 additions and 150 deletions

View File

@@ -1,111 +0,0 @@
import React, { useRef, useState } from 'react';
import { SafeAreaView, StyleSheet, Button, View } from 'react-native';
import { MapView, Marker, PropertyMap, CameraPresets } from '@lynkedup/map-sdk';
import type { MapHandle } from '@lynkedup/map-sdk';
export default function App() {
const mapRef = useRef<MapHandle | null>(null);
const [showPropertyMap, setShowPropertyMap] = useState(false);
return (
<SafeAreaView style={styles.container}>
<View style={styles.controls}>
<Button title="Toggle SDK Example" onPress={() => setShowPropertyMap((s) => !s)} />
{!showPropertyMap && (
<>
<View style={{ flexDirection: 'row' }}>
<Button
title="Fly to SF"
onPress={() => mapRef.current?.flyTo({ latitude: 37.78825, longitude: -122.4324 })}
/>
<Button
title="Fit Bounds"
onPress={() =>
mapRef.current?.fitBounds({ latitude: 37.809, longitude: -122.410 }, { latitude: 37.779, longitude: -122.450 })
}
/>
</View>
<View style={{ marginTop: 8, flexDirection: 'row', justifyContent: 'space-between' }}>
<Button
title="Tilt 60°"
onPress={() =>
mapRef.current?.animateCamera(
{ center: { latitude: 37.78825, longitude: -122.4324 }, pitch: 60 },
{ duration: 800 }
)
}
/>
<Button
title="Rotate 90°"
onPress={() =>
mapRef.current?.animateCamera(
{ center: { latitude: 37.78825, longitude: -122.4324 }, heading: 90 },
{ duration: 800 }
)
}
/>
<Button
title="Zoom In"
onPress={() =>
mapRef.current?.animateCamera(
{ center: { latitude: 37.78825, longitude: -122.4324 }, zoom: 15 },
{ duration: 800 }
)
}
/>
</View>
<View style={{ marginTop: 8, flexDirection: 'row', justifyContent: 'space-between' }}>
<Button
title="Overlook"
onPress={() =>
mapRef.current?.animateCamera(
CameraPresets.viewOverlook({ latitude: 37.78825, longitude: -122.4324 }),
{ duration: 1000 }
)
}
/>
<Button
title="Street"
onPress={() =>
mapRef.current?.animateCamera(
CameraPresets.streetLevel({ latitude: 37.78825, longitude: -122.4324 }),
{ duration: 1000 }
)
}
/>
<Button
title="Overview"
onPress={() =>
mapRef.current?.animateCamera(
CameraPresets.overview({ latitude: 37.78825, longitude: -122.4324 }),
{ duration: 1000 }
)
}
/>
</View>
</>
)}
</View>
{showPropertyMap ? (
<PropertyMap apiUrl="http://64.227.108.180:5000/property-search" />
) : (
<MapView
ref={mapRef}
style={styles.map}
initialRegion={{ latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.0922, longitudeDelta: 0.0421 }}
>
<Marker coordinate={{ latitude: 37.78825, longitude: -122.4324 }} />
</MapView>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
controls: { position: 'absolute', top: 40, left: 10, right: 10, zIndex: 1000, flexDirection: 'row', justifyContent: 'space-between' },
map: { flex: 1 }
});

View File

@@ -1,15 +0,0 @@
# Example App
This example demonstrates usage of `@lynkedup/map-sdk`.
Setup:
1. From repo root run `npm run bootstrap` to install workspace packages.
2. From `example` run `npm install` to install native deps.
3. iOS: run `npx pod-install ios` and add your Google Maps API key if you use Google provider.
4. Android: ensure Google Play services and API keys are set in `AndroidManifest.xml` if using Google maps.
Run:
- `npm run ios` or `npm run android` from `example` folder.
See `react-native-maps` docs for platform-specific instructions: https://github.com/react-native-maps/react-native-maps

View File

@@ -1,16 +0,0 @@
{
"name": "example",
"private": true,
"version": "0.0.1",
"scripts": {
"start": "react-native start",
"ios": "react-native run-ios",
"android": "react-native run-android"
},
"dependencies": {
"react": "18.2.0",
"react-native": "0.71.0",
"react-native-maps": "^1.3.2",
"@lynkedup/map-sdk": "*"
}
}

View File

@@ -15,20 +15,24 @@ Basic usage:
```ts ```ts
import React from 'react'; import React from 'react';
import { MapView, Marker } from '@lynkedup/map-sdk'; import { MapView, Marker, useLocationTracking, Polyline } from '@lynkedup/map-sdk';
export default function App() { export default function App() {
const { isTracking, path, startTracking, stopTracking, clear } = useLocationTracking();
return ( return (
<MapView <MapView style={{ flex: 1 }} initialRegion={{ latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.0922, longitudeDelta: 0.0421 }}>
style={{ flex: 1 }}
initialRegion={{ latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.0922, longitudeDelta: 0.0421 }}
>
<Marker coordinate={{ latitude: 37.78825, longitude: -122.4324 }} /> <Marker coordinate={{ latitude: 37.78825, longitude: -122.4324 }} />
{path.length > 1 && <Polyline coordinates={path} strokeWidth={4} strokeColor="#007AFF" />}
</MapView> </MapView>
); );
} }
``` ```
Tracking notes:
- `useLocationTracking()` uses the platform geolocation API (`navigator.geolocation.watchPosition`). Request permissions on iOS/Android as usual before starting tracking.
- It returns `{ isTracking, path, current, startTracking, stopTracking, clear }` so you can display the polyline and follow the user.
## Camera Controls 🔧 ## Camera Controls 🔧
This package exposes a small imperative `MapHandle` API via `ref` on `MapView`. Use it to animate the camera or fit bounds. This package exposes a small imperative `MapHandle` API via `ref` on `MapView`. Use it to animate the camera or fit bounds.

View File

@@ -0,0 +1,13 @@
module.exports = {
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
'^.+\\.(js|jsx)$': 'babel-jest'
},
transformIgnorePatterns: ["node_modules/(?!(react-native|@react-native|@react-native/polyfills|@react-native-maps)/)"],
testEnvironment: 'jsdom',
moduleNameMapper: {
'^react-native$': 'react-native'
},
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
testMatch: ['**/__tests__/**/*.test.(ts|tsx|js)']
};

View File

@@ -9,7 +9,8 @@
], ],
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"lint": "eslint . --ext .ts,.tsx" "lint": "eslint . --ext .ts,.tsx",
"test": "jest --runInBand"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=17", "react": ">=17",
@@ -21,7 +22,18 @@
"devDependencies": { "devDependencies": {
"typescript": "^5.0.0", "typescript": "^5.0.0",
"eslint": "^8.0.0", "eslint": "^8.0.0",
"@types/react": "^18.0.0", "@types/react": "^19.0.0",
"@types/react-native": "^0.70.0" "@types/react-native": "^0.70.0",
"jest": "^29.0.0",
"@types/jest": "^29.0.0",
"babel-jest": "^29.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/react-native": "^12.9.0",
"@testing-library/jest-native": "^5.4.3",
"react-test-renderer": "^19.0.0",
"react": "^19.2.4",
"ts-jest": "^29.0.0",
"babel-jest": "^29.0.0",
"jest-environment-jsdom": "^29.0.0"
} }
} }

View File

@@ -0,0 +1,48 @@
require('react-test-renderer');
const { renderHook, act } = require('@testing-library/react-hooks');
const { useLocationTracking } = require('../useLocationTracking');
describe('useLocationTracking', () => {
let origGeo;
beforeEach(() => {
origGeo = global.navigator && global.navigator.geolocation;
global.navigator = global.navigator || {};
global.navigator.geolocation = {
watchPosition: jest.fn((success) => {
// Immediately call with a sample position
success({ coords: { latitude: 10, longitude: 20 } });
// Return a fake id
return 42;
}),
clearWatch: jest.fn()
};
});
afterEach(() => {
if (origGeo) global.navigator.geolocation = origGeo;
else delete global.navigator.geolocation;
jest.resetAllMocks();
});
it('starts and stops tracking and collects path', () => {
const { result } = renderHook(() => useLocationTracking());
expect(result.current.isTracking).toBe(false);
act(() => result.current.startTracking());
expect(result.current.isTracking).toBe(true);
expect(result.current.path.length).toBeGreaterThanOrEqual(1);
act(() => result.current.stopTracking());
expect(result.current.isTracking).toBe(false);
expect(global.navigator.geolocation.clearWatch).toHaveBeenCalled();
});
it('clears path', () => {
const { result } = renderHook(() => useLocationTracking());
act(() => result.current.startTracking());
expect(result.current.path.length).toBeGreaterThanOrEqual(1);
act(() => result.current.clear());
expect(result.current.path.length).toBe(0);
});
});

View File

@@ -0,0 +1,63 @@
import { useRef, useState, useCallback } from 'react';
import type { LatLng } from 'react-native-maps';
export type TrackingOptions = {
distanceFilter?: number; // meters
interval?: number; // ms
fastestInterval?: number; // ms
accuracy?: 'high' | 'balanced' | 'low';
};
export function useLocationTracking(opts: TrackingOptions = {}) {
const [isTracking, setIsTracking] = useState(false);
const [path, setPath] = useState<LatLng[]>([]);
const [current, setCurrent] = useState<LatLng | null>(null);
const watchIdRef = useRef<number | null>(null);
const onPosition = useCallback((pos: any) => {
const lat = pos.coords?.latitude;
const lng = pos.coords?.longitude;
if (typeof lat === 'number' && typeof lng === 'number') {
const point = { latitude: lat, longitude: lng } as LatLng;
setPath((p) => [...p, point]);
setCurrent(point);
}
}, []);
const startTracking = useCallback(() => {
if (isTracking) return;
if (!global.navigator?.geolocation?.watchPosition) {
console.warn('[useLocationTracking] Geolocation.watchPosition not available.');
return;
}
const id = global.navigator.geolocation.watchPosition(
onPosition,
(err: any) => console.warn('geolocation error', err),
{
enableHighAccuracy: opts.accuracy === 'high',
distanceFilter: opts.distanceFilter ?? 0,
interval: opts.interval
}
);
watchIdRef.current = id as unknown as number;
setIsTracking(true);
}, [isTracking, onPosition, opts.accuracy, opts.distanceFilter, opts.interval]);
const stopTracking = useCallback(() => {
const id = watchIdRef.current;
if (id != null && global.navigator?.geolocation?.clearWatch) {
global.navigator.geolocation.clearWatch(id);
}
watchIdRef.current = null;
setIsTracking(false);
}, []);
const clear = useCallback(() => {
setPath([]);
setCurrent(null);
}, []);
return { isTracking, path, current, startTracking, stopTracking, clear } as const;
}

View File

@@ -2,6 +2,7 @@ export { default as MapView } from './MapView';
export type { MapHandle } from './MapView'; export type { MapHandle } from './MapView';
export { Marker, Polyline, PROVIDER_GOOGLE } from 'react-native-maps'; export { Marker, Polyline, PROVIDER_GOOGLE } from 'react-native-maps';
export { CameraPresets } from './cameraPresets'; export { CameraPresets } from './cameraPresets';
export { useLocationTracking } from './hooks/useLocationTracking';
// High-level components // High-level components
export { default as PropertyMap } from './components/PropertyMap'; export { default as PropertyMap } from './components/PropertyMap';