Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/mobile/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
const [selectedLocationId, setSelectedLocationId] = useState<string | null>(null);
const [selectedDetails, setSelectedDetails] = useState<LocationSheetDetails | null>(null);
const [isDetailsLoading, setIsDetailsLoading] = useState(false);
const [detailsRefreshKey, setDetailsRefreshKey] = useState(0);

const customMapStyle = useMemo(
() => (colorScheme === "dark" ? darkMapStyle : lightMapStyle),
Expand Down Expand Up @@ -91,7 +92,7 @@
clearTimeout(debounceTimer.current);
}
};
}, [setBoundingBox]);

Check warning on line 95 in apps/mobile/app/index.tsx

View workflow job for this annotation

GitHub Actions / Mobile Checks

React Hook useEffect has a missing dependency: 'initialRegion'. Either include it or remove the dependency array

useEffect(() => {
if (!location || hasCenteredOnUser.current) {
Expand Down Expand Up @@ -165,7 +166,7 @@
return () => {
cancelled = true;
};
}, [locationsById, selectedLocationId]);
}, [detailsRefreshKey, locationsById, selectedLocationId]);
return (
<View style={styles.container}>
<MapView
Expand Down Expand Up @@ -280,6 +281,9 @@
visible={Boolean(selectedLocationId)}
loading={isDetailsLoading}
details={selectedDetails}
onReportSubmitted={() => {
setDetailsRefreshKey((current) => current + 1);
}}
onDismiss={() => {
setSelectedLocationId(null);
}}
Expand Down
115 changes: 113 additions & 2 deletions apps/mobile/src/map/LocationBottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { useEffect, useMemo, useRef } from "react";
import { Dimensions, PanResponder, StyleSheet, Text, View } from "react-native";
import { useEffect, useMemo, useRef, useState } from "react";
import {
ActivityIndicator,

Check warning on line 3 in apps/mobile/src/map/LocationBottomSheet.tsx

View workflow job for this annotation

GitHub Actions / Mobile Checks

'ActivityIndicator' is defined but never used
Dimensions,
PanResponder,
Pressable,

Check warning on line 6 in apps/mobile/src/map/LocationBottomSheet.tsx

View workflow job for this annotation

GitHub Actions / Mobile Checks

'Pressable' is defined but never used
StyleSheet,
Text,
TextInput,

Check warning on line 9 in apps/mobile/src/map/LocationBottomSheet.tsx

View workflow job for this annotation

GitHub Actions / Mobile Checks

'TextInput' is defined but never used
View,
} from "react-native";
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
import { apiClient } from "@/src/network/apiClient";
import { getStoredSessionTokens } from "@/src/auth/secureTokens";

export type LocationSheetDetails = {
id: string;
Expand All @@ -23,8 +34,12 @@
loading: boolean;
details: LocationSheetDetails | null;
onDismiss: () => void;
onReportSubmitted?: () => void;
};

const REPORT_LEVELS = ["none", "low", "medium", "high", "unknown"] as const;

Check warning on line 40 in apps/mobile/src/map/LocationBottomSheet.tsx

View workflow job for this annotation

GitHub Actions / Mobile Checks

'REPORT_LEVELS' is assigned a value but only used as a type
type ReportLevel = (typeof REPORT_LEVELS)[number];

const SCREEN_HEIGHT = Dimensions.get("window").height;
const SHEET_HEIGHT = Math.max(Math.round(SCREEN_HEIGHT * 0.9), 420);
const SNAP_90 = 0;
Expand Down Expand Up @@ -52,10 +67,21 @@
loading,
details,
onDismiss,
onReportSubmitted,
}: LocationBottomSheetProps) {
const translateY = useSharedValue(CLOSE_POSITION);
const startYRef = useRef(SNAP_50);
const isClosingRef = useRef(false);
const [waitTimeMinutes, setWaitTimeMinutes] = useState("");
const [level, setLevel] = useState<ReportLevel>("medium");
const [notes, setNotes] = useState("");
const [reportState, setReportState] = useState<{

Check warning on line 78 in apps/mobile/src/map/LocationBottomSheet.tsx

View workflow job for this annotation

GitHub Actions / Mobile Checks

'reportState' is assigned a value but never used
status: "idle" | "submitting" | "success" | "error";
message: string;
}>({
status: "idle",
message: "",
});

useEffect(() => {
if (visible) {
Expand All @@ -68,6 +94,20 @@
translateY.value = withTiming(CLOSE_POSITION, { duration: 180 });
}, [translateY, visible]);

useEffect(() => {
if (!visible || !details?.id) {
return;
}

setWaitTimeMinutes("");
setLevel("medium");
setNotes("");
setReportState({
status: "idle",
message: "",
});
}, [details?.id, visible]);

const panResponder = useMemo(
() =>
PanResponder.create({
Expand Down Expand Up @@ -110,6 +150,77 @@
return null;
}

const handleSubmitReport = async () => {

Check warning on line 153 in apps/mobile/src/map/LocationBottomSheet.tsx

View workflow job for this annotation

GitHub Actions / Mobile Checks

'handleSubmitReport' is assigned a value but never used
if (!details?.id) {
return;
}

const stored = await getStoredSessionTokens();
if (!stored.accessToken) {
setReportState({
status: "error",
message: "Sign in support has not been configured on this build yet.",
});
return;
}

const parsedWait = waitTimeMinutes.trim() ? Number(waitTimeMinutes.trim()) : undefined;
if (parsedWait !== undefined && (!Number.isFinite(parsedWait) || parsedWait < 0)) {
setReportState({
status: "error",
message: "Wait time must be a valid positive number.",
});
return;
}

setReportState({
status: "submitting",
message: "",
});

try {
await apiClient.post(
"/queues/report",
{
locationId: details.id,
waitTimeMinutes: parsedWait,
level,
notes: notes.trim() || undefined,
},
{
headers: {
Authorization: `Bearer ${stored.accessToken}`,
...(stored.deviceId ? { "x-device-id": stored.deviceId } : {}),
},
}
);

setReportState({
status: "success",
message: "Queue report submitted.",
});
onReportSubmitted?.();
} catch (error) {
const status = typeof error === "object" && error && "response" in error
? (error as { response?: { status?: number } }).response?.status
: undefined;

const message =
status === 401
? "Your session is not authorized for reporting yet."
: status === 404
? "Reporting endpoint is not available on this backend yet."
: error instanceof Error
? error.message
: "Unable to submit queue report right now.";

setReportState({
status: "error",
message,
});
}
};

return (
<Animated.View style={[styles.container, animatedStyle]} {...panResponder.panHandlers}>
<View style={styles.handle} />
Expand Down
Loading