diff --git a/jest-setup.ts b/jest-setup.ts
index 419cfea..b6a8e2a 100644
--- a/jest-setup.ts
+++ b/jest-setup.ts
@@ -788,6 +788,32 @@ jest.mock('expo-asset', () => ({
},
}));
+// Mock @sentry/react-native to prevent native module errors in tests
+jest.mock('@sentry/react-native', () => {
+ const breadcrumbs: any[] = [];
+
+ return {
+ __esModule: true,
+ init: jest.fn().mockResolvedValue(undefined),
+ wrap: jest.fn((component: any) => component),
+ captureException: jest.fn(),
+ captureMessage: jest.fn(),
+ addBreadcrumb: jest.fn().mockImplementation((breadcrumb: any) => {
+ breadcrumbs.push(breadcrumb);
+ }),
+ reactNavigationIntegration: jest.fn(() => ({
+ registerNavigationContainer: jest.fn(),
+ })),
+ ErrorBoundary: ({ children }: any) => children,
+ setUser: jest.fn(),
+ setTag: jest.fn(),
+ setExtra: jest.fn(),
+ isInitialized: jest.fn().mockReturnValue(true),
+ // Re-export breadcrumbs for test assertions
+ _breadcrumbs: breadcrumbs,
+ };
+});
+
// Mock expo-av to avoid import issues
jest.mock('expo-av', () => ({
Audio: {
diff --git a/package.json b/package.json
index ceecab3..d17815e 100644
--- a/package.json
+++ b/package.json
@@ -88,14 +88,14 @@
"@react-native-community/netinfo": "11.4.1",
"@rnmapbox/maps": "10.2.10",
"@semantic-release/git": "^10.0.1",
- "@sentry/react-native": "~7.2.0",
+ "@sentry/react-native": "~8.10.0",
"@shopify/flash-list": "2.0.2",
"@tanstack/react-query": "~5.52.1",
"app-icon-badge": "^0.1.2",
"axios": "^1.12.0",
"babel-plugin-module-resolver": "^5.0.2",
"buffer": "^6.0.3",
- "countly-sdk-react-native-bridge": "^25.4.0",
+ "countly-sdk-react-native-bridge": "^25.4.1",
"crypto-js": "^4.2.0",
"date-fns": "^4.1.0",
"expo": "^54.0.33",
diff --git a/src/app/(app)/__tests__/map.test.tsx b/src/app/(app)/__tests__/map.test.tsx
index 108c4c3..a0b1265 100644
--- a/src/app/(app)/__tests__/map.test.tsx
+++ b/src/app/(app)/__tests__/map.test.tsx
@@ -353,7 +353,7 @@ describe('HomeMap', () => {
expect(screen.getByTestId('map-camera')).toBeTruthy();
});
- it('shows side menu in landscape mode', async () => {
+ it('renders in landscape mode', async () => {
// Mock landscape dimensions
const mockUseWindowDimensions = (jest.requireMock('react-native') as any).useWindowDimensions;
mockUseWindowDimensions.mockReturnValue({
@@ -368,11 +368,12 @@ describe('HomeMap', () => {
expect(screen.getByTestId('map-pins')).toBeTruthy();
});
- // In landscape mode, side menu should be permanently visible
- expect(screen.getByTestId('side-menu')).toBeTruthy();
+ // In landscape mode, the map container should still render correctly
+ // (the side menu is now handled by the parent _layout.tsx, not the map page itself)
+ expect(screen.getByTestId('home-map-container')).toBeTruthy();
});
- it('shows drawer in portrait mode when opened', async () => {
+ it('does not render its own drawer in portrait mode', async () => {
render();
// Wait for async map data to load
@@ -380,11 +381,8 @@ describe('HomeMap', () => {
expect(screen.getByTestId('map-pins')).toBeTruthy();
});
- // Initially drawer should not be visible
+ // The drawer is now handled by the parent _layout.tsx, not the map page itself
expect(screen.queryByTestId('drawer')).toBeNull();
-
- // Since there's no header menu button, we can't test opening the drawer
- // This test would need to be modified based on how the drawer is actually opened
});
it('shows recenter button when user has moved map and location is available', async () => {
@@ -548,8 +546,9 @@ describe('HomeMap', () => {
expect(screen.getByTestId('map-pins')).toBeTruthy();
});
- // In landscape mode, side menu should be permanently visible
- expect(screen.getByTestId('side-menu')).toBeTruthy();
+ // In landscape mode, the map container should still render correctly
+ // (side menu is handled by the parent _layout.tsx)
+ expect(screen.getByTestId('home-map-container')).toBeTruthy();
});
describe('Analytics Tracking', () => {
diff --git a/src/app/(app)/map.tsx b/src/app/(app)/map.tsx
index c4b64e7..bb9fa47 100644
--- a/src/app/(app)/map.tsx
+++ b/src/app/(app)/map.tsx
@@ -1,14 +1,10 @@
import { useLocalSearchParams, useRouter } from 'expo-router';
-import { Menu } from 'lucide-react-native';
-import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import { TouchableOpacity, useWindowDimensions, View } from 'react-native';
+import { useWindowDimensions, View } from 'react-native';
import MapPanel from '@/components/maps/map-panel';
import PoiListPanel from '@/components/maps/poi-list-panel';
-import { SideMenu } from '@/components/sidebar/side-menu';
-import { Button, ButtonText } from '@/components/ui/button';
-import { Drawer, DrawerBackdrop, DrawerBody, DrawerContent, DrawerFooter } from '@/components/ui/drawer/index';
import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar';
import { SharedTabs, type TabItem } from '@/components/ui/shared-tabs';
import { type PoiResultData } from '@/models/v4/mapping/poiResultData';
@@ -20,7 +16,6 @@ export default function HomeMap() {
const { tab, poiId } = useLocalSearchParams<{ tab?: string | string[]; poiId?: string | string[] }>();
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
- const [isSideMenuOpen, setIsSideMenuOpen] = useState(false);
const fetchPoisData = usePoiStore((state) => state.fetchPoisData);
useEffect(() => {
@@ -80,59 +75,21 @@ export default function HomeMap() {
- {/* Content Area with Side Menu */}
-
- {/* Permanent Side Menu in Landscape */}
- {isLandscape && (
-
-
-
- )}
-
- {/* Map Content */}
-
- {/* Portrait menu button */}
- {!isLandscape && (
- setIsSideMenuOpen(true)}
- >
-
-
- )}
-
-
+ {/* Map Content */}
+
+
-
- {/* Drawer for Portrait Mode */}
- {!isLandscape && (
- setIsSideMenuOpen(false)} size="lg">
- setIsSideMenuOpen(false)} />
-
-
- setIsSideMenuOpen(false)} />
-
-
-
-
-
-
- )}
>
);
}
diff --git a/src/app/(app)/messages.tsx b/src/app/(app)/messages.tsx
index 25ef31a..b4dc09e 100644
--- a/src/app/(app)/messages.tsx
+++ b/src/app/(app)/messages.tsx
@@ -1,22 +1,20 @@
import { useFocusEffect } from '@react-navigation/native';
import { router, Stack } from 'expo-router';
-import { ChevronDown, Mail, MailOpen, Menu, MessageSquarePlus, MoreVertical, Search, Trash2, X } from 'lucide-react-native';
+import { ChevronDown, Mail, MailOpen, MessageSquarePlus, MoreVertical, Search, Trash2, X } from 'lucide-react-native';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { Alert, useWindowDimensions } from 'react-native';
+import { Alert } from 'react-native';
import { Loading } from '@/components/common/loading';
import ZeroState from '@/components/common/zero-state';
import { ComposeMessageSheet } from '@/components/messages/compose-message-sheet';
import { MessageCard } from '@/components/messages/message-card';
import { MessageDetailsSheet } from '@/components/messages/message-details-sheet';
-import { SideMenu } from '@/components/sidebar/side-menu';
import { FocusAwareStatusBar, View } from '@/components/ui';
import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetItem, ActionsheetItemText } from '@/components/ui/actionsheet';
import { Badge } from '@/components/ui/badge';
import { Button, ButtonText } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
-import { Drawer, DrawerBackdrop, DrawerBody, DrawerContent, DrawerFooter } from '@/components/ui/drawer/index';
import { Fab, FabIcon } from '@/components/ui/fab';
import { FlatList } from '@/components/ui/flat-list';
import { HStack } from '@/components/ui/hstack';
@@ -32,11 +30,8 @@ import { useSecurityStore } from '@/stores/security/store';
export default function MessagesScreen() {
const { t } = useTranslation();
- const { width, height } = useWindowDimensions();
- const isLandscape = width > height;
const [isFilterMenuOpen, setIsFilterMenuOpen] = useState(false);
const [isSelectionMode, setIsSelectionMode] = useState(false);
- const [isSideMenuOpen, setIsSideMenuOpen] = useState(false);
const { canUserCreateMessages } = useSecurityStore();
const { trackEvent } = useAnalytics();
@@ -211,13 +206,6 @@ export default function MessagesScreen() {
- !isLandscape ? (
- setIsSideMenuOpen(true)} testID="messages-menu-button">
-
-
- ) : null,
- headerRight: () => null,
}}
/>
@@ -331,16 +319,6 @@ export default function MessagesScreen() {
{isLoading && filteredMessages.length === 0 && }
- {/* Side Menu Drawer */}
- setIsSideMenuOpen(false)} size={isLandscape ? 'md' : 'lg'}>
- setIsSideMenuOpen(false)} />
-
-
- setIsSideMenuOpen(false)} />
-
-
-
-
{/* Filter Action Sheet */}
setIsFilterMenuOpen(false)}>
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index 5c8c091..44c7d2a 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -55,21 +55,33 @@ const navigationIntegration = Sentry.reactNavigationIntegration({
enableTimeToInitialDisplay: false,
});
+// Validate DSN is configured — warn early if missing
+if (!Env.SENTRY_DSN) {
+ console.warn('[Sentry] DSN is empty — errors and profiles will NOT be sent. Set RESPOND_SENTRY_DSN in your environment.');
+}
+
Sentry.init({
dsn: Env.SENTRY_DSN,
debug: __DEV__, // Only debug in development, not production
- tracesSampleRate: __DEV__ ? 1.0 : 0.2, // 100% in dev, 20% in production to reduce performance impact
- profilesSampleRate: __DEV__ ? 1.0 : 0.2, // 100% in dev, 20% in production to reduce performance impact
+ // tracesSampleRate: percentage of transactions sent (1.0 = 100%, 0.2 = 20%)
+ tracesSampleRate: __DEV__ ? 1.0 : 0.2,
+ // profilesSampleRate: relative to tracesSampleRate!
+ // Effective profile rate = tracesSampleRate * profilesSampleRate.
+ // In production: 0.2 * 1.0 = 20% of all transactions get profiles.
+ // Set to 1.0 to profile ALL sampled transactions, or lower to further sub-sample.
+ profilesSampleRate: __DEV__ ? 1.0 : 1.0,
sendDefaultPii: false,
+ enableAppHangTracking: true, // v8: Tracks app freeze/hang events
+ enableWatchdogTerminationTracking: true, // v8: Tracks iOS watchdog terminations
integrations: [
// Pass integration
navigationIntegration,
],
- enableNativeFramesTracking: true, //!isRunningInExpoGo(), // Tracks slow and frozen frames in the application
+ enableNativeFramesTracking: true, // Tracks slow and frozen frames
// Add additional options to prevent timing issues
- beforeSendTransaction(event: any) {
+ beforeSend(event) {
// Filter out problematic navigation transactions that might cause timestamp errors
- if (event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) {
+ if (event.type === 'transaction' && event.contexts?.trace?.op === 'navigation' && !event.contexts?.trace?.data?.route) {
return null;
}
return event;
diff --git a/src/app/call/[id]/index.tsx b/src/app/call/[id]/index.tsx
index 178bf9b..bf42735 100644
--- a/src/app/call/[id]/index.tsx
+++ b/src/app/call/[id]/index.tsx
@@ -774,7 +774,7 @@ export default function CallDetail() {
{/* Tabs */}
-
+
setIsNotesModalOpen(false)} callId={callId || ''} />
diff --git a/src/components/maps/map-panel.tsx b/src/components/maps/map-panel.tsx
index 8602e60..dd60bfd 100644
--- a/src/components/maps/map-panel.tsx
+++ b/src/components/maps/map-panel.tsx
@@ -10,6 +10,7 @@ import { Loading } from '@/components/common/loading';
import { useAnalytics } from '@/hooks/use-analytics';
import { useMapSignalRUpdates } from '@/hooks/use-map-signalr-updates';
import { logger } from '@/lib/logging';
+import { isPoiMarker } from '@/lib/poi';
import { onSortOptions } from '@/lib/utils';
import { type MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData';
import { type PoiResultData } from '@/models/v4/mapping/poiResultData';
@@ -20,8 +21,6 @@ import { useToastStore } from '@/stores/toast/store';
import MapPins from './map-pins';
import PinDetailModal from './pin-detail-modal';
-const POI_MARKER_TYPE = 4;
-
interface MapPanelProps {
focusedPoi: PoiResultData | null;
}
@@ -266,9 +265,7 @@ export const MapPanel: React.FC = ({ focusedPoi }) => {
pinType: pin.Type,
});
- const isPoiPin = pin.Type === POI_MARKER_TYPE || pin.PoiTypeId != null;
-
- if (isPoiPin) {
+ if (isPoiMarker(pin)) {
router.push(`/poi/${pin.Id}`);
return;
}
diff --git a/src/components/maps/map-pins.tsx b/src/components/maps/map-pins.tsx
index a8a2787..cf31410 100644
--- a/src/components/maps/map-pins.tsx
+++ b/src/components/maps/map-pins.tsx
@@ -1,16 +1,32 @@
import Mapbox from '@rnmapbox/maps';
+import { useColorScheme } from 'nativewind';
import React from 'react';
+import { StyleSheet, Text } from 'react-native';
+import { POI_MARKER_HEIGHT, POI_MARKER_WIDTH } from '@/constants/poi-marker-shapes';
+import { isPoiMarker } from '@/lib/poi';
import { type MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData';
import { useSecurityStore } from '@/stores/security/store';
import PinMarker from './pin-marker';
+import PoiPinMarker from './poi-pin-marker';
interface MapPinsProps {
pins: MapMakerInfoData[];
onPinPress?: (pin: MapMakerInfoData) => void;
}
+const PoiTitle: React.FC<{ title: string }> = ({ title }) => {
+ const { colorScheme } = useColorScheme();
+ const isDark = colorScheme === 'dark';
+
+ return (
+
+ {title}
+
+ );
+};
+
const MapPins: React.FC = ({ pins, onPinPress }) => {
const { canUserViewPII } = useSecurityStore();
@@ -29,13 +45,41 @@ const MapPins: React.FC = ({ pins, onPinPress }) => {
return (
<>
- {filteredPins.map((pin) => (
-
- onPinPress?.(pin)} />
-
- ))}
+ {filteredPins.map((pin) => {
+ if (isPoiMarker(pin)) {
+ return (
+
+ {/* Shape + icon: 36x48 exact dimensions, anchor at bottom-center tip */}
+
+ onPinPress?.(pin)} />
+
+ {/* Title label: separate MarkerView so it does NOT affect shape anchor measurement */}
+ {pin.Title ? (
+
+
+
+ ) : null}
+
+ );
+ }
+
+ return (
+
+ onPinPress?.(pin)} />
+
+ );
+ })}
>
);
};
+const styles = StyleSheet.create({
+ poiTitle: {
+ fontSize: 10,
+ fontWeight: '600',
+ textAlign: 'center',
+ maxWidth: POI_MARKER_WIDTH + 20,
+ },
+});
+
export default MapPins;
diff --git a/src/components/maps/pin-marker.tsx b/src/components/maps/pin-marker.tsx
index 1a528cd..b8a45f5 100644
--- a/src/components/maps/pin-marker.tsx
+++ b/src/components/maps/pin-marker.tsx
@@ -7,6 +7,7 @@ import { MAP_ICONS, type MapIconKey, resolveMapIconKey } from '@/constants/map-i
interface PinMarkerProps {
imagePath?: string | null;
+ poiImage?: string | null;
marker?: string | null;
title: string;
size?: number;
@@ -15,10 +16,10 @@ interface PinMarkerProps {
fallbackIconKey?: MapIconKey;
}
-const PinMarker: React.FC = ({ imagePath, marker, title, size = 32, onPress, fallbackIconKey = 'call' }) => {
+const PinMarker: React.FC = ({ imagePath, poiImage, marker, title, size = 32, onPress, fallbackIconKey = 'call' }) => {
const { colorScheme } = useColorScheme();
- const icon = MAP_ICONS[resolveMapIconKey({ imagePath, marker, fallback: fallbackIconKey })];
+ const icon = MAP_ICONS[resolveMapIconKey({ poiImage, imagePath, marker, fallback: fallbackIconKey })];
return (
diff --git a/src/components/maps/poi-filter-bottom-sheet.tsx b/src/components/maps/poi-filter-bottom-sheet.tsx
new file mode 100644
index 0000000..ce69559
--- /dev/null
+++ b/src/components/maps/poi-filter-bottom-sheet.tsx
@@ -0,0 +1,149 @@
+import { ChevronDownIcon, FilterIcon, RotateCcwIcon, XIcon } from 'lucide-react-native';
+import { useColorScheme } from 'nativewind';
+import React, { useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { type PoiTypeResultData } from '@/models/v4/mapping/poiTypeResultData';
+
+import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper } from '../ui/actionsheet';
+import { Badge } from '../ui/badge';
+import { Box } from '../ui/box';
+import { Button, ButtonText } from '../ui/button';
+import { Heading } from '../ui/heading';
+import { HStack } from '../ui/hstack';
+import { Select, SelectBackdrop, SelectContent, SelectDragIndicator, SelectDragIndicatorWrapper, SelectIcon, SelectInput, SelectItem, SelectPortal, SelectTrigger } from '../ui/select';
+import { Text } from '../ui/text';
+import { VStack } from '../ui/vstack';
+
+type PoiSortOption = 'name' | 'type';
+const ALL_POI_TYPES_VALUE = 'all';
+
+interface PoiFilterBottomSheetProps {
+ isOpen: boolean;
+ onClose: () => void;
+ selectedPoiType: string;
+ onPoiTypeChange: (value: string) => void;
+ sortBy: PoiSortOption;
+ onSortByChange: (value: PoiSortOption) => void;
+ poiTypes: PoiTypeResultData[];
+}
+
+export const PoiFilterBottomSheet: React.FC = ({ isOpen, onClose, selectedPoiType, onPoiTypeChange, sortBy, onSortByChange, poiTypes }) => {
+ const { t } = useTranslation();
+ const { colorScheme } = useColorScheme();
+
+ const hasActiveFilters = selectedPoiType !== ALL_POI_TYPES_VALUE || sortBy !== 'name';
+
+ const handleReset = useCallback(() => {
+ onPoiTypeChange(ALL_POI_TYPES_VALUE);
+ onSortByChange('name');
+ }, [onPoiTypeChange, onSortByChange]);
+
+ const handleDone = useCallback(() => {
+ onClose();
+ }, [onClose]);
+
+ const handlePoiTypeChange = useCallback(
+ (value: string) => {
+ onPoiTypeChange(value);
+ },
+ [onPoiTypeChange]
+ );
+
+ const handleSortChange = useCallback(
+ (value: string) => {
+ onSortByChange(value as PoiSortOption);
+ },
+ [onSortByChange]
+ );
+
+ const selectedPoiTypeLabel = selectedPoiType === ALL_POI_TYPES_VALUE ? t('poi.filter_all_types') : poiTypes.find((pt) => pt.PoiTypeId.toString() === selectedPoiType)?.Name || t('poi.filter_all_types');
+
+ const selectedSortLabel = sortBy === 'name' ? t('poi.sort_name') : t('poi.sort_type');
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('poi.filter_sort_title', 'Filter & Sort')}
+
+ {hasActiveFilters ? (
+
+ !
+
+ ) : null}
+
+
+
+
+
+
+ {t('poi.filter_label')}
+
+
+
+
+ {t('poi.sort_label')}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PoiFilterBottomSheet;
diff --git a/src/components/maps/poi-icon.tsx b/src/components/maps/poi-icon.tsx
new file mode 100644
index 0000000..1961753
--- /dev/null
+++ b/src/components/maps/poi-icon.tsx
@@ -0,0 +1,131 @@
+import type { LucideIcon } from 'lucide-react-native';
+import * as LucideIcons from 'lucide-react-native';
+import React from 'react';
+
+/**
+ * Maps a kebab-case lucide icon name string to the corresponding
+ * lucide-react-native icon component.
+ *
+ * This allows dynamic icon selection based on POI type data without
+ * importing every possible icon statically at the call site.
+ */
+const LUCIDE_ICON_MAP: Record = {
+ // Map marker icons
+ 'map-pin': LucideIcons.MapPin,
+ 'shield-alert': LucideIcons.ShieldAlert,
+ shield: LucideIcons.Shield,
+ 'shield-check': LucideIcons.ShieldCheck,
+ flame: LucideIcons.Flame,
+ route: LucideIcons.Route,
+ square: LucideIcons.Square,
+ flag: LucideIcons.Flag,
+ info: LucideIcons.Info,
+ landmark: LucideIcons.Landmark,
+
+ // Transportation
+ car: LucideIcons.Car,
+ bike: LucideIcons.Bike,
+ bus: LucideIcons.Bus,
+ truck: LucideIcons.Truck,
+ ship: LucideIcons.Ship,
+ plane: LucideIcons.Plane,
+ 'train-front': LucideIcons.TrainFront,
+
+ // Buildings & Places
+ building: LucideIcons.Building,
+ 'building-2': LucideIcons.Building2,
+ home: LucideIcons.Home,
+ hotel: LucideIcons.Hotel,
+ store: LucideIcons.Store,
+ warehouse: LucideIcons.Warehouse,
+ school: LucideIcons.School,
+ hospital: LucideIcons.Hospital,
+ church: LucideIcons.Church,
+ tent: LucideIcons.Tent,
+ 'door-open': LucideIcons.DoorOpen,
+
+ // Nature & Outdoor
+ 'tree-pine': LucideIcons.TreePine,
+ mountain: LucideIcons.Mountain,
+ waves: LucideIcons.Waves,
+ droplets: LucideIcons.Droplets,
+ wind: LucideIcons.Wind,
+ fish: LucideIcons.Fish,
+ 'paw-print': LucideIcons.PawPrint,
+ flower: LucideIcons.Flower,
+
+ // Food & Drink
+ utensils: LucideIcons.Utensils,
+ coffee: LucideIcons.Coffee,
+ beer: LucideIcons.Beer,
+ croissant: LucideIcons.Croissant,
+ 'shopping-basket': LucideIcons.ShoppingBasket,
+ 'shopping-cart': LucideIcons.ShoppingCart,
+
+ // Commerce & Services
+ wrench: LucideIcons.Wrench,
+ hammer: LucideIcons.Hammer,
+ scissors: LucideIcons.Scissors,
+ key: LucideIcons.Key,
+ gem: LucideIcons.Gem,
+ scale: LucideIcons.Scale,
+ mail: LucideIcons.Mail,
+ phone: LucideIcons.Phone,
+ globe: LucideIcons.Globe,
+ monitor: LucideIcons.Monitor,
+ shirt: LucideIcons.Shirt,
+ armchair: LucideIcons.Armchair,
+ footprints: LucideIcons.Footprints,
+ paintbrush: LucideIcons.Paintbrush,
+ antenna: LucideIcons.Antenna,
+
+ // Education
+ 'graduation-cap': LucideIcons.GraduationCap,
+ baby: LucideIcons.Baby,
+ library: LucideIcons.Library,
+
+ // Recreation
+ 'ferris-wheel': LucideIcons.FerrisWheel,
+ palette: LucideIcons.Palette,
+ dices: LucideIcons.Dices,
+ clapperboard: LucideIcons.Clapperboard,
+ dumbbell: LucideIcons.Dumbbell,
+ music: LucideIcons.Music,
+ circle: LucideIcons.Circle,
+ 'circle-parking': LucideIcons.CircleParking,
+ 'pill-bottle': LucideIcons.PillBottle,
+
+ // Utility
+ zap: LucideIcons.Zap,
+ bell: LucideIcons.Bell,
+ cross: LucideIcons.Cross,
+ stethoscope: LucideIcons.Stethoscope,
+ gavel: LucideIcons.Gavel,
+ fuel: LucideIcons.Fuel,
+ 'arrow-down': LucideIcons.ArrowDown,
+};
+
+interface PoiIconProps {
+ /** Kebab-case lucide icon name (e.g. "hospital", "map-pin") */
+ name: string;
+ /** Icon size in pixels */
+ size?: number;
+ /** Icon color */
+ color?: string;
+}
+
+const DEFAULT_ICON_SIZE = 14;
+const DEFAULT_ICON_COLOR = '#ffffff';
+
+/**
+ * Renders a lucide-react-native icon by its kebab-case name string.
+ *
+ * Falls back to the MapPin icon if the requested icon name is not found.
+ */
+export const PoiIcon: React.FC = ({ name, size = DEFAULT_ICON_SIZE, color = DEFAULT_ICON_COLOR }) => {
+ const IconComponent = LUCIDE_ICON_MAP[name] || LUCIDE_ICON_MAP['map-pin'];
+
+ return ;
+};
+
+export default PoiIcon;
diff --git a/src/components/maps/poi-list-panel.tsx b/src/components/maps/poi-list-panel.tsx
index d6c9cd1..2de5c67 100644
--- a/src/components/maps/poi-list-panel.tsx
+++ b/src/components/maps/poi-list-panel.tsx
@@ -1,16 +1,16 @@
import { FlashList, type ListRenderItem } from '@shopify/flash-list';
-import { ChevronDownIcon, MapIcon } from 'lucide-react-native';
+import { FilterIcon, MapIcon, SearchIcon, XIcon } from 'lucide-react-native';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Loading } from '@/components/common/loading';
import ZeroState from '@/components/common/zero-state';
+import { PoiFilterBottomSheet } from '@/components/maps/poi-filter-bottom-sheet';
import { Badge } from '@/components/ui/badge';
import { Box } from '@/components/ui/box';
import { Button, ButtonText } from '@/components/ui/button';
import { HStack } from '@/components/ui/hstack';
-import { Pressable } from '@/components/ui/pressable';
-import { Select, SelectBackdrop, SelectContent, SelectDragIndicator, SelectDragIndicatorWrapper, SelectIcon, SelectInput, SelectItem, SelectPortal, SelectTrigger } from '@/components/ui/select';
+import { Input, InputField, InputIcon, InputSlot } from '@/components/ui/input';
import { Text } from '@/components/ui/text';
import { VStack } from '@/components/ui/vstack';
import { getPoiDisplayName } from '@/lib/poi';
@@ -88,9 +88,23 @@ export const PoiListPanel: React.FC = ({ onPoiPress, onViewOn
}));
const [selectedPoiType, setSelectedPoiType] = useState(ALL_POI_TYPES_VALUE);
const [sortBy, setSortBy] = useState('name');
+ const [searchQuery, setSearchQuery] = useState('');
+ const [isFilterSheetOpen, setIsFilterSheetOpen] = useState(false);
+
+ const openFilterSheet = useCallback(() => setIsFilterSheetOpen(true), []);
+ const closeFilterSheet = useCallback(() => setIsFilterSheetOpen(false), []);
+
+ const hasActiveFilters = selectedPoiType !== ALL_POI_TYPES_VALUE || sortBy !== 'name';
const filteredPois = useMemo(() => {
- const nextPois = selectedPoiType === ALL_POI_TYPES_VALUE ? [...pois] : pois.filter((poi) => poi.PoiTypeId.toString() === selectedPoiType);
+ let nextPois = selectedPoiType === ALL_POI_TYPES_VALUE ? [...pois] : pois.filter((poi) => poi.PoiTypeId.toString() === selectedPoiType);
+
+ if (searchQuery.trim()) {
+ const query = searchQuery.toLowerCase();
+ nextPois = nextPois.filter(
+ (poi) => poi.Name?.toLowerCase().includes(query) || poi.Address?.toLowerCase().includes(query) || poi.Note?.toLowerCase().includes(query) || poi.PoiTypeName?.toLowerCase().includes(query)
+ );
+ }
nextPois.sort((left, right) => {
if (sortBy === 'type') {
@@ -102,19 +116,7 @@ export const PoiListPanel: React.FC = ({ onPoiPress, onViewOn
});
return nextPois;
- }, [pois, selectedPoiType, sortBy]);
-
- const selectedPoiTypeLabel = useMemo(() => {
- if (selectedPoiType === ALL_POI_TYPES_VALUE) {
- return t('poi.filter_all_types');
- }
-
- return poiTypes.find((poiType) => poiType.PoiTypeId.toString() === selectedPoiType)?.Name || t('poi.filter_all_types');
- }, [poiTypes, selectedPoiType, t]);
-
- const selectedSortLabel = useMemo(() => {
- return sortBy === 'name' ? t('poi.sort_name') : t('poi.sort_type');
- }, [sortBy, t]);
+ }, [pois, selectedPoiType, sortBy, searchQuery]);
const renderPoiItem = useCallback>(({ item }) => , [onPoiPress, onViewOnMap]);
@@ -130,61 +132,39 @@ export const PoiListPanel: React.FC = ({ onPoiPress, onViewOn
return (
-
-
- {t('poi.list_description')}
-
-
-
- {t('poi.filter_label')}
-
-
-
-
- {t('poi.sort_label')}
-
-
-
-
-
+ {t('poi.list_description')}
+
+
+
+
+
+
+
+ {searchQuery ? (
+ setSearchQuery('')}>
+
+
+ ) : null}
+
+
+
{t('poi.results_count', { count: filteredPois.length })}
-
+
- {t('poi.view_on_map_hint')}
+
+ {t('poi.view_on_map_hint')}
+
@@ -193,6 +173,8 @@ export const PoiListPanel: React.FC = ({ onPoiPress, onViewOn
) : (
)}
+
+
);
};
diff --git a/src/components/maps/poi-pin-marker.tsx b/src/components/maps/poi-pin-marker.tsx
new file mode 100644
index 0000000..b563e91
--- /dev/null
+++ b/src/components/maps/poi-pin-marker.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import { StyleSheet, TouchableOpacity, View } from 'react-native';
+import Svg, { Path } from 'react-native-svg';
+
+import { resolvePoiIcon } from '@/constants/poi-icon-mapping';
+import { getPoiMarkerShapePath, POI_MARKER_HEIGHT, POI_MARKER_VIEWBOX, POI_MARKER_WIDTH } from '@/constants/poi-marker-shapes';
+
+import { PoiIcon } from './poi-icon';
+
+interface PoiPinMarkerProps {
+ /** Hex color for the SVG shape fill (e.g. "#2563eb") */
+ color?: string | null;
+ /** The `PoiImage` field from the API (e.g. "map-icon-hospital") */
+ poiImage?: string | null;
+ /** The `ImagePath` field from the API (fallback for icon resolution) */
+ imagePath?: string | null;
+ /** The `Marker` field from the API (shape type: "MAP_PIN", "SHIELD", etc.) */
+ marker?: string | null;
+ /** Display title for the marker */
+ title: string;
+ /** Press handler */
+ onPress?: () => void;
+}
+
+/** Default fill color matching the web app. */
+export const DEFAULT_POI_COLOR = '#2563eb';
+
+/**
+ * POI pin marker component that renders a colored SVG shape with a white icon inside.
+ *
+ * Matches the web application's POI marker rendering:
+ * - SVG background shape filled with the POI type's Color
+ * - White lucide icon centered on top of the shape
+ * - Drop shadow via an offset semi-transparent path
+ * - 36x48 pixel dimensions, bottom-center anchor
+ *
+ * This component is the SHAPE+ICON only (no title text). The title is rendered
+ * as a separate MarkerView in the parent (MapPins) so that the anchor measurement
+ * is not contaminated by overflow text.
+ */
+const PoiPinMarker: React.FC = ({ color, poiImage, imagePath, marker, title, onPress }) => {
+ const fillColor = color?.trim() || DEFAULT_POI_COLOR;
+ const shapePath = getPoiMarkerShapePath(marker);
+ const iconName = resolvePoiIcon(poiImage, imagePath);
+
+ return (
+
+ {/*
+ * Shape background — kept in normal layout flow (NOT absolutely positioned)
+ * so that Mapbox.MarkerView measures the correct 36×48 dimensions when
+ * computing the anchor offset. Previously both children were absolute,
+ * giving the container an intrinsic height of 0 and causing the SVG to
+ * render entirely below the coordinate point ("too low / cut off").
+ */}
+
+ {/* White icon — absolutely overlaid on the shape */}
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ /*
+ * No explicit width/height — the Svg in normal flow (36×48) determines the
+ * container size, which Mapbox can measure correctly for anchor computation.
+ */
+ container: {
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ },
+ iconContainer: {
+ position: 'absolute',
+ top: 12,
+ left: 0,
+ right: 0,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+});
+
+export default PoiPinMarker;
diff --git a/src/components/ui/shared-tabs.tsx b/src/components/ui/shared-tabs.tsx
index e303760..5f99247 100644
--- a/src/components/ui/shared-tabs.tsx
+++ b/src/components/ui/shared-tabs.tsx
@@ -197,10 +197,15 @@ export const SharedTabs: React.FC = ({
) : (
{tab.title}
)}
+ {/*
+ * Badge is absolutely positioned so it does NOT participate in the
+ * flex column layout (icon → text). Previously it was an inline
+ * third item that stacked below the label — the "2nd row" bug.
+ */}
{tab.badge !== undefined && tab.badge > 0 && (
-
- {tab.badge}
-
+
+ {tab.badge > 99 ? '99+' : tab.badge}
+
)}
))}
@@ -229,4 +234,24 @@ const tabContentStyles = StyleSheet.create({
active: { flex: 1 },
// display:'none' hides the view and removes it from layout without unmounting
hidden: { display: 'none' },
+ // Badge overlay for the non-scrollable tab bar. Absolutely positioned so it
+ // sits in the top-right corner of the tab without pushing text to a 2nd row.
+ badge: {
+ position: 'absolute',
+ top: 2,
+ right: 2,
+ minWidth: 16,
+ height: 16,
+ borderRadius: 8,
+ backgroundColor: '#ef4444',
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: 3,
+ },
+ badgeText: {
+ fontSize: 9,
+ fontWeight: '700',
+ color: '#ffffff',
+ lineHeight: 11,
+ },
});
diff --git a/src/constants/map-icons.ts b/src/constants/map-icons.ts
index 91651d4..8c8eaa5 100644
--- a/src/constants/map-icons.ts
+++ b/src/constants/map-icons.ts
@@ -225,16 +225,27 @@ export type MapIconKey = keyof typeof MAP_ICONS;
const MAP_ICON_KEYS = new Set(Object.keys(MAP_ICONS));
+const stripMapIconPrefix = (value: string): string => {
+ if (value.toLowerCase().startsWith('map-icon-')) {
+ return value.slice(9);
+ }
+ return value;
+};
+
const normalizeMapIconToken = (value: string) => {
- return value
+ // First strip "map-icon-" prefix if present, so "map-icon-hospital"
+ // normalizes to "hospital" instead of "mapiconhospital".
+ const stripped = stripMapIconPrefix(value);
+
+ return stripped
.trim()
.toLowerCase()
.replace(/\.[a-z0-9]+$/i, '')
.replace(/[^a-z0-9]+/g, '');
};
-export const resolveMapIconKey = ({ imagePath, marker, fallback = 'call' }: { imagePath?: string | null; marker?: string | null; fallback?: MapIconKey }): MapIconKey => {
- const tokens = [imagePath, marker].filter((value): value is string => typeof value === 'string' && value.trim().length > 0).map(normalizeMapIconToken);
+export const resolveMapIconKey = ({ poiImage, imagePath, marker, fallback = 'call' }: { poiImage?: string | null; imagePath?: string | null; marker?: string | null; fallback?: MapIconKey }): MapIconKey => {
+ const tokens = [poiImage, imagePath, marker].filter((value): value is string => typeof value === 'string' && value.trim().length > 0).map(normalizeMapIconToken);
for (const token of tokens) {
if (MAP_ICON_KEYS.has(token)) {
diff --git a/src/constants/poi-icon-mapping.ts b/src/constants/poi-icon-mapping.ts
new file mode 100644
index 0000000..634fbbb
--- /dev/null
+++ b/src/constants/poi-icon-mapping.ts
@@ -0,0 +1,200 @@
+/**
+ * POI icon mapping from map-icons CSS class names to lucide-react-native icon names.
+ *
+ * This mapping mirrors the map-icons font library used by the Resgrid web application.
+ * Each entry maps a map-icon CSS class name (e.g. "map-icon-hospital") to a
+ * lucide-react-native icon name (e.g. "hospital").
+ *
+ * For mobile apps, lucide-react-native provides native vector icons that are
+ * rendered in white inside the colored SVG shape background.
+ */
+
+/**
+ * Raw mapping: map-icon class name (without "map-icon-" prefix) → lucide icon name.
+ * All lucide icon names have been verified against lucide-react-native v0.475.
+ */
+const POI_ICON_TO_LUCIDE: Record = {
+ // Common POI types
+ 'map-pin': 'map-pin',
+ 'point-of-interest': 'map-pin',
+ hospital: 'hospital',
+ police: 'shield-alert',
+ 'fire-station': 'flame',
+ school: 'school',
+ bank: 'building',
+ 'post-office': 'mail',
+ church: 'church',
+ parking: 'circle-parking',
+ 'gas-station': 'fuel',
+ airport: 'plane',
+ restaurant: 'utensils',
+ 'grocery-or-supermarket': 'shopping-basket',
+ pharmacy: 'pill-bottle',
+ library: 'library',
+ museum: 'building-2',
+ stadium: 'building-2',
+ courthouse: 'gavel',
+ 'city-hall': 'building-2',
+ embassy: 'building-2',
+ campground: 'tent',
+ park: 'tree-pine',
+ lodging: 'hotel',
+ 'train-station': 'train-front',
+ 'bus-station': 'bus',
+ 'square-pin': 'map-pin',
+ shield: 'shield',
+ route: 'route',
+ square: 'square',
+ 'square-rounded': 'square',
+
+ // Transportation
+ 'car-rental': 'car',
+ 'car-repair': 'wrench',
+ 'car-wash': 'car',
+ 'bicycle-store': 'bike',
+ bicycle: 'bike',
+ motorcycle: 'bike',
+ boat: 'ship',
+ ferry: 'ship',
+ 'subway-station': 'train-front',
+ 'taxi-stand': 'car',
+
+ // Food & Drink
+ bakery: 'croissant',
+ bar: 'beer',
+ cafe: 'coffee',
+ 'convenience-store': 'store',
+ 'liquor-store': 'beer',
+ 'meal-delivery': 'bike',
+ 'meal-takeaway': 'shopping-basket',
+ supermarket: 'shopping-cart',
+ store: 'store',
+
+ // Health
+ dentist: 'stethoscope',
+ doctor: 'stethoscope',
+ 'veterinary-care': 'stethoscope',
+ 'medical-store': 'cross',
+
+ // Civic & Government
+ 'local-government-office': 'building-2',
+ 'local-post-office': 'mail',
+ 'place-of-worship': 'church',
+ mosque: 'church',
+ synagogue: 'church',
+ temple: 'church',
+ cemetery: 'church',
+ 'funeral-home': 'church',
+ 'fire-hydrant': 'flame',
+ 'police-box': 'shield-alert',
+
+ // Education
+ university: 'graduation-cap',
+ college: 'graduation-cap',
+ daycare: 'baby',
+ 'primary-school': 'school',
+ 'secondary-school': 'school',
+
+ // Recreation
+ aquarium: 'fish',
+ 'amusement-park': 'ferris-wheel',
+ 'art-gallery': 'palette',
+ beach: 'waves',
+ 'bowling-alley': 'circle',
+ casino: 'dices',
+ cinema: 'clapperboard',
+ 'movie-theater': 'clapperboard',
+ 'golf-course': 'flag',
+ gym: 'dumbbell',
+ 'hair-care': 'scissors',
+ 'night-club': 'music',
+ playground: 'tree-pine',
+ rink: 'circle',
+ spa: 'droplets',
+ 'tourist-information': 'info',
+ zoo: 'paw-print',
+
+ // Business & Services
+ atm: 'landmark',
+ 'beauty-salon': 'scissors',
+ 'car-dealer': 'car',
+ 'clothing-store': 'shirt',
+ 'department-store': 'store',
+ 'electronics-store': 'monitor',
+ florist: 'flower',
+ 'furniture-store': 'armchair',
+ 'hardware-store': 'hammer',
+ 'home-goods-store': 'home',
+ 'insurance-agency': 'shield-check',
+ 'jewelry-store': 'gem',
+ laundromat: 'shirt',
+ lawyer: 'scale',
+ locksmith: 'key',
+ 'moving-company': 'truck',
+ 'painter-shop': 'paintbrush',
+ 'pet-store': 'paw-print',
+ plumber: 'wrench',
+ 'real-estate-agency': 'building',
+ 'roofing-contractor': 'home',
+ 'shoe-store': 'footprints',
+ 'shopping-mall': 'store',
+ 'storage-facility': 'warehouse',
+ 'travel-agency': 'globe',
+
+ // Infrastructure
+ bridge: 'route',
+ dam: 'waves',
+ electrician: 'zap',
+ 'fire-alarm': 'bell',
+ 'fire-extinguisher': 'flame',
+ fountain: 'droplets',
+ gate: 'door-open',
+ lighthouse: 'tower',
+ memorial: 'flag',
+ monument: 'landmark',
+ 'mountain-pass': 'mountain',
+ 'natural-feature': 'mountain',
+ 'power-substation': 'zap',
+ 'power-tower': 'zap',
+ tower: 'antenna',
+ tunnel: 'arrow-down',
+ 'water-tower': 'droplets',
+ windmill: 'wind',
+};
+
+/** Default lucide icon name when a map-icon is not found in the mapping. */
+export const DEFAULT_POI_ICON = 'map-pin';
+
+/**
+ * Strips the "map-icon-" prefix from a CSS class name to get the icon key.
+ *
+ * @param iconClass - The full CSS class name (e.g. "map-icon-hospital")
+ * @returns The icon key without the prefix (e.g. "hospital"), or the original string
+ * if it doesn't start with "map-icon-".
+ */
+const stripMapIconPrefix = (iconClass: string): string => {
+ if (iconClass.toLowerCase().startsWith('map-icon-')) {
+ return iconClass.slice(9); // Remove "map-icon-"
+ }
+ return iconClass;
+};
+
+/**
+ * Resolves a map-icon CSS class name to a lucide-react-native icon name.
+ *
+ * @param poiImage - The `PoiImage` field value from the API (e.g. "map-icon-hospital")
+ * @param imagePath - The `ImagePath` field as fallback
+ * @returns A lucide-react-native icon name string, or the default if not found.
+ */
+export const resolvePoiIcon = (poiImage?: string | null, imagePath?: string | null): string => {
+ const tokens = [poiImage, imagePath].filter((v): v is string => typeof v === 'string' && v.trim().length > 0);
+
+ for (const token of tokens) {
+ const key = stripMapIconPrefix(token.trim()).toLowerCase();
+ if (POI_ICON_TO_LUCIDE[key]) {
+ return POI_ICON_TO_LUCIDE[key];
+ }
+ }
+
+ return DEFAULT_POI_ICON;
+};
diff --git a/src/constants/poi-marker-shapes.ts b/src/constants/poi-marker-shapes.ts
new file mode 100644
index 0000000..70361ed
--- /dev/null
+++ b/src/constants/poi-marker-shapes.ts
@@ -0,0 +1,91 @@
+/**
+ * SVG path definitions for POI marker background shapes.
+ * All shapes use the viewBox="-24 -48 48 48" coordinate system as defined in the web app.
+ *
+ * Origin (0,0) is at the top-center of the 48×48 bounding box.
+ * X goes from -24 (left) to +24 (right).
+ * Y goes from -48 (top) to 0 (bottom).
+ * The tip of the pin is at (0, 0) (bottom-center).
+ *
+ * When rendered at 36×48 pixels:
+ * scaleX = 36/48 = 0.75
+ * scaleY = 48/48 = 1.0
+ * offsetX = 24 * 0.75 = 18
+ * offsetY = 48 * 1.0 = 48
+ */
+
+export type PoiMarkerShape = 'MAP_PIN' | 'SHIELD' | 'ROUTE' | 'SQUARE' | 'SQUARE_ROUNDED';
+
+/**
+ * Classic teardrop-shaped map pin.
+ */
+const MAP_PIN_PATH = 'M0-48c-9.8 0-17.7 7.8-17.7 17.4 0 15.5 17.7 30.6 17.7 30.6s17.7-15.4 17.7-30.6c0-9.6-7.9-17.4-17.7-17.4z';
+
+/**
+ * Crest/shield shape.
+ */
+const SHIELD_PATH =
+ 'M18.8-31.8c.3-3.4 1.3-6.6 3.2-9.5l-7-6.7c-2.2 1.8-4.8 2.8-7.6 3-2.6.2-5.1-.2-7.5-1.4-2.4 1.1-4.9 1.6-7.5 1.4-2.7-.2-5.1-1.1-7.3-2.7l-7.1 6.7c1.7 2.9 2.7 6 2.9 9.2.1 1.5-.3 3.5-1.3 6.1-.5 1.5-.9 2.7-1.2 3.8-.2 1-.4 1.9-.5 2.5 0 2.8.8 5.3 2.5 7.5 1.3 1.6 3.5 3.4 6.5 5.4 3.3 1.6 5.8 2.6 7.6 3.1.5.2 1 .4 1.5.7l1.5.6c1.2.7 2 1.4 2.4 2.1.5-.8 1.3-1.5 2.4-2.1.7-.3 1.3-.5 1.9-.8.5-.2.9-.4 1.1-.5.4-.1.9-.3 1.5-.6.6-.2 1.3-.5 2.2-.8 1.7-.6 3-1.1 3.8-1.6 2.9-2 5.1-3.8 6.4-5.3 1.7-2.2 2.6-4.8 2.5-7.6-.1-1.3-.7-3.3-1.7-6.1-.9-2.8-1.3-4.9-1.2-6.4z';
+
+/**
+ * Route sign shape.
+ */
+const ROUTE_PATH =
+ 'M24-28.3c-.2-13.3-7.9-18.5-8.3-18.7l-1.2-.8-1.2.8c-2 1.4-4.1 2-6.1 2-3.4 0-5.8-1.9-5.9-1.9l-1.3-1.1-1.3 1.1c-.1.1-2.5 1.9-5.9 1.9-2.1 0-4.1-.7-6.1-2l-1.2-.8-1.2.8c-.8.6-8 5.9-8.2 18.7-.2 1.1 2.9 22.2 23.9 28.3 22.9-6.7 24.1-26.9 24-28.3z';
+
+/**
+ * Plain square (no rounded corners).
+ * Note: SVG viewBox is -24,-48,48,48, but for SQUARE we use a simpler path.
+ */
+const SQUARE_PATH = 'M-24-48h48v48h-48z';
+
+/**
+ * Square with rounded corners.
+ */
+const SQUARE_ROUNDED_PATH = 'M24-8c0 4.4-3.6 8-8 8h-32c-4.4 0-8-3.6-8-8v-32c0-4.4 3.6-8 8-8h32c4.4 0 8 3.6 8 8v32z';
+
+/**
+ * Lookup table for all supported POI marker shapes.
+ * The key is the normalized (uppercase) shape name as provided by the API's `Marker` field.
+ */
+export const POI_MARKER_PATHS: Record = {
+ MAP_PIN: MAP_PIN_PATH,
+ SHIELD: SHIELD_PATH,
+ ROUTE: ROUTE_PATH,
+ SQUARE: SQUARE_PATH,
+ SQUARE_ROUNDED: SQUARE_ROUNDED_PATH,
+} as const;
+
+/** Default shape when none is specified or the shape is unknown. */
+export const DEFAULT_POI_MARKER_SHAPE: PoiMarkerShape = 'MAP_PIN';
+
+/**
+ * Returns the SVG path data for a given POI marker shape.
+ *
+ * @param markerShape - The `Marker` field value from the API (e.g. "MAP_PIN", "SHIELD").
+ * Normalized to uppercase internally. Null/empty values default to MAP_PIN.
+ * @returns The SVG path `d` attribute string.
+ */
+export const getPoiMarkerShapePath = (markerShape?: string | null): string => {
+ const normalized = (markerShape ?? '').trim().toUpperCase();
+
+ if (normalized.length === 0) {
+ return MAP_PIN_PATH;
+ }
+
+ return POI_MARKER_PATHS[normalized] ?? MAP_PIN_PATH;
+};
+
+/** SVG viewBox used for all shapes. */
+export const POI_MARKER_VIEWBOX = '-24 -48 48 48';
+
+/** The aspect ratio of the SVG shapes (width/height). */
+export const POI_MARKER_ASPECT_RATIO = 48 / 48; // 1.0
+
+/** Total marker dimensions in pixels (matching the web app). */
+export const POI_MARKER_WIDTH = 36;
+export const POI_MARKER_HEIGHT = 48;
+
+/** Anchor point for the marker (bottom-center). */
+export const POI_MARKER_ANCHOR_X = 0.5;
+export const POI_MARKER_ANCHOR_Y = 1.0;
diff --git a/src/lib/logging/index.tsx b/src/lib/logging/index.tsx
index c0351b8..d6aaae1 100644
--- a/src/lib/logging/index.tsx
+++ b/src/lib/logging/index.tsx
@@ -1,3 +1,4 @@
+import * as Sentry from '@sentry/react-native';
import { consoleTransport, logger as rnLogger } from 'react-native-logs';
import type { LogEntry, Logger, LogLevel } from './types';
@@ -49,6 +50,22 @@ class LogService {
...context,
timestamp: new Date().toISOString(),
});
+
+ // Add log breadcrumb to Sentry for error/warn levels
+ // Provides rich debugging context when errors occur
+ if (level === 'error' || level === 'warn') {
+ try {
+ Sentry.addBreadcrumb({
+ category: 'log',
+ level: level === 'error' ? 'error' : 'warning',
+ message: String(message),
+ data: { ...this.globalContext, ...context },
+ timestamp: Date.now() / 1000, // Unix timestamp in seconds
+ });
+ } catch {
+ // Sentry may not be initialized yet — silently ignore
+ }
+ }
}
public setGlobalContext(context: Record): void {
diff --git a/src/lib/poi.ts b/src/lib/poi.ts
index b02f5ca..46c5465 100644
--- a/src/lib/poi.ts
+++ b/src/lib/poi.ts
@@ -1,6 +1,52 @@
+import { type MapMakerInfoData } from '@/models/v4/mapping/getMapDataAndMarkersData';
import { type PoiResultData } from '@/models/v4/mapping/poiResultData';
import { type PoiTypeResultData } from '@/models/v4/mapping/poiTypeResultData';
+//region POI Marker Detection
+
+export const POI_MARKER_TYPE = 4;
+
+/**
+ * Determines whether a map marker represents a Point of Interest (POI).
+ *
+ * Matches the web application's `isPoiMarker()` logic:
+ * 1. marker.Type === 4 (explicit POI type)
+ * 2. marker.PoiTypeId is a number greater than 0
+ * 3. marker.LayerId starts with the string "poi-type-"
+ * 4. marker.PoiImage (or ImagePath) starts with "map-icon-" (case-insensitive)
+ *
+ * @param marker - A map marker from the API response.
+ * @returns true if the marker is a POI, false otherwise.
+ */
+export const isPoiMarker = (marker: Pick): boolean => {
+ // 1. Explicit POI type
+ if (marker.Type === POI_MARKER_TYPE) {
+ return true;
+ }
+
+ // 2. Has a POI type ID greater than 0
+ if (marker.PoiTypeId != null && marker.PoiTypeId > 0) {
+ return true;
+ }
+
+ // 3. Layer ID starts with "poi-type-"
+ if (marker.LayerId != null && marker.LayerId.toLowerCase().startsWith('poi-type-')) {
+ return true;
+ }
+
+ // 4. PoiImage or ImagePath starts with "map-icon-" (case-insensitive)
+ const iconField = marker.PoiImage?.trim() || marker.ImagePath;
+ if (iconField != null && iconField.toLowerCase().startsWith('map-icon-')) {
+ return true;
+ }
+
+ return false;
+};
+
+//endregion
+
+//region POI Display & Grouping
+
export interface PoiDisplayable {
Name?: string | null;
Address?: string | null;
@@ -61,7 +107,7 @@ export const groupPoisByType = (pois: PoiResultData[], poiTypes: PoiTypeResultDa
poiTypeName: poi.PoiTypeName || poiType?.Name || '',
isDestination: poi.IsDestination || poiType?.IsDestination || false,
color: poi.Color || poiType?.Color || '',
- imagePath: poi.ImagePath || poiType?.ImagePath || '',
+ imagePath: poi.PoiImage || poi.ImagePath || poiType?.PoiImage || poiType?.ImagePath || '',
marker: poi.Marker || poiType?.Marker || '',
pois: [poi],
});
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 3080f8f..8a5f631 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -2,7 +2,7 @@ import { Linking } from 'react-native';
import { Platform } from 'react-native';
import type { StoreApi, UseBoundStore } from 'zustand';
-import { Env } from './env';
+import { getBaseApiUrl } from '@/lib/storage/app';
export function openLinkInBrowser(url: string) {
Linking.canOpenURL(url).then((canOpen) => canOpen && Linking.openURL(url));
@@ -54,7 +54,7 @@ export function onSortOptions(a: any, b: any) {
}
export function getAvatarUrl(userId: string) {
- return Env.BASE_API_URL + Env.RESGRID_API_URL + '/Avatars/Get?id=' + userId;
+ return getBaseApiUrl() + '/Avatars/Get?id=' + userId;
}
export function invertColor(hex: string, bw: boolean): string {
diff --git a/src/models/v4/mapping/getMapDataAndMarkersData.ts b/src/models/v4/mapping/getMapDataAndMarkersData.ts
index 5ccb770..304d7e7 100644
--- a/src/models/v4/mapping/getMapDataAndMarkersData.ts
+++ b/src/models/v4/mapping/getMapDataAndMarkersData.ts
@@ -18,6 +18,7 @@ export class MapMakerInfoData {
public InfoWindowContent: string = '';
public Color: string = '';
public Type: number = 0;
+ public PoiImage?: string;
public Marker?: string;
public PoiTypeId?: number | null;
public PoiTypeName?: string;
diff --git a/src/models/v4/mapping/poiLayerData.ts b/src/models/v4/mapping/poiLayerData.ts
index ae72c3b..89f533e 100644
--- a/src/models/v4/mapping/poiLayerData.ts
+++ b/src/models/v4/mapping/poiLayerData.ts
@@ -3,6 +3,7 @@ export class PoiLayerData {
public Name: string = '';
public Color: string = '';
public ImagePath: string = '';
+ public PoiImage: string = '';
public Marker: string = '';
public IsDestination: boolean = false;
}
diff --git a/src/models/v4/mapping/poiResultData.ts b/src/models/v4/mapping/poiResultData.ts
index 2dad659..c74549c 100644
--- a/src/models/v4/mapping/poiResultData.ts
+++ b/src/models/v4/mapping/poiResultData.ts
@@ -9,6 +9,7 @@ export class PoiResultData {
public Longitude: number = 0;
public Color: string = '';
public ImagePath: string = '';
+ public PoiImage: string = '';
public Marker: string = '';
public IsDestination: boolean = false;
}
diff --git a/src/models/v4/mapping/poiTypeResultData.ts b/src/models/v4/mapping/poiTypeResultData.ts
index e26082d..23fab04 100644
--- a/src/models/v4/mapping/poiTypeResultData.ts
+++ b/src/models/v4/mapping/poiTypeResultData.ts
@@ -3,6 +3,7 @@ export class PoiTypeResultData {
public Name: string = '';
public Color: string = '';
public ImagePath: string = '';
+ public PoiImage: string = '';
public Marker: string = '';
public IsDestination: boolean = false;
}
diff --git a/src/translations/ar.json b/src/translations/ar.json
index 93ece3b..e158117 100644
--- a/src/translations/ar.json
+++ b/src/translations/ar.json
@@ -838,6 +838,7 @@
"empty_title": "لم يتم العثور على نقاط اهتمام",
"filter_all_types": "كل الأنواع",
"filter_label": "تصفية",
+ "filter_sort_title": "فلترة وفرز",
"invalid_description": "مطلوب معرّف نقطة اهتمام لعرض التفاصيل.",
"invalid_title": "نقطة اهتمام غير صالحة",
"list_description": "تصفح نقاط الاهتمام الخاصة بالقسم، وقم بالتصفية حسب النوع، ثم ارجع إلى الخريطة.",
@@ -846,9 +847,11 @@
"loading_detail": "جارٍ تحميل تفاصيل نقطة الاهتمام...",
"not_found": "تعذر العثور على نقطة الاهتمام",
"note_label": "ملاحظة",
+ "reset_filters": "إعادة تعيين",
"results_count": "{{count}} نقطة اهتمام",
"results_count_plural": "{{count}} نقاط اهتمام",
"route_error": "تعذر فتح تطبيق الخرائط",
+ "search_placeholder": "البحث في نقاط الاهتمام...",
"set_status_destination": "استخدامها كوجهة للحالة",
"sort_label": "ترتيب",
"sort_name": "الاسم",
diff --git a/src/translations/de.json b/src/translations/de.json
index 7056af5..dc0cda7 100644
--- a/src/translations/de.json
+++ b/src/translations/de.json
@@ -838,6 +838,7 @@
"empty_title": "Keine POIs gefunden",
"filter_all_types": "Alle Typen",
"filter_label": "Filter",
+ "filter_sort_title": "Filter & Sortierung",
"invalid_description": "Zum Anzeigen der POI-Details ist eine POI-ID erforderlich.",
"invalid_title": "Ungültiger POI",
"list_description": "Durchsuchen Sie die Abteilungs-POIs, filtern Sie nach Typ und kehren Sie zur Karte zurück.",
@@ -846,9 +847,11 @@
"loading_detail": "Lade POI-Details...",
"not_found": "POI nicht gefunden",
"note_label": "Hinweis",
+ "reset_filters": "Zurücksetzen",
"results_count": "{{count}} POI",
"results_count_plural": "{{count}} POIs",
"route_error": "Karten-App konnte nicht geöffnet werden",
+ "search_placeholder": "POIs durchsuchen...",
"set_status_destination": "Als Statusziel verwenden",
"sort_label": "Sortieren",
"sort_name": "Name",
diff --git a/src/translations/en.json b/src/translations/en.json
index 3cf038d..9fafd0b 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -838,6 +838,7 @@
"empty_title": "No POIs found",
"filter_all_types": "All types",
"filter_label": "Filter",
+ "filter_sort_title": "Filter & Sort",
"invalid_description": "A POI id is required to view POI details.",
"invalid_title": "Invalid POI",
"list_description": "Browse department POIs, filter by type, and jump back to the map.",
@@ -846,9 +847,11 @@
"loading_detail": "Loading POI details...",
"not_found": "POI not found",
"note_label": "Note",
+ "reset_filters": "Reset",
"results_count": "{{count}} POI",
"results_count_plural": "{{count}} POIs",
"route_error": "Failed to open maps application",
+ "search_placeholder": "Search POIs...",
"set_status_destination": "Set status destination",
"sort_label": "Sort",
"sort_name": "Name",
diff --git a/src/translations/es.json b/src/translations/es.json
index f056a30..c6f6db2 100644
--- a/src/translations/es.json
+++ b/src/translations/es.json
@@ -838,6 +838,7 @@
"empty_title": "No se encontraron POI",
"filter_all_types": "Todos los tipos",
"filter_label": "Filtro",
+ "filter_sort_title": "Filtrar y ordenar",
"invalid_description": "Se requiere un identificador de POI para ver los detalles.",
"invalid_title": "POI no válido",
"list_description": "Explora los POI del departamento, filtra por tipo y vuelve al mapa.",
@@ -846,9 +847,11 @@
"loading_detail": "Cargando detalles del POI...",
"not_found": "POI no encontrado",
"note_label": "Nota",
+ "reset_filters": "Restablecer",
"results_count": "{{count}} POI",
"results_count_plural": "{{count}} POIs",
"route_error": "No se pudo abrir la aplicación de mapas",
+ "search_placeholder": "Buscar POI...",
"set_status_destination": "Usar como destino del estado",
"sort_label": "Ordenar",
"sort_name": "Nombre",
diff --git a/src/translations/fr.json b/src/translations/fr.json
index 3090cd9..11d2dd0 100644
--- a/src/translations/fr.json
+++ b/src/translations/fr.json
@@ -838,6 +838,7 @@
"empty_title": "Aucun POI trouvé",
"filter_all_types": "Tous les types",
"filter_label": "Filtre",
+ "filter_sort_title": "Filtrer et trier",
"invalid_description": "Un identifiant de POI est requis pour afficher les détails.",
"invalid_title": "POI invalide",
"list_description": "Parcourez les POI du service, filtrez par type et revenez à la carte.",
@@ -846,9 +847,11 @@
"loading_detail": "Chargement des détails du POI...",
"not_found": "POI introuvable",
"note_label": "Remarque",
+ "reset_filters": "Réinitialiser",
"results_count": "{{count}} POI",
"results_count_plural": "{{count}} POIs",
"route_error": "Impossible d'ouvrir l'application de cartographie",
+ "search_placeholder": "Rechercher des POI...",
"set_status_destination": "Utiliser comme destination du statut",
"sort_label": "Trier",
"sort_name": "Nom",
diff --git a/src/translations/it.json b/src/translations/it.json
index 59e8b5f..d110cb6 100644
--- a/src/translations/it.json
+++ b/src/translations/it.json
@@ -838,6 +838,7 @@
"empty_title": "Nessun POI trovato",
"filter_all_types": "Tutti i tipi",
"filter_label": "Filtro",
+ "filter_sort_title": "Filtra e ordina",
"invalid_description": "Per visualizzare i dettagli del POI è necessario un ID POI.",
"invalid_title": "POI non valido",
"list_description": "Sfoglia i POI del dipartimento, filtra per tipo e torna alla mappa.",
@@ -846,9 +847,11 @@
"loading_detail": "Caricamento dettagli POI...",
"not_found": "POI non trovato",
"note_label": "Nota",
+ "reset_filters": "Reimposta",
"results_count": "{{count}} POI",
"results_count_plural": "{{count}} POIs",
"route_error": "Impossibile aprire l'app di mappe",
+ "search_placeholder": "Cerca POI...",
"set_status_destination": "Usa come destinazione dello stato",
"sort_label": "Ordina",
"sort_name": "Nome",
diff --git a/src/translations/pl.json b/src/translations/pl.json
index 42f0187..9280c29 100644
--- a/src/translations/pl.json
+++ b/src/translations/pl.json
@@ -838,6 +838,7 @@
"empty_title": "Nie znaleziono POI",
"filter_all_types": "Wszystkie typy",
"filter_label": "Filtr",
+ "filter_sort_title": "Filtruj i sortuj",
"invalid_description": "Aby wyświetlić szczegóły POI, wymagany jest identyfikator POI.",
"invalid_title": "Nieprawidłowy POI",
"list_description": "Przeglądaj POI działu, filtruj według typu i wróć do mapy.",
@@ -846,9 +847,11 @@
"loading_detail": "Ładowanie szczegółów POI...",
"not_found": "Nie znaleziono POI",
"note_label": "Notatka",
+ "reset_filters": "Resetuj",
"results_count": "{{count}} POI",
"results_count_plural": "{{count}} POIs",
"route_error": "Nie udało się otworzyć aplikacji map",
+ "search_placeholder": "Szukaj POI...",
"set_status_destination": "Użyj jako celu statusu",
"sort_label": "Sortuj",
"sort_name": "Nazwa",
diff --git a/src/translations/sv.json b/src/translations/sv.json
index 6097bfc..c16216c 100644
--- a/src/translations/sv.json
+++ b/src/translations/sv.json
@@ -838,6 +838,7 @@
"empty_title": "Inga POI:er hittades",
"filter_all_types": "Alla typer",
"filter_label": "Filter",
+ "filter_sort_title": "Filtrera och sortera",
"invalid_description": "Ett POI-id krävs för att visa POI-detaljer.",
"invalid_title": "Ogiltig POI",
"list_description": "Bläddra bland avdelningens POI:er, filtrera efter typ och hoppa tillbaka till kartan.",
@@ -846,9 +847,11 @@
"loading_detail": "Laddar POI-detaljer...",
"not_found": "POI hittades inte",
"note_label": "Anteckning",
+ "reset_filters": "Återställ",
"results_count": "{{count}} POI",
"results_count_plural": "{{count}} POIs",
"route_error": "Det gick inte att öppna kartappen",
+ "search_placeholder": "Sök POI:er...",
"set_status_destination": "Använd som statusdestination",
"sort_label": "Sortera",
"sort_name": "Namn",
diff --git a/src/translations/uk.json b/src/translations/uk.json
index 516a173..3becefb 100644
--- a/src/translations/uk.json
+++ b/src/translations/uk.json
@@ -838,6 +838,7 @@
"empty_title": "POI не знайдено",
"filter_all_types": "Усі типи",
"filter_label": "Фільтр",
+ "filter_sort_title": "Фільтр та сортування",
"invalid_description": "Щоб переглянути деталі POI, потрібен ідентифікатор POI.",
"invalid_title": "Недійсний POI",
"list_description": "Переглядайте POI підрозділу, фільтруйте за типом і повертайтеся до карти.",
@@ -846,9 +847,11 @@
"loading_detail": "Завантаження деталей POI...",
"not_found": "POI не знайдено",
"note_label": "Примітка",
+ "reset_filters": "Скинути",
"results_count": "{{count}} POI",
"results_count_plural": "{{count}} POIs",
"route_error": "Не вдалося відкрити програму карт",
+ "search_placeholder": "Пошук POI...",
"set_status_destination": "Використати як пункт призначення статусу",
"sort_label": "Сортувати",
"sort_name": "Назва",
diff --git a/yarn.lock b/yarn.lock
index fd3f259..aad1d37 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4101,144 +4101,150 @@
micromatch "^4.0.0"
p-reduce "^2.0.0"
-"@sentry-internal/browser-utils@10.12.0":
- version "10.12.0"
- resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-10.12.0.tgz#aa3a05653e530d2693e307c0131571ee8a97b60d"
- integrity sha512-dozbx389jhKynj0d657FsgbBVOar7pX3mb6GjqCxslXF0VKpZH2Xks0U32RgDY/nK27O+o095IWz7YvjVmPkDw==
+"@sentry-internal/browser-utils@10.51.0":
+ version "10.51.0"
+ resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-10.51.0.tgz#03f36703f13a9af7b593e24d0e9ab92aa6a0b601"
+ integrity sha512-lNKBS4P7RUvf1niojXQWe9bU3gnBUCbST4Dj0pSiyat1N96cXVyHkeE+uGxowD0RrVWhs+kGHiVX3FcmRWF6sA==
dependencies:
- "@sentry/core" "10.12.0"
+ "@sentry/core" "10.51.0"
-"@sentry-internal/feedback@10.12.0":
- version "10.12.0"
- resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-10.12.0.tgz#a48039507f37fe62e19566128a894661a724ef0d"
- integrity sha512-0+7ceO6yQPPqfxRc9ue/xoPHKcnB917ezPaehGQNfAFNQB9PNTG1y55+8mRu0Fw+ANbZeCt/HyoCmXuRdxmkpg==
+"@sentry-internal/feedback@10.51.0":
+ version "10.51.0"
+ resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-10.51.0.tgz#2119ee63d06a1f1efe0017e60c5718f995192c61"
+ integrity sha512-bCM95bcpphx28e6aU0bwRLxOgwosYsdNzezM1sM0pVOkb0TB3hDFRamramVDK+/Hp1o8qmRxS4c5w/A7YBZGkA==
dependencies:
- "@sentry/core" "10.12.0"
+ "@sentry/core" "10.51.0"
-"@sentry-internal/replay-canvas@10.12.0":
- version "10.12.0"
- resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-10.12.0.tgz#f79dde92bcba67b4f706db6c217467e14d6348c5"
- integrity sha512-W/z1/+69i3INNfPjD1KuinSNaRQaApjzwb37IFmiyF440F93hxmEYgXHk3poOlYYaigl2JMYbysGPWOiXnqUXA==
+"@sentry-internal/replay-canvas@10.51.0":
+ version "10.51.0"
+ resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-10.51.0.tgz#f20fc2703e155778562e42fcbfd1bce43e9adba2"
+ integrity sha512-8PW1Pp+Yl3lPwYqhBCr5SgkuhDanu9ZLzUqD2bPKL/ElqbM2eDVIWxq4z4ZzePrmZa6IcCjTv6sVQJ7Z4dLyLA==
dependencies:
- "@sentry-internal/replay" "10.12.0"
- "@sentry/core" "10.12.0"
+ "@sentry-internal/replay" "10.51.0"
+ "@sentry/core" "10.51.0"
-"@sentry-internal/replay@10.12.0":
- version "10.12.0"
- resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-10.12.0.tgz#47ca89acdc621217991c7ed1d133fd37915a512d"
- integrity sha512-/1093gSNGN5KlOBsuyAl33JkzGiG38kCnxswQLZWpPpR6LBbR1Ddb18HjhDpoQNNEZybJBgJC3a5NKl43C2TSQ==
+"@sentry-internal/replay@10.51.0":
+ version "10.51.0"
+ resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-10.51.0.tgz#2b2153104066de466b55a682b900ea31e60b11a0"
+ integrity sha512-jCpI5HXSwK6ZT2HX70+mDRciAocHzSiDk4DTgvzV69Wvd+Ei5WLgE+d39eaEPsm8lUC0Ydntb5sJIB6uG9D4bw==
dependencies:
- "@sentry-internal/browser-utils" "10.12.0"
- "@sentry/core" "10.12.0"
-
-"@sentry/babel-plugin-component-annotate@4.3.0":
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.3.0.tgz#c5b6cbb986952596d3ad233540a90a1fd18bad80"
- integrity sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw==
+ "@sentry-internal/browser-utils" "10.51.0"
+ "@sentry/core" "10.51.0"
-"@sentry/browser@10.12.0":
- version "10.12.0"
- resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.12.0.tgz#79dffc88e1f9241b9fdb5def5a7c6809f41230b3"
- integrity sha512-lKyaB2NFmr7SxPjmMTLLhQ7xfxaY3kdkMhpzuRI5qwOngtKt4+FtvNYHRuz+PTtEFv4OaHhNNbRn6r91gWguQg==
- dependencies:
- "@sentry-internal/browser-utils" "10.12.0"
- "@sentry-internal/feedback" "10.12.0"
- "@sentry-internal/replay" "10.12.0"
- "@sentry-internal/replay-canvas" "10.12.0"
- "@sentry/core" "10.12.0"
-
-"@sentry/cli-darwin@2.55.0":
- version "2.55.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.55.0.tgz#79513547d15223d51905e94d8c1e2bc7377cfdf1"
- integrity sha512-jGHE7SHHzqXUmnsmRLgorVH6nmMmTjQQXdPZbSL5tRtH8d3OIYrVNr5D72DSgD26XAPBDMV0ibqOQ9NKoiSpfA==
-
-"@sentry/cli-linux-arm64@2.55.0":
- version "2.55.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.55.0.tgz#5a2e8ea6e088f4884cbe272525f0781f8c748ff3"
- integrity sha512-jNB/0/gFcOuDCaY/TqeuEpsy/k52dwyk1SOV3s1ku4DUsln6govTppeAGRewY3T1Rj9B2vgIWTrnB8KVh9+Rgg==
-
-"@sentry/cli-linux-arm@2.55.0":
- version "2.55.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.55.0.tgz#54c330471c4b23ff6769bfd886f092afba3380ce"
- integrity sha512-ATjU0PsiWADSPLF/kZroLZ7FPKd5W9TDWHVkKNwIUNTei702LFgTjNeRwOIzTgSvG3yTmVEqtwFQfFN/7hnVXQ==
-
-"@sentry/cli-linux-i686@2.55.0":
- version "2.55.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.55.0.tgz#12c64453ef7014ba89c885497e3c9764a5f3e0a0"
- integrity sha512-8LZjo6PncTM6bWdaggscNOi5r7F/fqRREsCwvd51dcjGj7Kp1plqo9feEzYQ+jq+KUzVCiWfHrUjddFmYyZJrg==
-
-"@sentry/cli-linux-x64@2.55.0":
- version "2.55.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.55.0.tgz#c640464fe533fe87e57d0ca5585e0c504e0ef5b3"
- integrity sha512-5LUVvq74Yj2cZZy5g5o/54dcWEaX4rf3myTHy73AKhRj1PABtOkfexOLbF9xSrZy95WXWaXyeH+k5n5z/vtHfA==
-
-"@sentry/cli-win32-arm64@2.55.0":
- version "2.55.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.55.0.tgz#da1e8ba13083c281fcdf72d1dd35255df7fdb5c5"
- integrity sha512-cWIQdzm1pfLwPARsV6dUb8TVd6Y3V1A2VWxjTons3Ift6GvtVmiAe0OWL8t2Yt95i8v61kTD/6Tq21OAaogqzA==
-
-"@sentry/cli-win32-i686@2.55.0":
- version "2.55.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.55.0.tgz#c2eae0a75fc55f101c31de0999214d7e613d65c4"
- integrity sha512-ldepCn2t9r4I0wvgk7NRaA7coJyy4rTQAzM66u9j5nTEsUldf66xym6esd5ZZRAaJUjffqvHqUIr/lrieTIrVg==
-
-"@sentry/cli-win32-x64@2.55.0":
- version "2.55.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.55.0.tgz#499e6697663a3e1453ff2e921bf3876f774520b5"
- integrity sha512-4hPc/I/9tXx+HLTdTGwlagtAfDSIa2AoTUP30tl32NAYQhx9a6niUbPAemK2qfxesiufJ7D2djX83rCw6WnJVA==
-
-"@sentry/cli@2.55.0":
- version "2.55.0"
- resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.55.0.tgz#c22d2edbd320242e7881c15ecf89649378c05387"
- integrity sha512-cynvcIM2xL8ddwELyFRSpZQw4UtFZzoM2rId2l9vg7+wDREPDocMJB9lEQpBIo3eqhp9JswqUT037yjO6iJ5Sw==
- dependencies:
- https-proxy-agent "^5.0.0"
- node-fetch "^2.6.7"
+"@sentry/babel-plugin-component-annotate@5.2.1":
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-5.2.1.tgz#99ab8cc4e5482d9b1ad3d09671ce999c59f40bae"
+ integrity sha512-QQ9AL5EXIbSK26ObLVtiU6l3tCUdpGSJ/6VwDkPhC3qvtoksSlcoU9Yzm7XC0NBcvu1N2abL5R7gckKGZ4JewQ==
+
+"@sentry/browser@10.51.0":
+ version "10.51.0"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.51.0.tgz#d5ba0215b48651ad4f07c9865c8df04e517c584e"
+ integrity sha512-Zdc0sKfenxUtW/OGhtJ7xHFN44bXR7YqxJ1zBDzlZfW0nTbeTTUZBq9z5NUw6qdS0Vs/i3V4qzAKTbRKWfqSEA==
+ dependencies:
+ "@sentry-internal/browser-utils" "10.51.0"
+ "@sentry-internal/feedback" "10.51.0"
+ "@sentry-internal/replay" "10.51.0"
+ "@sentry-internal/replay-canvas" "10.51.0"
+ "@sentry/core" "10.51.0"
+
+"@sentry/cli-darwin@3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-3.4.1.tgz#71d7daed5d87f86ba479c3fbdafeb280bb22b909"
+ integrity sha512-44foor4g/nfFaOaEZYQnxBrAW7TOMO4LatYsRjPI8dAoqXNVsl+P77FIk0gGAFnTwbt5gREXeeOn6j8DA9NyZg==
+
+"@sentry/cli-linux-arm64@3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.4.1.tgz#bd75c2b4f50faa3faa65e4de996c982b1c01af95"
+ integrity sha512-rYWeBxVEiYMZ5hUe16qkpCmJQBc+lxT50sls/CqO5WTN3VlrSRlJsd+jMTKUNesM0j4PMEi82Xy7rovD8a+2tA==
+
+"@sentry/cli-linux-arm@3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-3.4.1.tgz#c1911c632d82ee3bd7a4ffae32497a087d6407e3"
+ integrity sha512-XIT4ICA86vwrZWfbvKRiY9HgMg1aJNv1VJgNuiWu/3ysk0H4U7U4rJl6SQNbthgNGpcxvFdXmHbujKv7VZdv5w==
+
+"@sentry/cli-linux-i686@3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-3.4.1.tgz#56532d100037ab6806aa658a59fcb79061dddf50"
+ integrity sha512-yirtGGummlarcrPmIm4cg5vEA5gYnL/GJ6FLV6yfq1zAeMzEWnl7kaWIVsoTZ8qBi7vmHpy0APlH5LXxK4QXNw==
+
+"@sentry/cli-linux-x64@3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-3.4.1.tgz#8df916f85d6def62ba2b42bdf5cb86621dedca05"
+ integrity sha512-r2wJq9Bt1eRFtnAo3vWACfAN3IdV5gRfLnH8rbtDsg7pqdM0MP7tgAjzJDLptLGP02ndPhJfeJ1mXjcDY1MqqQ==
+
+"@sentry/cli-win32-arm64@3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.4.1.tgz#793b7ee9f4ab12d6ba575199b3fa56341279b419"
+ integrity sha512-IWg2FeB9OzxDky3q885MqepN2QEujyGdbcCB0VbHB3zRpT2D2fbk87FIy64JGoZkPVmoI4d7Mbxa+XKFS4jlrw==
+
+"@sentry/cli-win32-i686@3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-3.4.1.tgz#8049f96619a33b645242ef73cc792ce04e57ac95"
+ integrity sha512-RlKYU1Cdyk0uqRa9llKA6yVxg2QK0CXX0bogv89lIfABgZ4o1o45zcwVmEQLrD5rrIQmUcIJHWysQVnSyydJkA==
+
+"@sentry/cli-win32-x64@3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-3.4.1.tgz#b17eeddb4dd92015c89400dace0a657e143966a1"
+ integrity sha512-MgKSglGeD+zOIEdbZrt+P9v9ExkMrHKwlIK8hnJA8qNVjmPuOW9yR4khzdVIYd3cdujAQQhLcV3gEB9ceR4PxQ==
+
+"@sentry/cli@3.4.1":
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-3.4.1.tgz#a1336010e8af55a613ed80dc5030f38bb012de11"
+ integrity sha512-xY9WcIg+/B/bJxY1KbP0XrDZGPQaFNjIOVJbXNbwVtRujiG6DEwVHqGJhADjPa8rilLQVDOUumVMwLbAUCmh6A==
+ dependencies:
progress "^2.0.3"
proxy-from-env "^1.1.0"
+ undici "^6.22.0"
which "^2.0.2"
optionalDependencies:
- "@sentry/cli-darwin" "2.55.0"
- "@sentry/cli-linux-arm" "2.55.0"
- "@sentry/cli-linux-arm64" "2.55.0"
- "@sentry/cli-linux-i686" "2.55.0"
- "@sentry/cli-linux-x64" "2.55.0"
- "@sentry/cli-win32-arm64" "2.55.0"
- "@sentry/cli-win32-i686" "2.55.0"
- "@sentry/cli-win32-x64" "2.55.0"
-
-"@sentry/core@10.12.0":
- version "10.12.0"
- resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.12.0.tgz#3f6a0f5c2f63f2c1761e3cf442a986d74adf6403"
- integrity sha512-Jrf0Yo7DvmI/ZQcvBnA0xKNAFkJlVC/fMlvcin+5IrFNRcqOToZ2vtF+XqTgjRZymXQNE8s1QTD7IomPHk0TAw==
-
-"@sentry/react-native@~7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-7.2.0.tgz#d29deacc36fd55fc52119ffcf53f61c21cab3600"
- integrity sha512-rjqYgEjntPz1sPysud78wi4B9ui7LBVPsG6qr8s/htLMYho9GPGFA5dF+eqsQWqMX8NDReAxNkLTC4+gCNklLQ==
- dependencies:
- "@sentry/babel-plugin-component-annotate" "4.3.0"
- "@sentry/browser" "10.12.0"
- "@sentry/cli" "2.55.0"
- "@sentry/core" "10.12.0"
- "@sentry/react" "10.12.0"
- "@sentry/types" "10.12.0"
-
-"@sentry/react@10.12.0":
- version "10.12.0"
- resolved "https://registry.yarnpkg.com/@sentry/react/-/react-10.12.0.tgz#c121f37bf582f4851108f67ef492de6a4c8f7a8d"
- integrity sha512-TpqgdoYbkf5JynmmW2oQhHQ/h5w+XPYk0cEb/UrsGlvJvnBSR+5tgh0AqxCSi3gvtp82rAXI5w1TyRPBbhLDBw==
- dependencies:
- "@sentry/browser" "10.12.0"
- "@sentry/core" "10.12.0"
- hoist-non-react-statics "^3.3.2"
-
-"@sentry/types@10.12.0":
- version "10.12.0"
- resolved "https://registry.yarnpkg.com/@sentry/types/-/types-10.12.0.tgz#86d24346efde7b0757474537af7ae4b3d931d6a8"
- integrity sha512-sKGj3l3V8ZKISh2Tu88bHfnm5ztkRtSLdmpZ6TmCeJdSM9pV+RRd6CMJ0RnSEXmYHselPNUod521t2NQFd4W1w==
- dependencies:
- "@sentry/core" "10.12.0"
+ "@sentry/cli-darwin" "3.4.1"
+ "@sentry/cli-linux-arm" "3.4.1"
+ "@sentry/cli-linux-arm64" "3.4.1"
+ "@sentry/cli-linux-i686" "3.4.1"
+ "@sentry/cli-linux-x64" "3.4.1"
+ "@sentry/cli-win32-arm64" "3.4.1"
+ "@sentry/cli-win32-i686" "3.4.1"
+ "@sentry/cli-win32-x64" "3.4.1"
+
+"@sentry/core@10.51.0":
+ version "10.51.0"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.51.0.tgz#bf103c6f03882311b93258f5424b777b24d4fb4f"
+ integrity sha512-Y45V/YXvVLEXmOdkbD1oG1gkRWFi9guCEGg3PlIlIpRjAbZUrvLGgjRJIc1E7XpSzmOnWbs5BbUxMv4PDaPj2w==
+
+"@sentry/expo-upload-sourcemaps@8.10.0":
+ version "8.10.0"
+ resolved "https://registry.yarnpkg.com/@sentry/expo-upload-sourcemaps/-/expo-upload-sourcemaps-8.10.0.tgz#d83bfa749e14c356b352232b111b7cf819059a6a"
+ integrity sha512-9nca3zuzeohl77Hspkox0CcpCQz11gvplgJMktD0fVLrHKBLW9/KTtAOBSez7FfXe2e8FbF7cd5/Cb5EHyJjpw==
+ dependencies:
+ "@sentry/cli" "3.4.1"
+
+"@sentry/react-native@~8.10.0":
+ version "8.10.0"
+ resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-8.10.0.tgz#eeaf79b53758aeee5e993276c616372cfe19a206"
+ integrity sha512-Pfr7h1unqMsE87UMwaUIZ26VjX7SSsitBLpK4gHeIwYmuXn+qfdYUmme6RnoLlL5IPzu8pCLoRNCvdAJy6eTgw==
+ dependencies:
+ "@sentry/babel-plugin-component-annotate" "5.2.1"
+ "@sentry/browser" "10.51.0"
+ "@sentry/cli" "3.4.1"
+ "@sentry/core" "10.51.0"
+ "@sentry/expo-upload-sourcemaps" "8.10.0"
+ "@sentry/react" "10.51.0"
+ "@sentry/types" "10.51.0"
+
+"@sentry/react@10.51.0":
+ version "10.51.0"
+ resolved "https://registry.yarnpkg.com/@sentry/react/-/react-10.51.0.tgz#2c65becc65c4fd0a77470fda00af778dfd94977c"
+ integrity sha512-RRHHqjNvjji6ebIqdlAr453AkST8Vm4cxdu1vWm772IgbzTO7Jx46Cj6Bt2/GjMyH0YLE5euDaAOQhFMmpvAOw==
+ dependencies:
+ "@sentry/browser" "10.51.0"
+ "@sentry/core" "10.51.0"
+
+"@sentry/types@10.51.0":
+ version "10.51.0"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-10.51.0.tgz#1408ccac89fdbffaefeabac9ad9e7b1f0494458b"
+ integrity sha512-/0nTcXk82RKtGGv0mxmY56o+BE85lBuSWG9chtSEfeypvxHFyWn3D7td9rPmjboDMtytC24cYbUzx55jb2OjQA==
+ dependencies:
+ "@sentry/core" "10.51.0"
"@shopify/flash-list@2.0.2":
version "2.0.2"
@@ -6488,10 +6494,10 @@ cosmiconfig@^9.0.0:
js-yaml "^4.1.0"
parse-json "^5.2.0"
-countly-sdk-react-native-bridge@^25.4.0:
- version "25.4.0"
- resolved "https://registry.yarnpkg.com/countly-sdk-react-native-bridge/-/countly-sdk-react-native-bridge-25.4.0.tgz#dd04086142becf41b4312c8fe361db87b235e04d"
- integrity sha512-MIkQtb5UfWW7FhC7pB6luudlfdTk0YA42YCKtnAwH+0gcm4jkMMuqq0HLytqFWki9fcCzfyatz+HGIu5s5mKvA==
+countly-sdk-react-native-bridge@^25.4.1:
+ version "25.4.1"
+ resolved "https://registry.yarnpkg.com/countly-sdk-react-native-bridge/-/countly-sdk-react-native-bridge-25.4.1.tgz#068485670d6d0920e3993171d1c2f5550d5128c2"
+ integrity sha512-6rwQ2TIfh+F1zKsTpat5XtW8v/GQb5SV4Q1Ly0SDpyfsvLfFLh72DaEHzdjRnP5qOMWnG38AICPrz9Fm3DzY/w==
create-jest@^29.7.0:
version "29.7.0"
@@ -8798,7 +8804,7 @@ hey-listen@^1.0.8:
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
-hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -8866,7 +8872,7 @@ http-proxy-agent@^5.0.0:
agent-base "6"
debug "4"
-https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1:
+https-proxy-agent@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
@@ -14779,6 +14785,11 @@ undici@^6.18.2:
resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.3.tgz#185752ad92c3d0efe7a7d1f6854a50f83b552d7a"
integrity sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==
+undici@^6.22.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/undici/-/undici-6.25.0.tgz#8c4efb8c998dc187fc1cfb5dde1ef19a211849fb"
+ integrity sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==
+
unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2"