feat: section 3 — km milestone notifications
- Foreground notification handler in App.tsx (iOS shows banners while active) - requestNotificationPermissions() called on app mount - GPS task tracks running distance per recording session (module-level state) - Fires immediate notification at each km crossed, gated on kmNotifications setting
This commit is contained in:
@@ -1,8 +1,25 @@
|
||||
import { useEffect } from 'react';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { AppNavigator } from './src/navigation/AppNavigator';
|
||||
import { requestNotificationPermissions } from './src/services/gps';
|
||||
|
||||
// Show notifications even when the app is in the foreground (iOS suppresses by default)
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowBanner: true,
|
||||
shouldShowList: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: false,
|
||||
}),
|
||||
});
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
requestNotificationPermissions();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<StatusBar style="light" />
|
||||
|
||||
@@ -112,10 +112,10 @@ Items below are what remains before v1 is shippable.
|
||||
- [x] **Cadence CSC parsing** — stateful parsing in `parseCscMeasurement()`: tracks previous crank revolution count + event time (uint16, 1/1024 s), computes RPM from delta with uint16 rollover handling
|
||||
- [x] **BLE persistence** — `savePairedDevice` / `loadPairedDevices` / `removePairedDevice` in `ble.ts` via AsyncStorage; SensorPairingScreen shows saved sensors at top, auto-attempts reconnect on mount, Forget button removes a device
|
||||
|
||||
### 3 — Km notifications (half day)
|
||||
### 3 — Km notifications ✅
|
||||
|
||||
- [ ] **Permission request** — call `Notifications.requestPermissionsAsync()` on first launch
|
||||
- [ ] **Milestone tracker** — track last notified km in the GPS background task (or store); fire a notification each time `distanceMeters` crosses a new km boundary, gated on the `kmNotifications` setting from AsyncStorage
|
||||
- [x] **Permission request** — `requestNotificationPermissions()` called in `App.tsx` on mount; foreground notification handler set at module level so iOS shows banners while app is active
|
||||
- [x] **Milestone tracker** — module-level `runningDistanceMeters` / `lastNotifiedKm` / `prevPoint` in `gps.ts`, reset on each `startGpsRecording()`; each incoming location adds haversine distance and fires a notification when a new km is crossed, gated on the `kmNotifications` AsyncStorage setting
|
||||
|
||||
### 4 — Android battery optimization prompt (1–2 hours)
|
||||
|
||||
|
||||
+55
-2
@@ -1,9 +1,16 @@
|
||||
import * as Location from 'expo-location';
|
||||
import * as TaskManager from 'expo-task-manager';
|
||||
import * as Notifications from 'expo-notifications';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { useRecordingStore } from '../store/recording';
|
||||
|
||||
const BACKGROUND_LOCATION_TASK = 'background-location-task';
|
||||
|
||||
// Module-level km milestone state — reset each recording
|
||||
let runningDistanceMeters = 0;
|
||||
let lastNotifiedKm = 0;
|
||||
let prevPoint: { lat: number; lon: number } | null = null;
|
||||
|
||||
TaskManager.defineTask(BACKGROUND_LOCATION_TASK, async ({ data, error }: any) => {
|
||||
if (error) return;
|
||||
const locations: Location.LocationObject[] = data?.locations ?? [];
|
||||
@@ -11,7 +18,7 @@ TaskManager.defineTask(BACKGROUND_LOCATION_TASK, async ({ data, error }: any) =>
|
||||
if (status !== 'recording') return;
|
||||
|
||||
for (const loc of locations) {
|
||||
addTrackPoint({
|
||||
const point = {
|
||||
lat: loc.coords.latitude,
|
||||
lon: loc.coords.longitude,
|
||||
ele: loc.coords.altitude ?? 0,
|
||||
@@ -19,10 +26,35 @@ TaskManager.defineTask(BACKGROUND_LOCATION_TASK, async ({ data, error }: any) =>
|
||||
hr: ble.hr,
|
||||
power: ble.power,
|
||||
cad: ble.cadence,
|
||||
});
|
||||
};
|
||||
addTrackPoint(point);
|
||||
|
||||
if (prevPoint) {
|
||||
runningDistanceMeters += haversineMeters(prevPoint, point);
|
||||
await maybeNotifyKm(runningDistanceMeters);
|
||||
}
|
||||
prevPoint = point;
|
||||
}
|
||||
});
|
||||
|
||||
async function maybeNotifyKm(distanceMeters: number): Promise<void> {
|
||||
const kmSetting = await AsyncStorage.getItem('kmNotifications');
|
||||
if (kmSetting === 'false') return;
|
||||
|
||||
const crossedKm = Math.floor(distanceMeters / 1000);
|
||||
if (crossedKm <= lastNotifiedKm) return;
|
||||
lastNotifiedKm = crossedKm;
|
||||
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: `${crossedKm} km`,
|
||||
body: `You've covered ${crossedKm} km!`,
|
||||
sound: true,
|
||||
},
|
||||
trigger: null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function requestLocationPermissions(): Promise<boolean> {
|
||||
const { status: fg } = await Location.requestForegroundPermissionsAsync();
|
||||
if (fg !== 'granted') return false;
|
||||
@@ -31,6 +63,11 @@ export async function requestLocationPermissions(): Promise<boolean> {
|
||||
}
|
||||
|
||||
export async function startGpsRecording(): Promise<void> {
|
||||
// Reset km tracking for this recording session
|
||||
runningDistanceMeters = 0;
|
||||
lastNotifiedKm = 0;
|
||||
prevPoint = null;
|
||||
|
||||
await Location.startLocationUpdatesAsync(BACKGROUND_LOCATION_TASK, {
|
||||
accuracy: Location.Accuracy.BestForNavigation,
|
||||
timeInterval: 1000,
|
||||
@@ -47,3 +84,19 @@ export async function stopGpsRecording(): Promise<void> {
|
||||
const isRunning = await Location.hasStartedLocationUpdatesAsync(BACKGROUND_LOCATION_TASK);
|
||||
if (isRunning) await Location.stopLocationUpdatesAsync(BACKGROUND_LOCATION_TASK);
|
||||
}
|
||||
|
||||
export async function requestNotificationPermissions(): Promise<void> {
|
||||
await Notifications.requestPermissionsAsync();
|
||||
}
|
||||
|
||||
function haversineMeters(a: { lat: number; lon: number }, b: { lat: number; lon: number }): number {
|
||||
const R = 6371000;
|
||||
const dLat = toRad(b.lat - a.lat);
|
||||
const dLon = toRad(b.lon - a.lon);
|
||||
const sinLat = Math.sin(dLat / 2);
|
||||
const sinLon = Math.sin(dLon / 2);
|
||||
const c = sinLat * sinLat + Math.cos(toRad(a.lat)) * Math.cos(toRad(b.lat)) * sinLon * sinLon;
|
||||
return R * 2 * Math.atan2(Math.sqrt(c), Math.sqrt(1 - c));
|
||||
}
|
||||
|
||||
const toRad = (deg: number) => (deg * Math.PI) / 180;
|
||||
|
||||
Reference in New Issue
Block a user