Skip to content

Commit 6f9562c

Browse files
committed
RU-T45 Protocols list fix and contact sheet email phone and address fix.
1 parent b8e12bf commit 6f9562c

13 files changed

Lines changed: 389 additions & 49 deletions

File tree

src/app/(app)/__tests__/protocols.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jest.mock('@/components/protocols/protocol-card', () => ({
3232
ProtocolCard: ({ protocol, onPress }: { protocol: any; onPress: (id: string) => void }) => {
3333
const { Pressable, Text } = require('react-native');
3434
return (
35-
<Pressable testID={`protocol-card-${protocol.Id}`} onPress={() => onPress(protocol.Id)}>
35+
<Pressable testID={`protocol-card-${protocol.ProtocolId}`} onPress={() => onPress(protocol.ProtocolId)}>
3636
<Text>{protocol.Name}</Text>
3737
</Pressable>
3838
);
@@ -108,7 +108,7 @@ jest.mock('@/stores/protocols/store', () => ({
108108
// Mock protocols test data
109109
const mockProtocols: CallProtocolsResultData[] = [
110110
{
111-
Id: '1',
111+
ProtocolId: '1',
112112
DepartmentId: 'dept1',
113113
Name: 'Fire Emergency Response',
114114
Code: 'FIRE001',
@@ -126,7 +126,7 @@ const mockProtocols: CallProtocolsResultData[] = [
126126
Questions: [],
127127
},
128128
{
129-
Id: '2',
129+
ProtocolId: '2',
130130
DepartmentId: 'dept1',
131131
Name: 'Medical Emergency',
132132
Code: 'MED001',
@@ -144,7 +144,7 @@ const mockProtocols: CallProtocolsResultData[] = [
144144
Questions: [],
145145
},
146146
{
147-
Id: '3',
147+
ProtocolId: '3',
148148
DepartmentId: 'dept1',
149149
Name: 'Hazmat Response',
150150
Code: 'HAZ001',
@@ -162,7 +162,7 @@ const mockProtocols: CallProtocolsResultData[] = [
162162
Questions: [],
163163
},
164164
{
165-
Id: '', // Empty ID to test the keyExtractor fix
165+
ProtocolId: '', // Empty ID to test the keyExtractor fix
166166
DepartmentId: 'dept1',
167167
Name: 'Protocol with Empty ID',
168168
Code: 'EMPTY001',

src/app/(app)/protocols.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ export default function Protocols() {
3939
setRefreshing(false);
4040
}, [fetchProtocols]);
4141

42+
const handleProtocolPress = React.useCallback(
43+
(id: string) => {
44+
selectProtocol(id);
45+
},
46+
[selectProtocol]
47+
);
48+
4249
const filteredProtocols = React.useMemo(() => {
4350
if (!searchQuery.trim()) return protocols;
4451

@@ -69,11 +76,13 @@ export default function Protocols() {
6976
<FlatList
7077
testID="protocols-list"
7178
data={filteredProtocols}
72-
keyExtractor={(item, index) => item.Id || `protocol-${index}`}
73-
renderItem={({ item }) => <ProtocolCard protocol={item} onPress={selectProtocol} />}
79+
keyExtractor={(item, index) => item.ProtocolId || `protocol-${index}`}
80+
renderItem={({ item }) => <ProtocolCard protocol={item} onPress={handleProtocolPress} />}
7481
showsVerticalScrollIndicator={false}
7582
contentContainerStyle={{ paddingBottom: 100 }}
7683
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
84+
extraData={handleProtocolPress}
85+
estimatedItemSize={120}
7786
/>
7887
) : (
7988
<ZeroState icon={FileText} heading={t('protocols.empty')} description={t('protocols.emptyDescription')} />

src/app/(app)/settings.tsx

Lines changed: 261 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable react/react-in-jsx-scope */
22
import { Env } from '@env';
33
import { useColorScheme } from 'nativewind';
4-
import React, { useEffect } from 'react';
4+
import React, { useCallback, useEffect } from 'react';
55
import { useTranslation } from 'react-i18next';
66

77
import { BackgroundGeolocationItem } from '@/components/settings/background-geolocation-item';
@@ -15,16 +15,35 @@ import { ThemeItem } from '@/components/settings/theme-item';
1515
import { ToggleItem } from '@/components/settings/toggle-item';
1616
import { UnitSelectionBottomSheet } from '@/components/settings/unit-selection-bottom-sheet';
1717
import { FocusAwareStatusBar, ScrollView } from '@/components/ui';
18+
import { AlertDialog, AlertDialogBackdrop, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader } from '@/components/ui/alert-dialog';
1819
import { Box } from '@/components/ui/box';
20+
import { Button, ButtonText } from '@/components/ui/button';
1921
import { Card } from '@/components/ui/card';
2022
import { Heading } from '@/components/ui/heading';
23+
import { Text } from '@/components/ui/text';
2124
import { VStack } from '@/components/ui/vstack';
2225
import { useAnalytics } from '@/hooks/use-analytics';
2326
import { useAuth, useAuthStore } from '@/lib';
2427
import { logger } from '@/lib/logging';
25-
import { getBaseApiUrl } from '@/lib/storage/app';
28+
import { storage } from '@/lib/storage';
29+
import { getBaseApiUrl, removeActiveCallId, removeActiveUnitId, removeDeviceUuid } from '@/lib/storage/app';
2630
import { openLinkInBrowser } from '@/lib/utils';
31+
import { useAudioStreamStore } from '@/stores/app/audio-stream-store';
32+
import { useBluetoothAudioStore } from '@/stores/app/bluetooth-audio-store';
2733
import { useCoreStore } from '@/stores/app/core-store';
34+
import { useLiveKitStore } from '@/stores/app/livekit-store';
35+
import { useLoadingStore } from '@/stores/app/loading-store';
36+
import { useLocationStore } from '@/stores/app/location-store';
37+
import { useCallsStore } from '@/stores/calls/store';
38+
import { useContactsStore } from '@/stores/contacts/store';
39+
import { useDispatchStore } from '@/stores/dispatch/store';
40+
import { useNotesStore } from '@/stores/notes/store';
41+
import { useOfflineQueueStore } from '@/stores/offline-queue/store';
42+
import { useProtocolsStore } from '@/stores/protocols/store';
43+
import { usePushNotificationModalStore } from '@/stores/push-notification/store';
44+
import { useRolesStore } from '@/stores/roles/store';
45+
import { securityStore } from '@/stores/security/store';
46+
import { useStatusBottomSheetStore } from '@/stores/status/store';
2847
import { useUnitsStore } from '@/stores/units/store';
2948

3049
export default function Settings() {
@@ -36,6 +55,7 @@ export default function Settings() {
3655
const { login, status, isAuthenticated } = useAuth();
3756
const [showServerUrl, setShowServerUrl] = React.useState(false);
3857
const [showUnitSelection, setShowUnitSelection] = React.useState(false);
58+
const [showLogoutConfirm, setShowLogoutConfirm] = React.useState(false);
3959
const activeUnit = useCoreStore((state) => state.activeUnit);
4060
const { units } = useUnitsStore();
4161

@@ -44,6 +64,223 @@ export default function Settings() {
4464
return activeUnit?.Name || t('common.unknown');
4565
}, [activeUnit, t]);
4666

67+
/**
68+
* Clears all app data, cached values, settings, and stores
69+
* Called when user confirms logout
70+
*/
71+
const clearAllAppData = useCallback(async () => {
72+
logger.info({
73+
message: 'Clearing all app data on logout',
74+
});
75+
76+
try {
77+
// Clear persisted storage items
78+
removeActiveUnitId();
79+
removeActiveCallId();
80+
removeDeviceUuid();
81+
82+
// Clear all MMKV storage except first time flag and user preferences
83+
const allKeys = storage.getAllKeys();
84+
const keysToPreserve = ['IS_FIRST_TIME'];
85+
allKeys.forEach((key) => {
86+
if (!keysToPreserve.includes(key)) {
87+
storage.delete(key);
88+
}
89+
});
90+
91+
// Reset all zustand stores to their initial states
92+
// Core stores
93+
useCoreStore.setState({
94+
activeUnitId: null,
95+
activeUnit: null,
96+
activeUnitStatus: null,
97+
activeUnitStatusType: null,
98+
activeCallId: null,
99+
activeCall: null,
100+
activePriority: null,
101+
config: null,
102+
isLoading: false,
103+
isInitialized: false,
104+
isInitializing: false,
105+
error: null,
106+
activeStatuses: null,
107+
});
108+
109+
// Calls store
110+
useCallsStore.setState({
111+
calls: [],
112+
callPriorities: [],
113+
callTypes: [],
114+
isLoading: false,
115+
error: null,
116+
});
117+
118+
// Units store
119+
useUnitsStore.setState({
120+
units: [],
121+
unitStatuses: [],
122+
isLoading: false,
123+
error: null,
124+
});
125+
126+
// Contacts store
127+
useContactsStore.setState({
128+
contacts: [],
129+
contactNotes: {},
130+
searchQuery: '',
131+
selectedContactId: null,
132+
isDetailsOpen: false,
133+
isLoading: false,
134+
isNotesLoading: false,
135+
error: null,
136+
});
137+
138+
// Notes store
139+
useNotesStore.setState({
140+
notes: [],
141+
searchQuery: '',
142+
selectedNoteId: null,
143+
isDetailsOpen: false,
144+
isLoading: false,
145+
error: null,
146+
});
147+
148+
// Roles store
149+
useRolesStore.setState({
150+
roles: [],
151+
unitRoleAssignments: [],
152+
users: [],
153+
isLoading: false,
154+
error: null,
155+
});
156+
157+
// Protocols store
158+
useProtocolsStore.setState({
159+
protocols: [],
160+
searchQuery: '',
161+
selectedProtocolId: null,
162+
isDetailsOpen: false,
163+
isLoading: false,
164+
error: null,
165+
});
166+
167+
// Dispatch store
168+
useDispatchStore.setState({
169+
data: {
170+
users: [],
171+
groups: [],
172+
roles: [],
173+
units: [],
174+
},
175+
selection: {
176+
everyone: false,
177+
users: [],
178+
groups: [],
179+
roles: [],
180+
units: [],
181+
},
182+
isLoading: false,
183+
error: null,
184+
searchQuery: '',
185+
});
186+
187+
// Security store
188+
securityStore.setState({
189+
error: null,
190+
rights: null,
191+
});
192+
193+
// Status bottom sheet store
194+
useStatusBottomSheetStore.getState().reset();
195+
196+
// Offline queue store
197+
useOfflineQueueStore.getState().clearAllEvents();
198+
199+
// Loading store
200+
useLoadingStore.getState().resetLoading();
201+
202+
// Location store
203+
useLocationStore.setState({
204+
latitude: null,
205+
longitude: null,
206+
heading: null,
207+
accuracy: null,
208+
speed: null,
209+
altitude: null,
210+
timestamp: null,
211+
});
212+
213+
// LiveKit store - disconnect and reset
214+
const liveKitState = useLiveKitStore.getState();
215+
if (liveKitState.isConnected) {
216+
liveKitState.disconnectFromRoom();
217+
}
218+
useLiveKitStore.setState({
219+
isConnected: false,
220+
isConnecting: false,
221+
currentRoom: null,
222+
currentRoomInfo: null,
223+
isTalking: false,
224+
availableRooms: [],
225+
isBottomSheetVisible: false,
226+
});
227+
228+
// Audio stream store - cleanup and reset
229+
const audioStreamState = useAudioStreamStore.getState();
230+
await audioStreamState.cleanup();
231+
useAudioStreamStore.setState({
232+
availableStreams: [],
233+
currentStream: null,
234+
isPlaying: false,
235+
isLoading: false,
236+
isBuffering: false,
237+
isBottomSheetVisible: false,
238+
});
239+
240+
// Bluetooth audio store
241+
useBluetoothAudioStore.setState({
242+
connectedDevice: null,
243+
isScanning: false,
244+
isConnecting: false,
245+
availableDevices: [],
246+
connectionError: null,
247+
isAudioRoutingActive: false,
248+
});
249+
250+
// Push notification modal store
251+
usePushNotificationModalStore.setState({
252+
isOpen: false,
253+
notification: null,
254+
});
255+
256+
logger.info({
257+
message: 'Successfully cleared all app data',
258+
});
259+
} catch (error) {
260+
logger.error({
261+
message: 'Error clearing app data on logout',
262+
context: { error },
263+
});
264+
}
265+
}, []);
266+
267+
/**
268+
* Handles logout confirmation - clears all data and signs out
269+
*/
270+
const handleLogoutConfirm = useCallback(async () => {
271+
setShowLogoutConfirm(false);
272+
273+
trackEvent('user_logout_confirmed', {
274+
hadActiveUnit: !!activeUnit,
275+
});
276+
277+
// Clear all app data first
278+
await clearAllAppData();
279+
280+
// Then sign out
281+
await signOut();
282+
}, [clearAllAppData, signOut, trackEvent, activeUnit]);
283+
47284
const handleLoginInfoSubmit = async (data: { username: string; password: string }) => {
48285
logger.info({
49286
message: 'Updating login info',
@@ -89,7 +326,7 @@ export default function Settings() {
89326
<Item text={t('settings.server')} value={getBaseApiUrl()} onPress={() => setShowServerUrl(true)} textStyle="text-info-600" />
90327
<Item text={t('settings.login_info')} onPress={() => setShowLoginInfo(true)} textStyle="text-info-600" />
91328
<Item text={t('settings.active_unit')} value={activeUnitName} onPress={() => setShowUnitSelection(true)} textStyle="text-info-600" />
92-
<Item text={t('settings.logout')} onPress={signOut} textStyle="text-error-600" />
329+
<Item text={t('settings.logout')} onPress={() => setShowLogoutConfirm(true)} textStyle="text-error-600" />
93330
</VStack>
94331
</Card>
95332

@@ -122,6 +359,27 @@ export default function Settings() {
122359
<LoginInfoBottomSheet isOpen={showLoginInfo} onClose={() => setShowLoginInfo(false)} onSubmit={handleLoginInfoSubmit} />
123360
<ServerUrlBottomSheet isOpen={showServerUrl} onClose={() => setShowServerUrl(false)} />
124361
<UnitSelectionBottomSheet isOpen={showUnitSelection} onClose={() => setShowUnitSelection(false)} />
362+
363+
{/* Logout Confirmation Dialog */}
364+
<AlertDialog isOpen={showLogoutConfirm} onClose={() => setShowLogoutConfirm(false)}>
365+
<AlertDialogBackdrop />
366+
<AlertDialogContent>
367+
<AlertDialogHeader>
368+
<Heading size="lg">{t('settings.logout_confirm_title')}</Heading>
369+
</AlertDialogHeader>
370+
<AlertDialogBody>
371+
<Text>{t('settings.logout_confirm_message')}</Text>
372+
</AlertDialogBody>
373+
<AlertDialogFooter>
374+
<Button variant="outline" action="secondary" onPress={() => setShowLogoutConfirm(false)}>
375+
<ButtonText>{t('common.cancel')}</ButtonText>
376+
</Button>
377+
<Button action="negative" onPress={handleLogoutConfirm}>
378+
<ButtonText>{t('settings.logout')}</ButtonText>
379+
</Button>
380+
</AlertDialogFooter>
381+
</AlertDialogContent>
382+
</AlertDialog>
125383
</Box>
126384
);
127385
}

0 commit comments

Comments
 (0)