From b8e12bfac57f383d8aae49eaa2d5036b0d77d467 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Thu, 15 Jan 2026 18:29:19 -0800 Subject: [PATCH 1/3] RU-T45 Bottom sheet soft keyboard issues, logout fix, action button fix. --- expo-env.d.ts | 2 +- src/api/common/client.tsx | 26 ++- .../calls/close-call-bottom-sheet.tsx | 107 ++++++------ .../settings/unit-selection-bottom-sheet.tsx | 71 ++++---- src/components/sidebar/sidebar.tsx | 12 +- src/components/status/status-bottom-sheet.tsx | 106 +++++++----- src/stores/auth/store.tsx | 154 +++++++++++++++--- src/translations/ar.json | 1 + src/translations/en.json | 1 + src/translations/es.json | 1 + 10 files changed, 328 insertions(+), 153 deletions(-) diff --git a/expo-env.d.ts b/expo-env.d.ts index 5411fdde..bf3c1693 100644 --- a/expo-env.d.ts +++ b/expo-env.d.ts @@ -1,3 +1,3 @@ /// -// NOTE: This file should not be edited and should be in your git ignore \ No newline at end of file +// NOTE: This file should not be edited and should be in your git ignore diff --git a/src/api/common/client.tsx b/src/api/common/client.tsx index 2eda70c1..cd53425d 100644 --- a/src/api/common/client.tsx +++ b/src/api/common/client.tsx @@ -98,12 +98,26 @@ axiosInstance.interceptors.response.use( return axiosInstance(originalRequest); } catch (refreshError) { processQueue(refreshError as Error); - // Handle refresh token failure - useAuthStore.getState().logout(); - logger.error({ - message: 'Token refresh failed', - context: { error: refreshError }, - }); + + // Check if it's a network error vs an invalid refresh token + const isNetworkError = + refreshError instanceof Error && + (refreshError.message.includes('Network Error') || refreshError.message.includes('timeout') || refreshError.message.includes('ECONNREFUSED') || refreshError.message.includes('ETIMEDOUT')); + + if (!isNetworkError) { + // Only logout for non-network errors (e.g., invalid refresh token, 400/401 from token endpoint) + logger.error({ + message: 'Token refresh failed with non-recoverable error, logging out user', + context: { error: refreshError }, + }); + useAuthStore.getState().logout(); + } else { + logger.warn({ + message: 'Token refresh failed due to network error', + context: { error: refreshError }, + }); + } + return Promise.reject(refreshError); } finally { isRefreshing = false; diff --git a/src/components/calls/close-call-bottom-sheet.tsx b/src/components/calls/close-call-bottom-sheet.tsx index 1ae40f6c..ccf7d8a9 100644 --- a/src/components/calls/close-call-bottom-sheet.tsx +++ b/src/components/calls/close-call-bottom-sheet.tsx @@ -1,9 +1,9 @@ import { useRouter } from 'expo-router'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useWindowDimensions } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; -import { CustomBottomSheet } from '@/components/ui/bottom-sheet'; +import { Actionsheet, ActionsheetBackdrop, ActionsheetContent, ActionsheetDragIndicator, ActionsheetDragIndicatorWrapper } from '@/components/ui/actionsheet'; import { Button, ButtonText } from '@/components/ui/button'; import { FormControl, FormControlLabel, FormControlLabelText } from '@/components/ui/form-control'; import { HStack } from '@/components/ui/hstack'; @@ -26,8 +26,6 @@ interface CloseCallBottomSheetProps { export const CloseCallBottomSheet: React.FC = ({ isOpen, onClose, callId, isLoading = false }) => { const { t } = useTranslation(); const router = useRouter(); - const { width, height } = useWindowDimensions(); - const isLandscape = width > height; const showToast = useToastStore((state) => state.showToast); const { trackEvent } = useAnalytics(); const { closeCall } = useCallDetailStore(); @@ -90,52 +88,59 @@ export const CloseCallBottomSheet: React.FC = ({ isOp const isButtonDisabled = isLoading || isSubmitting; return ( - - - {t('call_detail.close_call')} - - - - {t('call_detail.close_call_type')} - - - - - - - {t('call_detail.close_call_note')} - - - - - - - - - - + + + + + + + + + + {t('call_detail.close_call')} + + + + {t('call_detail.close_call_type')} + + + + + + {t('call_detail.close_call_note')} + + + + + + + + + + + ); }; diff --git a/src/components/settings/unit-selection-bottom-sheet.tsx b/src/components/settings/unit-selection-bottom-sheet.tsx index e08e2a17..6f3230d0 100644 --- a/src/components/settings/unit-selection-bottom-sheet.tsx +++ b/src/components/settings/unit-selection-bottom-sheet.tsx @@ -139,39 +139,48 @@ export const UnitSelectionBottomSheet = React.memo - - {isLoadingUnits ? ( -
+ {isLoading ? ( +
+ -
- ) : units.length > 0 ? ( - - {units.map((unit) => ( - handleUnitSelection(unit)} - disabled={isLoading} - className={activeUnit?.UnitId === unit.UnitId ? 'data-[checked=true]:bg-background-100' : ''} - testID={`unit-item-${unit.UnitId}`} - > - - - {unit.Name} - - - {unit.Type} - - - {activeUnit?.UnitId === unit.UnitId && } - - ))} + {t('settings.activating_unit')} - ) : ( -
- {t('settings.no_units_available')} -
- )} - +
+ ) : ( + + {isLoadingUnits ? ( +
+ +
+ ) : units.length > 0 ? ( + + {units.map((unit) => ( + handleUnitSelection(unit)} + disabled={isLoading} + className={activeUnit?.UnitId === unit.UnitId ? 'data-[checked=true]:bg-background-100' : ''} + testID={`unit-item-${unit.UnitId}`} + > + + + {unit.Name} + + + {unit.Type} + + + {activeUnit?.UnitId === unit.UnitId && } + + ))} + + ) : ( +
+ {t('settings.no_units_available')} +
+ )} +
+ )} {/* Cancel Button - Fixed to bottom */} diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx index 1352543c..38b2e9de 100644 --- a/src/components/sidebar/sidebar.tsx +++ b/src/components/sidebar/sidebar.tsx @@ -67,7 +67,7 @@ const Sidebar = () => { ))} diff --git a/src/components/status/status-bottom-sheet.tsx b/src/components/status/status-bottom-sheet.tsx index 332bbd4f..f0b24174 100644 --- a/src/components/status/status-bottom-sheet.tsx +++ b/src/components/status/status-bottom-sheet.tsx @@ -3,6 +3,7 @@ import { useColorScheme } from 'nativewind'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { ScrollView, TouchableOpacity } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; import { invertColor } from '@/lib/utils'; import { type CustomStatusResultData } from '@/models/v4/customStatuses/customStatusResultData'; @@ -504,7 +505,10 @@ export const StatusBottomSheet = () => {
- + + + ) : ( + + )} + + {cameFromStatusSelection ? ( + + ) : ( + + )} + + )} {currentStep === 'add-note' && ( - - {/* Selected Status */} - - {t('status.selected_status')}: - - - {selectedStatus?.Text} - + + + {/* Selected Status */} + + {t('status.selected_status')}: + + + {selectedStatus?.Text} + + - - {/* Selected Destination */} - - {t('status.selected_destination')}: - {getSelectedDestinationDisplay()} - + {/* Selected Destination */} + + {t('status.selected_destination')}: + {getSelectedDestinationDisplay()} + - - - {t('status.note')} {isNoteRequired ? '' : `(${t('common.optional')})`}: - - - + + + {t('status.note')} {isNoteRequired ? '' : `(${t('common.optional')})`}: + + + - - - - - + + + + + + )} diff --git a/src/stores/auth/store.tsx b/src/stores/auth/store.tsx index efdc6b95..e5850aa0 100644 --- a/src/stores/auth/store.tsx +++ b/src/stores/auth/store.tsx @@ -57,7 +57,14 @@ const useAuthStore = create()( // .getTime() // .toString(); - //setTimeout(() => get().refreshAccessToken(), expiresIn); + // Schedule proactive token refresh before expiry + // expires_in is in seconds, so convert to milliseconds and refresh 1 minute before expiry + const refreshDelayMs = Math.max((response.authResponse!.expires_in - 60) * 1000, 60000); + logger.info({ + message: 'Login successful, scheduling token refresh', + context: { refreshDelayMs, expiresInSeconds: response.authResponse!.expires_in }, + }); + setTimeout(() => get().refreshAccessToken(), refreshDelayMs); } else { set({ status: 'error', @@ -87,11 +94,18 @@ const useAuthStore = create()( try { const { refreshToken } = get(); if (!refreshToken) { - throw new Error('No refresh token available'); + logger.warn({ + message: 'No refresh token available, logging out user', + }); + get().logout(); + return; } const response = await refreshTokenRequest(refreshToken); + // Update the stored auth response for hydration + setItem('authResponse', response); + set({ accessToken: response.access_token, refreshToken: response.refresh_token, @@ -99,39 +113,94 @@ const useAuthStore = create()( error: null, }); - // Set up next token refresh - //const decodedToken: { exp: number } = jwt_decode( - // response.access_token - //); - const expiresIn = response.expires_in * 1000 - Date.now() - 60000; // Refresh 1 minute before expiry - setTimeout(() => get().refreshAccessToken(), expiresIn); + // Set up next token refresh - refresh 1 minute before expiry + // expires_in is in seconds, so convert to milliseconds + const refreshDelayMs = Math.max((response.expires_in - 60) * 1000, 60000); // At least 1 minute + logger.info({ + message: 'Token refreshed successfully, scheduling next refresh', + context: { refreshDelayMs, expiresInSeconds: response.expires_in }, + }); + setTimeout(() => get().refreshAccessToken(), refreshDelayMs); } catch (error) { - // If refresh fails, log out the user - get().logout(); + // Check if it's a network error vs an invalid refresh token + const isNetworkError = + error instanceof Error && + (error.message.includes('Network Error') || + error.message.includes('timeout') || + error.message.includes('ECONNREFUSED') || + error.message.includes('ETIMEDOUT')); + + if (isNetworkError) { + // Network error - retry after a delay, don't logout + logger.warn({ + message: 'Token refresh failed due to network error, will retry', + context: { error: error instanceof Error ? error.message : String(error) }, + }); + // Retry after 30 seconds for network errors + setTimeout(() => get().refreshAccessToken(), 30000); + } else { + // Invalid refresh token or server rejected it - logout user + logger.error({ + message: 'Token refresh failed with non-recoverable error, logging out user', + context: { error: error instanceof Error ? error.message : String(error) }, + }); + get().logout(); + } } }, hydrate: () => { try { const authResponse = getAuth(); - if (authResponse !== null) { - const payload = sanitizeJson(base64.decode(authResponse!.id_token!.split('.')[1])); + if (authResponse !== null && authResponse.refresh_token) { + // We have stored auth data, try to restore the session + try { + const payload = sanitizeJson(base64.decode(authResponse.id_token!.split('.')[1])); + const profileData = JSON.parse(payload) as ProfileModel; - const profileData = JSON.parse(payload) as ProfileModel; + set({ + accessToken: authResponse.access_token, + refreshToken: authResponse.refresh_token, + status: 'signedIn', + error: null, + profile: profileData, + userId: profileData.sub, + }); - set({ - accessToken: authResponse.access_token, - refreshToken: authResponse.refresh_token, - status: 'signedIn', - error: null, - profile: profileData, - userId: profileData.sub, - }); + logger.info({ + message: 'Auth state hydrated from storage, scheduling token refresh', + }); + + // Schedule an immediate token refresh to ensure we have a valid access token + // Use a small delay to allow the app to fully initialize + setTimeout(() => get().refreshAccessToken(), 1000); + } catch (parseError) { + // Token parsing failed, but we have a refresh token - try to refresh + logger.warn({ + message: 'Failed to parse stored token, attempting refresh', + context: { error: parseError instanceof Error ? parseError.message : String(parseError) }, + }); + + set({ + refreshToken: authResponse.refresh_token, + status: 'loading', + }); + + // Attempt to refresh the token + get().refreshAccessToken(); + } } else { + logger.info({ + message: 'No stored auth data found, user needs to login', + }); get().logout(); } } catch (e) { - // catch error here - // Maybe sign_out user! + logger.error({ + message: 'Failed to hydrate auth state', + context: { error: e instanceof Error ? e.message : String(e) }, + }); + // Don't logout here - let the user try to use the app + // and handle auth errors via the axios interceptor } }, isAuthenticated: (): boolean => { @@ -161,6 +230,45 @@ const useAuthStore = create()( { name: 'auth-storage', storage: createJSONStorage(() => zustandStorage), + onRehydrateStorage: () => { + return (state, error) => { + if (error) { + logger.error({ + message: 'Failed to rehydrate auth storage', + context: { error: error instanceof Error ? error.message : String(error) }, + }); + return; + } + + if (state && state.refreshToken && state.status === 'signedIn') { + // We have a stored refresh token and were previously signed in + // Schedule an immediate token refresh to ensure we have a valid access token + logger.info({ + message: 'Auth state rehydrated from storage, scheduling token refresh', + context: { hasAccessToken: !!state.accessToken, hasRefreshToken: !!state.refreshToken }, + }); + + // Use a small delay to allow the app to fully initialize + setTimeout(() => { + useAuthStore.getState().refreshAccessToken(); + }, 2000); + } else if (state && state.refreshToken && state.status !== 'signedIn') { + // We have a refresh token but status is not signedIn (maybe was idle/error) + // Try to refresh and restore the session + logger.info({ + message: 'Found refresh token in storage with non-signedIn status, attempting to restore session', + context: { status: state.status }, + }); + + // Set status to loading while we try to refresh + useAuthStore.setState({ status: 'loading' }); + + setTimeout(() => { + useAuthStore.getState().refreshAccessToken(); + }, 2000); + } + }; + }, } ) ); diff --git a/src/translations/ar.json b/src/translations/ar.json index 9ee3983c..21aee925 100644 --- a/src/translations/ar.json +++ b/src/translations/ar.json @@ -598,6 +598,7 @@ "title": "المظهر" }, "title": "الإعدادات", + "activating_unit": "جاري تفعيل الوحدة...", "unit_selected_successfully": "تم اختيار {{unitName}} بنجاح", "unit_selection": "اختيار الوحدة", "unit_selection_failed": "فشل في اختيار الوحدة. يرجى المحاولة مرة أخرى.", diff --git a/src/translations/en.json b/src/translations/en.json index 1c0d6c20..d78e5706 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -598,6 +598,7 @@ "title": "Theme" }, "title": "Settings", + "activating_unit": "Activating unit...", "unit_selected_successfully": "{{unitName}} selected successfully", "unit_selection": "Unit Selection", "unit_selection_failed": "Failed to select unit. Please try again.", diff --git a/src/translations/es.json b/src/translations/es.json index 2e39239d..468a31f0 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -598,6 +598,7 @@ "title": "Tema" }, "title": "Configuración", + "activating_unit": "Activando unidad...", "unit_selected_successfully": "{{unitName}} seleccionada exitosamente", "unit_selection": "Selección de unidad", "unit_selection_failed": "Error al seleccionar la unidad. Inténtalo de nuevo.", From 6f9562ccad57b6d6abc65b52af60221dfdfb8afa Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Thu, 15 Jan 2026 19:35:40 -0800 Subject: [PATCH 2/3] RU-T45 Protocols list fix and contact sheet email phone and address fix. --- src/app/(app)/__tests__/protocols.test.tsx | 10 +- src/app/(app)/protocols.tsx | 13 +- src/app/(app)/settings.tsx | 264 +++++++++++++++++- .../contacts/contact-details-sheet.tsx | 93 ++++-- .../__tests__/protocol-card.test.tsx | 8 +- .../__tests__/protocol-details-sheet.test.tsx | 18 +- src/components/protocols/protocol-card.tsx | 2 +- .../protocols/protocol-details-sheet.tsx | 16 +- .../callProtocols/callProtocolsResultData.ts | 2 +- src/stores/protocols/__tests__/store.test.ts | 6 +- src/translations/ar.json | 2 + src/translations/en.json | 2 + src/translations/es.json | 2 + 13 files changed, 389 insertions(+), 49 deletions(-) diff --git a/src/app/(app)/__tests__/protocols.test.tsx b/src/app/(app)/__tests__/protocols.test.tsx index a74b76f3..859c5713 100644 --- a/src/app/(app)/__tests__/protocols.test.tsx +++ b/src/app/(app)/__tests__/protocols.test.tsx @@ -32,7 +32,7 @@ jest.mock('@/components/protocols/protocol-card', () => ({ ProtocolCard: ({ protocol, onPress }: { protocol: any; onPress: (id: string) => void }) => { const { Pressable, Text } = require('react-native'); return ( - onPress(protocol.Id)}> + onPress(protocol.ProtocolId)}> {protocol.Name} ); @@ -108,7 +108,7 @@ jest.mock('@/stores/protocols/store', () => ({ // Mock protocols test data const mockProtocols: CallProtocolsResultData[] = [ { - Id: '1', + ProtocolId: '1', DepartmentId: 'dept1', Name: 'Fire Emergency Response', Code: 'FIRE001', @@ -126,7 +126,7 @@ const mockProtocols: CallProtocolsResultData[] = [ Questions: [], }, { - Id: '2', + ProtocolId: '2', DepartmentId: 'dept1', Name: 'Medical Emergency', Code: 'MED001', @@ -144,7 +144,7 @@ const mockProtocols: CallProtocolsResultData[] = [ Questions: [], }, { - Id: '3', + ProtocolId: '3', DepartmentId: 'dept1', Name: 'Hazmat Response', Code: 'HAZ001', @@ -162,7 +162,7 @@ const mockProtocols: CallProtocolsResultData[] = [ Questions: [], }, { - Id: '', // Empty ID to test the keyExtractor fix + ProtocolId: '', // Empty ID to test the keyExtractor fix DepartmentId: 'dept1', Name: 'Protocol with Empty ID', Code: 'EMPTY001', diff --git a/src/app/(app)/protocols.tsx b/src/app/(app)/protocols.tsx index f59e64bb..35919dd3 100644 --- a/src/app/(app)/protocols.tsx +++ b/src/app/(app)/protocols.tsx @@ -39,6 +39,13 @@ export default function Protocols() { setRefreshing(false); }, [fetchProtocols]); + const handleProtocolPress = React.useCallback( + (id: string) => { + selectProtocol(id); + }, + [selectProtocol] + ); + const filteredProtocols = React.useMemo(() => { if (!searchQuery.trim()) return protocols; @@ -69,11 +76,13 @@ export default function Protocols() { item.Id || `protocol-${index}`} - renderItem={({ item }) => } + keyExtractor={(item, index) => item.ProtocolId || `protocol-${index}`} + renderItem={({ item }) => } showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 100 }} refreshControl={} + extraData={handleProtocolPress} + estimatedItemSize={120} /> ) : ( diff --git a/src/app/(app)/settings.tsx b/src/app/(app)/settings.tsx index c20791c7..bfc265c9 100644 --- a/src/app/(app)/settings.tsx +++ b/src/app/(app)/settings.tsx @@ -1,7 +1,7 @@ /* eslint-disable react/react-in-jsx-scope */ import { Env } from '@env'; import { useColorScheme } from 'nativewind'; -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { BackgroundGeolocationItem } from '@/components/settings/background-geolocation-item'; @@ -15,16 +15,35 @@ import { ThemeItem } from '@/components/settings/theme-item'; import { ToggleItem } from '@/components/settings/toggle-item'; import { UnitSelectionBottomSheet } from '@/components/settings/unit-selection-bottom-sheet'; import { FocusAwareStatusBar, ScrollView } from '@/components/ui'; +import { AlertDialog, AlertDialogBackdrop, AlertDialogBody, AlertDialogContent, AlertDialogFooter, AlertDialogHeader } from '@/components/ui/alert-dialog'; import { Box } from '@/components/ui/box'; +import { Button, ButtonText } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { Heading } from '@/components/ui/heading'; +import { Text } from '@/components/ui/text'; import { VStack } from '@/components/ui/vstack'; import { useAnalytics } from '@/hooks/use-analytics'; import { useAuth, useAuthStore } from '@/lib'; import { logger } from '@/lib/logging'; -import { getBaseApiUrl } from '@/lib/storage/app'; +import { storage } from '@/lib/storage'; +import { getBaseApiUrl, removeActiveCallId, removeActiveUnitId, removeDeviceUuid } from '@/lib/storage/app'; import { openLinkInBrowser } from '@/lib/utils'; +import { useAudioStreamStore } from '@/stores/app/audio-stream-store'; +import { useBluetoothAudioStore } from '@/stores/app/bluetooth-audio-store'; import { useCoreStore } from '@/stores/app/core-store'; +import { useLiveKitStore } from '@/stores/app/livekit-store'; +import { useLoadingStore } from '@/stores/app/loading-store'; +import { useLocationStore } from '@/stores/app/location-store'; +import { useCallsStore } from '@/stores/calls/store'; +import { useContactsStore } from '@/stores/contacts/store'; +import { useDispatchStore } from '@/stores/dispatch/store'; +import { useNotesStore } from '@/stores/notes/store'; +import { useOfflineQueueStore } from '@/stores/offline-queue/store'; +import { useProtocolsStore } from '@/stores/protocols/store'; +import { usePushNotificationModalStore } from '@/stores/push-notification/store'; +import { useRolesStore } from '@/stores/roles/store'; +import { securityStore } from '@/stores/security/store'; +import { useStatusBottomSheetStore } from '@/stores/status/store'; import { useUnitsStore } from '@/stores/units/store'; export default function Settings() { @@ -36,6 +55,7 @@ export default function Settings() { const { login, status, isAuthenticated } = useAuth(); const [showServerUrl, setShowServerUrl] = React.useState(false); const [showUnitSelection, setShowUnitSelection] = React.useState(false); + const [showLogoutConfirm, setShowLogoutConfirm] = React.useState(false); const activeUnit = useCoreStore((state) => state.activeUnit); const { units } = useUnitsStore(); @@ -44,6 +64,223 @@ export default function Settings() { return activeUnit?.Name || t('common.unknown'); }, [activeUnit, t]); + /** + * Clears all app data, cached values, settings, and stores + * Called when user confirms logout + */ + const clearAllAppData = useCallback(async () => { + logger.info({ + message: 'Clearing all app data on logout', + }); + + try { + // Clear persisted storage items + removeActiveUnitId(); + removeActiveCallId(); + removeDeviceUuid(); + + // Clear all MMKV storage except first time flag and user preferences + const allKeys = storage.getAllKeys(); + const keysToPreserve = ['IS_FIRST_TIME']; + allKeys.forEach((key) => { + if (!keysToPreserve.includes(key)) { + storage.delete(key); + } + }); + + // Reset all zustand stores to their initial states + // Core stores + useCoreStore.setState({ + activeUnitId: null, + activeUnit: null, + activeUnitStatus: null, + activeUnitStatusType: null, + activeCallId: null, + activeCall: null, + activePriority: null, + config: null, + isLoading: false, + isInitialized: false, + isInitializing: false, + error: null, + activeStatuses: null, + }); + + // Calls store + useCallsStore.setState({ + calls: [], + callPriorities: [], + callTypes: [], + isLoading: false, + error: null, + }); + + // Units store + useUnitsStore.setState({ + units: [], + unitStatuses: [], + isLoading: false, + error: null, + }); + + // Contacts store + useContactsStore.setState({ + contacts: [], + contactNotes: {}, + searchQuery: '', + selectedContactId: null, + isDetailsOpen: false, + isLoading: false, + isNotesLoading: false, + error: null, + }); + + // Notes store + useNotesStore.setState({ + notes: [], + searchQuery: '', + selectedNoteId: null, + isDetailsOpen: false, + isLoading: false, + error: null, + }); + + // Roles store + useRolesStore.setState({ + roles: [], + unitRoleAssignments: [], + users: [], + isLoading: false, + error: null, + }); + + // Protocols store + useProtocolsStore.setState({ + protocols: [], + searchQuery: '', + selectedProtocolId: null, + isDetailsOpen: false, + isLoading: false, + error: null, + }); + + // Dispatch store + useDispatchStore.setState({ + data: { + users: [], + groups: [], + roles: [], + units: [], + }, + selection: { + everyone: false, + users: [], + groups: [], + roles: [], + units: [], + }, + isLoading: false, + error: null, + searchQuery: '', + }); + + // Security store + securityStore.setState({ + error: null, + rights: null, + }); + + // Status bottom sheet store + useStatusBottomSheetStore.getState().reset(); + + // Offline queue store + useOfflineQueueStore.getState().clearAllEvents(); + + // Loading store + useLoadingStore.getState().resetLoading(); + + // Location store + useLocationStore.setState({ + latitude: null, + longitude: null, + heading: null, + accuracy: null, + speed: null, + altitude: null, + timestamp: null, + }); + + // LiveKit store - disconnect and reset + const liveKitState = useLiveKitStore.getState(); + if (liveKitState.isConnected) { + liveKitState.disconnectFromRoom(); + } + useLiveKitStore.setState({ + isConnected: false, + isConnecting: false, + currentRoom: null, + currentRoomInfo: null, + isTalking: false, + availableRooms: [], + isBottomSheetVisible: false, + }); + + // Audio stream store - cleanup and reset + const audioStreamState = useAudioStreamStore.getState(); + await audioStreamState.cleanup(); + useAudioStreamStore.setState({ + availableStreams: [], + currentStream: null, + isPlaying: false, + isLoading: false, + isBuffering: false, + isBottomSheetVisible: false, + }); + + // Bluetooth audio store + useBluetoothAudioStore.setState({ + connectedDevice: null, + isScanning: false, + isConnecting: false, + availableDevices: [], + connectionError: null, + isAudioRoutingActive: false, + }); + + // Push notification modal store + usePushNotificationModalStore.setState({ + isOpen: false, + notification: null, + }); + + logger.info({ + message: 'Successfully cleared all app data', + }); + } catch (error) { + logger.error({ + message: 'Error clearing app data on logout', + context: { error }, + }); + } + }, []); + + /** + * Handles logout confirmation - clears all data and signs out + */ + const handleLogoutConfirm = useCallback(async () => { + setShowLogoutConfirm(false); + + trackEvent('user_logout_confirmed', { + hadActiveUnit: !!activeUnit, + }); + + // Clear all app data first + await clearAllAppData(); + + // Then sign out + await signOut(); + }, [clearAllAppData, signOut, trackEvent, activeUnit]); + const handleLoginInfoSubmit = async (data: { username: string; password: string }) => { logger.info({ message: 'Updating login info', @@ -89,7 +326,7 @@ export default function Settings() { setShowServerUrl(true)} textStyle="text-info-600" /> setShowLoginInfo(true)} textStyle="text-info-600" /> setShowUnitSelection(true)} textStyle="text-info-600" /> - + setShowLogoutConfirm(true)} textStyle="text-error-600" /> @@ -122,6 +359,27 @@ export default function Settings() { setShowLoginInfo(false)} onSubmit={handleLoginInfoSubmit} /> setShowServerUrl(false)} /> setShowUnitSelection(false)} /> + + {/* Logout Confirmation Dialog */} + setShowLogoutConfirm(false)}> + + + + {t('settings.logout_confirm_title')} + + + {t('settings.logout_confirm_message')} + + + + + + + ); } diff --git a/src/components/contacts/contact-details-sheet.tsx b/src/components/contacts/contact-details-sheet.tsx index 2d7d45dd..e5dc7cf7 100644 --- a/src/components/contacts/contact-details-sheet.tsx +++ b/src/components/contacts/contact-details-sheet.tsx @@ -16,9 +16,9 @@ import { UserIcon, X, } from 'lucide-react-native'; -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ScrollView, useWindowDimensions, View } from 'react-native'; +import { Linking, Platform, ScrollView, useWindowDimensions, View } from 'react-native'; import { Avatar, AvatarImage } from '@/components/ui/avatar'; import { useAnalytics } from '@/hooks/use-analytics'; @@ -59,28 +59,79 @@ const Section: React.FC = ({ title, icon, children, isCollapsible ); }; +type ContactFieldAction = 'email' | 'phone' | 'address' | 'website' | 'none'; + interface ContactFieldProps { label: string; value: string | null | undefined; icon?: React.ReactNode; isLink?: boolean; linkPrefix?: string; + action?: ContactFieldAction; + fullAddress?: string; } -const ContactField: React.FC = ({ label, value, icon, isLink, linkPrefix }) => { +const ContactField: React.FC = ({ label, value, icon, isLink, linkPrefix, action = 'none', fullAddress }) => { if (!value || value.toString().trim() === '') return null; const displayValue = isLink && linkPrefix ? `${linkPrefix}${value}` : value.toString(); - return ( + const handlePress = useCallback(async () => { + if (action === 'none') return; + + let url: string | null = null; + + switch (action) { + case 'email': + url = `mailto:${value}`; + break; + case 'phone': + url = `tel:${value.toString().replace(/[^0-9+]/g, '')}`; + break; + case 'address': { + const addressToOpen = fullAddress || value.toString(); + const encodedAddress = encodeURIComponent(addressToOpen); + url = Platform.select({ + ios: `maps:0,0?q=${encodedAddress}`, + android: `geo:0,0?q=${encodedAddress}`, + default: `https://maps.google.com/?q=${encodedAddress}`, + }); + break; + } + case 'website': { + const websiteUrl = value.toString(); + url = websiteUrl.startsWith('http') ? websiteUrl : `https://${websiteUrl}`; + break; + } + } + + if (url) { + const canOpen = await Linking.canOpenURL(url); + if (canOpen) { + await Linking.openURL(url); + } + } + }, [action, value, fullAddress]); + + const isActionable = action !== 'none'; + + const content = ( {icon ? {icon} : null} {label} - {displayValue} + {displayValue} ); + + return isActionable ? ( + + {content} + + ) : ( + content + ); }; export const ContactDetailsSheet: React.FC = () => { @@ -226,13 +277,13 @@ export const ContactDetailsSheet: React.FC = () => { {hasContactInfo ? (
}> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } action="email" /> + } action="phone" /> + } action="phone" /> + } action="phone" /> + } action="phone" /> + } action="phone" /> + } action="phone" />
) : null} @@ -241,17 +292,25 @@ export const ContactDetailsSheet: React.FC = () => { {hasLocationInfo ? (
}> - } /> + } + action="address" + fullAddress={[selectedContact.Address, selectedContact.City, selectedContact.State, selectedContact.Zip].filter(Boolean).join(', ')} + /> {selectedContact.City || selectedContact.State || selectedContact.Zip ? ( } + action="address" + fullAddress={[selectedContact.Address, selectedContact.City, selectedContact.State, selectedContact.Zip].filter(Boolean).join(', ')} /> ) : null} - } /> - } /> - } /> + } action="address" /> + } action="address" /> + } action="address" />
) : null} @@ -260,7 +319,7 @@ export const ContactDetailsSheet: React.FC = () => { {hasSocialMedia ? (
} defaultExpanded={false}> - } isLink /> + } isLink action="website" /> diff --git a/src/components/protocols/__tests__/protocol-card.test.tsx b/src/components/protocols/__tests__/protocol-card.test.tsx index 3fda919d..8f98fb86 100644 --- a/src/components/protocols/__tests__/protocol-card.test.tsx +++ b/src/components/protocols/__tests__/protocol-card.test.tsx @@ -20,7 +20,7 @@ describe('ProtocolCard', () => { }); const baseProtocol: CallProtocolsResultData = { - Id: '1', + ProtocolId: '1', DepartmentId: 'dept1', Name: 'Fire Emergency Response', Code: 'FIRE001', @@ -39,7 +39,7 @@ describe('ProtocolCard', () => { }; const protocolWithoutOptionalFields: CallProtocolsResultData = { - Id: '2', + ProtocolId: '2', DepartmentId: 'dept1', Name: 'Basic Protocol', Code: '', @@ -58,7 +58,7 @@ describe('ProtocolCard', () => { }; const protocolWithHtmlDescription: CallProtocolsResultData = { - Id: '3', + ProtocolId: '3', DepartmentId: 'dept1', Name: 'Protocol with HTML', Code: 'HTML001', @@ -220,7 +220,7 @@ describe('ProtocolCard', () => { describe('Edge Cases', () => { it('should handle protocol with empty ID', () => { - const protocolWithEmptyId = { ...baseProtocol, Id: '' }; + const protocolWithEmptyId = { ...baseProtocol, ProtocolId: '' }; render(); const card = screen.getByText('Fire Emergency Response'); diff --git a/src/components/protocols/__tests__/protocol-details-sheet.test.tsx b/src/components/protocols/__tests__/protocol-details-sheet.test.tsx index 86f3c650..d9ad2121 100644 --- a/src/components/protocols/__tests__/protocol-details-sheet.test.tsx +++ b/src/components/protocols/__tests__/protocol-details-sheet.test.tsx @@ -76,7 +76,7 @@ jest.mock('@/components/ui/actionsheet', () => ({ // Mock protocols test data const mockProtocols: CallProtocolsResultData[] = [ { - Id: '1', + ProtocolId: '1', DepartmentId: 'dept1', Name: 'Fire Emergency Response', Code: 'FIRE001', @@ -94,7 +94,7 @@ const mockProtocols: CallProtocolsResultData[] = [ Questions: [], }, { - Id: '2', + ProtocolId: '2', DepartmentId: 'dept1', Name: 'Basic Protocol', Code: '', @@ -402,7 +402,7 @@ describe('ProtocolDetailsSheet', () => { render(); expect(mockTrackEvent).toHaveBeenCalledWith('protocol_details_sheet_opened', { - protocolId: '1', + protocolProtocolId: '1', protocolName: 'Fire Emergency Response', hasCode: true, hasDescription: true, @@ -413,7 +413,7 @@ describe('ProtocolDetailsSheet', () => { it('should track analytics event with false flags when protocol has no optional data', () => { const minimalProtocol: CallProtocolsResultData = { - Id: 'protocol-minimal', + ProtocolId: 'protocol-minimal', DepartmentId: 'dept1', Name: 'Minimal Protocol', Code: '', @@ -440,7 +440,7 @@ describe('ProtocolDetailsSheet', () => { render(); expect(mockTrackEvent).toHaveBeenCalledWith('protocol_details_sheet_opened', { - protocolId: 'protocol-minimal', + protocolProtocolId: 'protocol-minimal', protocolName: 'Minimal Protocol', hasCode: false, hasDescription: false, @@ -496,7 +496,7 @@ describe('ProtocolDetailsSheet', () => { expect(mockTrackEvent).toHaveBeenCalledTimes(1); expect(mockTrackEvent).toHaveBeenCalledWith('protocol_details_sheet_opened', { - protocolId: '1', + protocolProtocolId: '1', protocolName: 'Fire Emergency Response', hasCode: true, hasDescription: true, @@ -513,7 +513,7 @@ describe('ProtocolDetailsSheet', () => { it('should track analytics event when selected protocol changes', () => { const secondProtocol: CallProtocolsResultData = { ...mockProtocols[0], - Id: '3', + ProtocolId: '3', Name: 'Second Protocol', Code: 'SP002', }; @@ -529,7 +529,7 @@ describe('ProtocolDetailsSheet', () => { expect(mockTrackEvent).toHaveBeenCalledTimes(1); expect(mockTrackEvent).toHaveBeenCalledWith('protocol_details_sheet_opened', { - protocolId: '1', + protocolProtocolId: '1', protocolName: 'Fire Emergency Response', hasCode: true, hasDescription: true, @@ -548,7 +548,7 @@ describe('ProtocolDetailsSheet', () => { expect(mockTrackEvent).toHaveBeenCalledTimes(2); expect(mockTrackEvent).toHaveBeenLastCalledWith('protocol_details_sheet_opened', { - protocolId: '3', + protocolProtocolId: '3', protocolName: 'Second Protocol', hasCode: true, hasDescription: true, diff --git a/src/components/protocols/protocol-card.tsx b/src/components/protocols/protocol-card.tsx index e9eb4aaf..48298dc7 100644 --- a/src/components/protocols/protocol-card.tsx +++ b/src/components/protocols/protocol-card.tsx @@ -16,7 +16,7 @@ interface ProtocolCardProps { export const ProtocolCard: React.FC = ({ protocol, onPress }) => { return ( - onPress(protocol.Id)} testID={`protocol-card-${protocol.Id}`}> + onPress(protocol.ProtocolId)} testID={`protocol-card-${protocol.ProtocolId}`}> {protocol.Name} diff --git a/src/components/protocols/protocol-details-sheet.tsx b/src/components/protocols/protocol-details-sheet.tsx index 8ec5eb1e..793583b7 100644 --- a/src/components/protocols/protocol-details-sheet.tsx +++ b/src/components/protocols/protocol-details-sheet.tsx @@ -22,13 +22,13 @@ export const ProtocolDetailsSheet: React.FC = () => { const { trackEvent } = useAnalytics(); const { protocols, selectedProtocolId, isDetailsOpen, closeDetails } = useProtocolsStore(); - const selectedProtocol = protocols.find((protocol) => protocol.Id === selectedProtocolId); + const selectedProtocol = protocols.find((protocol) => protocol.ProtocolId === selectedProtocolId); // Track when protocol details sheet is opened/rendered useEffect(() => { if (isDetailsOpen && selectedProtocol) { trackEvent('protocol_details_sheet_opened', { - protocolId: selectedProtocol.Id, + protocolId: selectedProtocol.ProtocolId, protocolName: selectedProtocol.Name, hasCode: !!selectedProtocol.Code, hasDescription: !!selectedProtocol.Description, @@ -38,14 +38,21 @@ export const ProtocolDetailsSheet: React.FC = () => { } }, [isDetailsOpen, selectedProtocol, trackEvent]); - if (!selectedProtocol) return null; + if (!selectedProtocol) { + return ( + + + + + ); + } const textColor = colorScheme === 'dark' ? '#E5E7EB' : '#1F2937'; // gray-200 : gray-800 return ( - + @@ -79,6 +86,7 @@ export const ProtocolDetailsSheet: React.FC = () => { {/* Protocol content in WebView */} ({ // Mock protocols test data const mockProtocols: CallProtocolsResultData[] = [ { - Id: '1', + ProtocolId: '1', DepartmentId: 'dept1', Name: 'Fire Emergency Response', Code: 'FIRE001', @@ -32,7 +32,7 @@ const mockProtocols: CallProtocolsResultData[] = [ Questions: [], }, { - Id: '2', + ProtocolId: '2', DepartmentId: 'dept1', Name: 'Medical Emergency', Code: 'MED001', @@ -57,7 +57,7 @@ const mockApiResponse: CallProtocolsResult = { Timestamp: '2023-01-01T00:00:00Z', Version: '1.0', Node: 'test-node', - RequestId: 'test-request-id', + RequestProtocolId: 'test-request-id', Status: 'success', Environment: 'test', }; diff --git a/src/translations/ar.json b/src/translations/ar.json index 21aee925..fa2a659f 100644 --- a/src/translations/ar.json +++ b/src/translations/ar.json @@ -569,6 +569,8 @@ "links": "الروابط", "login_info": "معلومات تسجيل الدخول", "logout": "تسجيل خروج", + "logout_confirm_title": "تأكيد تسجيل الخروج", + "logout_confirm_message": "هل أنت متأكد من رغبتك في تسجيل الخروج؟ سيؤدي هذا إلى مسح جميع بيانات التطبيق والقيم المخزنة مؤقتاً والإعدادات.", "more": "المزيد", "no_units_available": "لا توجد وحدات متاحة", "none_selected": "لم يتم اختيار أي شيء", diff --git a/src/translations/en.json b/src/translations/en.json index d78e5706..4138672f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -569,6 +569,8 @@ "links": "Links", "login_info": "Login Info", "logout": "Logout", + "logout_confirm_title": "Confirm Logout", + "logout_confirm_message": "Are you sure you want to logout? This will clear all app data, cached values, and settings.", "more": "More", "no_units_available": "No units available", "none_selected": "None Selected", diff --git a/src/translations/es.json b/src/translations/es.json index 468a31f0..c87eb105 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -569,6 +569,8 @@ "links": "Enlaces", "login_info": "Información de inicio de sesión", "logout": "Cerrar sesión", + "logout_confirm_title": "Confirmar cierre de sesión", + "logout_confirm_message": "¿Está seguro de que desea cerrar sesión? Esto borrará todos los datos de la aplicación, valores en caché y configuraciones.", "more": "Más", "no_units_available": "No hay unidades disponibles", "none_selected": "Ninguna seleccionada", From b8ed51180d98421162e5b9db5516bee2807f06b0 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Thu, 15 Jan 2026 20:03:51 -0800 Subject: [PATCH 3/3] RU-T45 PR Fixes --- .../close-call-bottom-sheet.test.tsx | 26 +++++++++++++++++++ .../contacts/contact-details-sheet.tsx | 10 +++---- .../__tests__/protocol-details-sheet.test.tsx | 10 +++---- .../__tests__/push-notification.test.ts | 11 ++++++++ src/stores/auth/store.tsx | 7 +---- src/stores/protocols/__tests__/store.test.ts | 2 +- src/translations/ar.json | 4 +-- src/translations/en.json | 4 +-- src/translations/es.json | 4 +-- 9 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx b/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx index bf420759..e344f53a 100644 --- a/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx +++ b/src/components/calls/__tests__/close-call-bottom-sheet.test.tsx @@ -40,6 +40,32 @@ jest.mock('nativewind', () => ({ // Mock cssInterop globally (global as any).cssInterop = jest.fn(); +// Mock actionsheet components +jest.mock('@/components/ui/actionsheet', () => ({ + Actionsheet: ({ children, isOpen }: any) => { + const { View } = require('react-native'); + return isOpen ? {children} : null; + }, + ActionsheetBackdrop: () => null, + ActionsheetContent: ({ children }: any) => { + const { View } = require('react-native'); + return {children}; + }, + ActionsheetDragIndicator: () => null, + ActionsheetDragIndicatorWrapper: ({ children }: any) => { + const { View } = require('react-native'); + return {children}; + }, +})); + +// Mock keyboard aware scroll view +jest.mock('react-native-keyboard-controller', () => ({ + KeyboardAwareScrollView: ({ children }: any) => { + const { View } = require('react-native'); + return {children}; + }, +})); + // Mock UI components jest.mock('@/components/ui/bottom-sheet', () => ({ CustomBottomSheet: ({ children, isOpen }: any) => { diff --git a/src/components/contacts/contact-details-sheet.tsx b/src/components/contacts/contact-details-sheet.tsx index e5dc7cf7..e05373b4 100644 --- a/src/components/contacts/contact-details-sheet.tsx +++ b/src/components/contacts/contact-details-sheet.tsx @@ -72,12 +72,8 @@ interface ContactFieldProps { } const ContactField: React.FC = ({ label, value, icon, isLink, linkPrefix, action = 'none', fullAddress }) => { - if (!value || value.toString().trim() === '') return null; - - const displayValue = isLink && linkPrefix ? `${linkPrefix}${value}` : value.toString(); - const handlePress = useCallback(async () => { - if (action === 'none') return; + if (action === 'none' || !value) return; let url: string | null = null; @@ -113,6 +109,10 @@ const ContactField: React.FC = ({ label, value, icon, isLink, } }, [action, value, fullAddress]); + if (!value || value.toString().trim() === '') return null; + + const displayValue = isLink && linkPrefix ? `${linkPrefix}${value}` : value.toString(); + const isActionable = action !== 'none'; const content = ( diff --git a/src/components/protocols/__tests__/protocol-details-sheet.test.tsx b/src/components/protocols/__tests__/protocol-details-sheet.test.tsx index d9ad2121..b4e28836 100644 --- a/src/components/protocols/__tests__/protocol-details-sheet.test.tsx +++ b/src/components/protocols/__tests__/protocol-details-sheet.test.tsx @@ -402,7 +402,7 @@ describe('ProtocolDetailsSheet', () => { render(); expect(mockTrackEvent).toHaveBeenCalledWith('protocol_details_sheet_opened', { - protocolProtocolId: '1', + protocolId: '1', protocolName: 'Fire Emergency Response', hasCode: true, hasDescription: true, @@ -440,7 +440,7 @@ describe('ProtocolDetailsSheet', () => { render(); expect(mockTrackEvent).toHaveBeenCalledWith('protocol_details_sheet_opened', { - protocolProtocolId: 'protocol-minimal', + protocolId: 'protocol-minimal', protocolName: 'Minimal Protocol', hasCode: false, hasDescription: false, @@ -496,7 +496,7 @@ describe('ProtocolDetailsSheet', () => { expect(mockTrackEvent).toHaveBeenCalledTimes(1); expect(mockTrackEvent).toHaveBeenCalledWith('protocol_details_sheet_opened', { - protocolProtocolId: '1', + protocolId: '1', protocolName: 'Fire Emergency Response', hasCode: true, hasDescription: true, @@ -529,7 +529,7 @@ describe('ProtocolDetailsSheet', () => { expect(mockTrackEvent).toHaveBeenCalledTimes(1); expect(mockTrackEvent).toHaveBeenCalledWith('protocol_details_sheet_opened', { - protocolProtocolId: '1', + protocolId: '1', protocolName: 'Fire Emergency Response', hasCode: true, hasDescription: true, @@ -548,7 +548,7 @@ describe('ProtocolDetailsSheet', () => { expect(mockTrackEvent).toHaveBeenCalledTimes(2); expect(mockTrackEvent).toHaveBeenLastCalledWith('protocol_details_sheet_opened', { - protocolProtocolId: '3', + protocolId: '3', protocolName: 'Second Protocol', hasCode: true, hasDescription: true, diff --git a/src/services/__tests__/push-notification.test.ts b/src/services/__tests__/push-notification.test.ts index 4b7baa45..88e4df8b 100644 --- a/src/services/__tests__/push-notification.test.ts +++ b/src/services/__tests__/push-notification.test.ts @@ -98,6 +98,8 @@ const mockNotifeeRequestPermission = jest.fn(() => }) ); const mockDisplayNotification = jest.fn(() => Promise.resolve('notification-id')); +const mockOnForegroundEvent = jest.fn(() => jest.fn()); +const mockOnBackgroundEvent = jest.fn(); jest.mock('@notifee/react-native', () => ({ __esModule: true, @@ -105,6 +107,8 @@ jest.mock('@notifee/react-native', () => ({ createChannel: mockCreateChannel, requestPermission: mockNotifeeRequestPermission, displayNotification: mockDisplayNotification, + onForegroundEvent: mockOnForegroundEvent, + onBackgroundEvent: mockOnBackgroundEvent, }, AndroidImportance: { HIGH: 4, @@ -431,13 +435,20 @@ describe('Push Notification Service Integration', () => { }); it('should store listener handles on initialization', async () => { + jest.useFakeTimers(); + await pushNotificationService.initialize(); + // Run all timers to trigger getInitialNotification which is called in setTimeout + jest.runAllTimers(); + // Verify listeners were registered expect(mockOnMessage).toHaveBeenCalled(); expect(mockOnNotificationOpenedApp).toHaveBeenCalled(); expect(mockGetInitialNotification).toHaveBeenCalled(); expect(mockSetBackgroundMessageHandler).toHaveBeenCalled(); + + jest.useRealTimers(); }); it('should properly cleanup all listeners', async () => { diff --git a/src/stores/auth/store.tsx b/src/stores/auth/store.tsx index e5850aa0..ea50afb4 100644 --- a/src/stores/auth/store.tsx +++ b/src/stores/auth/store.tsx @@ -123,12 +123,7 @@ const useAuthStore = create()( setTimeout(() => get().refreshAccessToken(), refreshDelayMs); } catch (error) { // Check if it's a network error vs an invalid refresh token - const isNetworkError = - error instanceof Error && - (error.message.includes('Network Error') || - error.message.includes('timeout') || - error.message.includes('ECONNREFUSED') || - error.message.includes('ETIMEDOUT')); + const isNetworkError = error instanceof Error && (error.message.includes('Network Error') || error.message.includes('timeout') || error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')); if (isNetworkError) { // Network error - retry after a delay, don't logout diff --git a/src/stores/protocols/__tests__/store.test.ts b/src/stores/protocols/__tests__/store.test.ts index fedd0ec8..1edf8291 100644 --- a/src/stores/protocols/__tests__/store.test.ts +++ b/src/stores/protocols/__tests__/store.test.ts @@ -57,7 +57,7 @@ const mockApiResponse: CallProtocolsResult = { Timestamp: '2023-01-01T00:00:00Z', Version: '1.0', Node: 'test-node', - RequestProtocolId: 'test-request-id', + RequestId: 'test-request-id', Status: 'success', Environment: 'test', }; diff --git a/src/translations/ar.json b/src/translations/ar.json index fa2a659f..b09ea599 100644 --- a/src/translations/ar.json +++ b/src/translations/ar.json @@ -531,6 +531,7 @@ "settings": { "about": "حول التطبيق", "account": "الحساب", + "activating_unit": "جاري تفعيل الوحدة...", "active_unit": "الوحدة النشطة", "app_info": "معلومات التطبيق", "app_name": "اسم التطبيق", @@ -569,8 +570,8 @@ "links": "الروابط", "login_info": "معلومات تسجيل الدخول", "logout": "تسجيل خروج", - "logout_confirm_title": "تأكيد تسجيل الخروج", "logout_confirm_message": "هل أنت متأكد من رغبتك في تسجيل الخروج؟ سيؤدي هذا إلى مسح جميع بيانات التطبيق والقيم المخزنة مؤقتاً والإعدادات.", + "logout_confirm_title": "تأكيد تسجيل الخروج", "more": "المزيد", "no_units_available": "لا توجد وحدات متاحة", "none_selected": "لم يتم اختيار أي شيء", @@ -600,7 +601,6 @@ "title": "المظهر" }, "title": "الإعدادات", - "activating_unit": "جاري تفعيل الوحدة...", "unit_selected_successfully": "تم اختيار {{unitName}} بنجاح", "unit_selection": "اختيار الوحدة", "unit_selection_failed": "فشل في اختيار الوحدة. يرجى المحاولة مرة أخرى.", diff --git a/src/translations/en.json b/src/translations/en.json index 4138672f..27f32edd 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -531,6 +531,7 @@ "settings": { "about": "About", "account": "Account", + "activating_unit": "Activating unit...", "active_unit": "Active Unit", "app_info": "App Info", "app_name": "App Name", @@ -569,8 +570,8 @@ "links": "Links", "login_info": "Login Info", "logout": "Logout", - "logout_confirm_title": "Confirm Logout", "logout_confirm_message": "Are you sure you want to logout? This will clear all app data, cached values, and settings.", + "logout_confirm_title": "Confirm Logout", "more": "More", "no_units_available": "No units available", "none_selected": "None Selected", @@ -600,7 +601,6 @@ "title": "Theme" }, "title": "Settings", - "activating_unit": "Activating unit...", "unit_selected_successfully": "{{unitName}} selected successfully", "unit_selection": "Unit Selection", "unit_selection_failed": "Failed to select unit. Please try again.", diff --git a/src/translations/es.json b/src/translations/es.json index c87eb105..8fc36d9b 100644 --- a/src/translations/es.json +++ b/src/translations/es.json @@ -531,6 +531,7 @@ "settings": { "about": "Acerca de", "account": "Cuenta", + "activating_unit": "Activando unidad...", "active_unit": "Unidad activa", "app_info": "Información de la aplicación", "app_name": "Nombre de la aplicación", @@ -569,8 +570,8 @@ "links": "Enlaces", "login_info": "Información de inicio de sesión", "logout": "Cerrar sesión", - "logout_confirm_title": "Confirmar cierre de sesión", "logout_confirm_message": "¿Está seguro de que desea cerrar sesión? Esto borrará todos los datos de la aplicación, valores en caché y configuraciones.", + "logout_confirm_title": "Confirmar cierre de sesión", "more": "Más", "no_units_available": "No hay unidades disponibles", "none_selected": "Ninguna seleccionada", @@ -600,7 +601,6 @@ "title": "Tema" }, "title": "Configuración", - "activating_unit": "Activando unidad...", "unit_selected_successfully": "{{unitName}} seleccionada exitosamente", "unit_selection": "Selección de unidad", "unit_selection_failed": "Error al seleccionar la unidad. Inténtalo de nuevo.",