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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ module.exports = {
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleDirectories: ['node_modules', '<rootDir>/'],
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|@legendapp/motion|@gluestack-ui|expo-audio/.*|@aptabase/.*))',
'node_modules/(?!((jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg|@legendapp/motion|@gluestack-ui|expo-audio/.*|@aptabase/.*|@dev-plugins/.*))',
],
coverageReporters: ['json-summary', ['text', { file: 'coverage.txt' }], 'cobertura'],
reporters: [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"@gluestack-ui/link": "~0.1.22",
"@gluestack-ui/menu": "~0.2.43",
"@gluestack-ui/modal": "^0.1.35",
"@gluestack-ui/nativewind-utils": "~1.0.26",
"@gluestack-ui/nativewind-utils": "^1.0.28",
"@gluestack-ui/overlay": "~0.1.16",
"@gluestack-ui/popover": "~0.1.49",
"@gluestack-ui/pressable": "~0.1.16",
Expand Down
11 changes: 5 additions & 6 deletions src/api/calls/callFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,20 +126,19 @@ export const getCallAudio = async (callId: string, includeData: boolean) => {
};

export const saveCallFile = async (callId: string, userId: string, note: string, name: string, latitude: number | null, longitude: number | null, file: string, type: number) => {
let data = {
let data: any = {
CallId: callId,
UserId: userId,
Type: type,
Name: name,
Latitude: '',
Longitude: '',
Note: note,
Data: file,
};

if (latitude && longitude) {
data.Latitude = latitude?.toString();
data.Longitude = longitude?.toString();
// Only include Latitude and Longitude if both are provided
if (latitude !== null && longitude !== null) {
data.Latitude = latitude.toString();
data.Longitude = longitude.toString();
}

const response = await saveCallFileApi.post<SaveCallFileResult>({
Expand Down
6 changes: 5 additions & 1 deletion src/app/(app)/__tests__/protocols.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ jest.mock('@/components/protocols/protocol-card', () => ({
const React = require('react');
return React.createElement(
'Pressable',
{ testID: `protocol-card-${protocol.Id}`, onPress: () => onPress(protocol.Id) },
{ testID: `protocol-card-${protocol.ProtocolId}`, onPress: () => onPress(protocol.ProtocolId) },
React.createElement('Text', null, protocol.Name)
);
},
Expand Down Expand Up @@ -215,6 +215,7 @@ import Protocols from '../protocols';
const mockProtocols: CallProtocolsResultData[] = [
{
Id: '1',
ProtocolId: '1',
DepartmentId: 'dept1',
Name: 'Fire Emergency Response',
Code: 'FIRE001',
Expand All @@ -233,6 +234,7 @@ const mockProtocols: CallProtocolsResultData[] = [
},
{
Id: '2',
ProtocolId: '2',
DepartmentId: 'dept1',
Name: 'Medical Emergency',
Code: 'MED001',
Expand All @@ -251,6 +253,7 @@ const mockProtocols: CallProtocolsResultData[] = [
},
{
Id: '3',
ProtocolId: '3',
DepartmentId: 'dept1',
Name: 'Hazmat Response',
Code: 'HAZ001',
Expand All @@ -269,6 +272,7 @@ const mockProtocols: CallProtocolsResultData[] = [
},
{
Id: '', // Empty ID to test the keyExtractor fix
ProtocolId: '', // Empty ProtocolId to test the keyExtractor fix
DepartmentId: 'dept1',
Name: 'Protocol with Empty ID',
Code: 'EMPTY001',
Expand Down
113 changes: 111 additions & 2 deletions src/app/(app)/__tests__/settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,63 @@ jest.mock('@/lib/logging', () => ({
},
}));

// Mock clear all app data
const mockClearAllAppData = jest.fn().mockResolvedValue(undefined);
jest.mock('@/lib/storage/clear-all-data', () => ({
clearAllAppData: () => mockClearAllAppData(),
}));

// Mock AlertDialog components
jest.mock('@/components/ui/alert-dialog', () => ({
AlertDialog: ({ isOpen, onClose, children }: any) => {
const { View } = require('react-native');
return isOpen ? (
<View testID="logout-confirmation-dialog">{children}</View>
) : null;
},
AlertDialogBackdrop: () => null,
AlertDialogContent: ({ children }: any) => {
const { View } = require('react-native');
return <View testID="alert-dialog-content">{children}</View>;
},
AlertDialogHeader: ({ children }: any) => {
const { View } = require('react-native');
return <View testID="alert-dialog-header">{children}</View>;
},
AlertDialogBody: ({ children }: any) => {
const { View } = require('react-native');
return <View testID="alert-dialog-body">{children}</View>;
},
AlertDialogFooter: ({ children }: any) => {
const { View } = require('react-native');
return <View testID="alert-dialog-footer">{children}</View>;
},
}));

// Mock Button components
jest.mock('@/components/ui/button', () => ({
Button: ({ children, onPress, disabled, testID }: any) => {
const { TouchableOpacity } = require('react-native');
return (
<TouchableOpacity testID={testID} onPress={onPress} disabled={disabled}>
{children}
</TouchableOpacity>
);
},
ButtonText: ({ children }: any) => {
const { Text } = require('react-native');
return <Text>{children}</Text>;
},
}));
Comment on lines +275 to +288
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate and examine the test file
fd "settings.test.tsx" --type f

Repository: Resgrid/Responder

Length of output: 102


🏁 Script executed:

# Read the test file to understand context
wc -l src/app/\(app\)/__tests__/settings.test.tsx

Repository: Resgrid/Responder

Length of output: 106


🏁 Script executed:

# Read the Button mock and surrounding test code
sed -n '270,320p' src/app/\(app\)/__tests__/settings.test.tsx

Repository: Resgrid/Responder

Length of output: 1508


🏁 Script executed:

# Search for fireEvent.press calls to understand how tests interact with Button
rg "fireEvent\.press|getByText.*ButtonText" src/app/\(app\)/__tests__/settings.test.tsx -A 2 -B 2

Repository: Resgrid/Responder

Length of output: 1881


🏁 Script executed:

# Find the actual Button component to understand expected usage
fd "button" --type f -e tsx -e ts | grep -E "components/ui"

Repository: Resgrid/Responder

Length of output: 97


🏁 Script executed:

# Read the actual Button component implementation
cat -n src/components/ui/button.tsx

Repository: Resgrid/Responder

Length of output: 121


🏁 Script executed:

# Search for how Button and ButtonText are used together in production code
rg "ButtonText" src --type tsx -B 2 -A 1 | head -50

Repository: Resgrid/Responder

Length of output: 88


🏁 Script executed:

# Search for where Button is defined or imported
rg "export.*Button|from.*button" src --type ts --type tsx -l

Repository: Resgrid/Responder

Length of output: 88


🏁 Script executed:

# Check if Button comes from gluestack-ui directly
rg "from.*gluestack-ui|import.*Button" src --type ts --type tsx -A 1 | head -30

Repository: Resgrid/Responder

Length of output: 88


🏁 Script executed:

# Look for the components/ui directory structure
fd --type f "button|Button" src/components --type f

Repository: Resgrid/Responder

Length of output: 192


🏁 Script executed:

# Check what's in components/ui
fd --type f . src/components/ui 2>/dev/null | head -20

Repository: Resgrid/Responder

Length of output: 850


🏁 Script executed:

cat -n src/components/ui/button/index.tsx

Repository: Resgrid/Responder

Length of output: 13940


🏁 Script executed:

# Check if there are other button-related files
ls -la src/components/ui/button/

Repository: Resgrid/Responder

Length of output: 237


🏁 Script executed:

# Check if there are other Button mock usages in test files to see patterns
find . -name "*.test.tsx" -o -name "*.spec.tsx" | head -20

Repository: Resgrid/Responder

Length of output: 998


🏁 Script executed:

# Look for __mocks__ button file
cat -n __mocks__/@/components/ui/button.tsx 2>/dev/null || echo "Mock not found at expected location"

Repository: Resgrid/Responder

Length of output: 403


🏁 Script executed:

# Search for ButtonText usage in actual component code (non-test)
rg "ButtonText" src --type ts -g "!*.test.tsx" -g "!*.spec.tsx" -B 1 -A 1 | head -40

Repository: Resgrid/Responder

Length of output: 3355


🏁 Script executed:

# Run the test to see if it actually passes or fails
cd src/app/\(app\)/__tests__ && npm test -- settings.test.tsx 2>&1 | tail -50 || echo "Test execution note: tests may not run in sandbox"

Repository: Resgrid/Responder

Length of output: 179


Button mock's ButtonText doesn't accept or forward props.
The mock ButtonText only accepts {children} while production ButtonText spreads {...props}. Tests press ButtonText via getByText, and without forwarding onPress, handlers may not trigger reliably. Spread props in ButtonText to match production behavior and ensure test reliability.

🤖 Prompt for AI Agents
In `@src/app/`(app)/__tests__/settings.test.tsx around lines 275 - 288, The mocked
ButtonText component in the test only accepts {children} so it doesn't forward
props (like onPress) used in production; update the mock for ButtonText to
accept (props) and spread {...props} onto the rendered Text (preserving
children) so event handlers and test queries (e.g., getByText) behave like the
real ButtonText; ensure the mock still returns a Text element and keep the
Button mock using TouchableOpacity as-is.


// Mock Text component
jest.mock('@/components/ui/text', () => ({
Text: ({ children }: any) => {
const { Text: RNText } = require('react-native');
return <RNText>{children}</RNText>;
},
}));

describe('Settings Screen', () => {
const mockUseColorScheme = useColorScheme as jest.MockedFunction<typeof useColorScheme>;
const mockUseFocusEffect = useFocusEffect as jest.MockedFunction<typeof useFocusEffect>;
Expand Down Expand Up @@ -347,7 +404,7 @@ describe('Settings Screen', () => {
expect(loginSheet.props.style.display).toBe('flex');
});

it('handles logout press and tracks analytics', () => {
it('handles logout press and shows confirmation dialog', () => {
render(<Settings />);

const logoutItem = screen.getByTestId('item-settings.logout');
Expand All @@ -357,7 +414,59 @@ describe('Settings Screen', () => {
timestamp: expect.any(String),
});

expect(mockLogout).toHaveBeenCalledTimes(1);
// Confirmation dialog should now be shown
expect(screen.getByTestId('logout-confirmation-dialog')).toBeTruthy();
expect(screen.getByText('settings.logout_confirm_title')).toBeTruthy();
expect(screen.getByText('settings.logout_confirm_message')).toBeTruthy();
});

it('handles logout confirmation and clears all app data', async () => {
render(<Settings />);

// Open logout confirmation dialog
const logoutItem = screen.getByTestId('item-settings.logout');
fireEvent.press(logoutItem);

// Find and press the confirm button (Yes, Logout)
const confirmButton = screen.getByText('settings.logout_confirm_yes');
fireEvent.press(confirmButton);

await waitFor(() => {
expect(mockTrackEvent).toHaveBeenCalledWith('settings_logout_confirmed', {
timestamp: expect.any(String),
});
});

await waitFor(() => {
expect(mockClearAllAppData).toHaveBeenCalled();
});

await waitFor(() => {
expect(mockLogout).toHaveBeenCalledTimes(1);
});
});

it('handles logout cancellation and does not clear data', () => {
render(<Settings />);

// Open logout confirmation dialog
const logoutItem = screen.getByTestId('item-settings.logout');
fireEvent.press(logoutItem);

// Dialog should be visible
expect(screen.getByTestId('logout-confirmation-dialog')).toBeTruthy();

// Find and press the cancel button
const cancelButton = screen.getByText('settings.logout_confirm_cancel');
fireEvent.press(cancelButton);

expect(mockTrackEvent).toHaveBeenCalledWith('settings_logout_cancelled', {
timestamp: expect.any(String),
});

// Logout and clear should not be called
expect(mockClearAllAppData).not.toHaveBeenCalled();
expect(mockLogout).not.toHaveBeenCalled();
});

it('handles support link presses and tracks analytics', () => {
Expand Down
7 changes: 5 additions & 2 deletions src/app/(app)/home/units.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useFocusEffect } from '@react-navigation/native';
import { FlashList } from '@shopify/flash-list';
import { Filter, Search, Truck, X } from 'lucide-react-native';
import * as React from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { View } from 'react-native';

Expand All @@ -24,7 +25,7 @@ import { useUnitsStore } from '@/stores/units/store';

export default function Units() {
const { t } = useTranslation();
const { units, searchQuery, setSearchQuery, selectUnit, isLoading, fetchUnits, selectedFilters, openFilterSheet } = useUnitsStore();
const { units, unitTypeStatuses, searchQuery, setSearchQuery, selectUnit, isLoading, fetchUnits, selectedFilters, openFilterSheet } = useUnitsStore();
const { trackEvent } = useAnalytics();
const [refreshing, setRefreshing] = React.useState(false);

Expand All @@ -47,6 +48,8 @@ export default function Units() {
setRefreshing(false);
}, [fetchUnits]);

const renderUnitItem = useCallback(({ item }: { item: any }) => <UnitCard unit={item} unitTypeStatuses={unitTypeStatuses} onPress={selectUnit} />, [unitTypeStatuses, selectUnit]);

const filteredUnits = React.useMemo(() => {
if (!searchQuery.trim()) return units;

Expand Down Expand Up @@ -96,7 +99,7 @@ export default function Units() {
<FlashList
data={filteredUnits}
keyExtractor={(item, index) => item.UnitId || `unit-${index}`}
renderItem={({ item }) => <UnitCard unit={item as any} onPress={selectUnit} />}
renderItem={renderUnitItem}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 100 }}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />}
Expand Down
2 changes: 1 addition & 1 deletion src/app/(app)/protocols.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function Protocols() {
<FlashList
testID="protocols-list"
data={filteredProtocols}
keyExtractor={(item, index) => item.Id || `protocol-${index}`}
keyExtractor={(item, index) => item.ProtocolId || `protocol-${index}`}
renderItem={({ item }) => <ProtocolCard protocol={item} onPress={selectProtocol} />}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 100 }}
Expand Down
76 changes: 74 additions & 2 deletions src/app/(app)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { Env } from '@env';
import { useFocusEffect } from '@react-navigation/native';
import { useColorScheme } from 'nativewind';
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';

import { BackgroundGeolocationItem } from '@/components/settings/background-geolocation-item';
Expand All @@ -16,14 +16,18 @@ import { ServerUrlBottomSheet } from '@/components/settings/server-url-bottom-sh
import { ThemeItem } from '@/components/settings/theme-item';
import { ToggleItem } from '@/components/settings/toggle-item';
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 { clearAllAppData } from '@/lib/storage/clear-all-data';
import { openLinkInBrowser } from '@/lib/utils';
import { useUnitsStore } from '@/stores/units/store';

Expand All @@ -36,6 +40,8 @@ export default function Settings() {
const { login, status, isAuthenticated } = useAuth();
const [showServerUrl, setShowServerUrl] = React.useState(false);
const [showUnitSelection, setShowUnitSelection] = React.useState(false);
const [showLogoutConfirm, setShowLogoutConfirm] = useState(false);
const [isLoggingOut, setIsLoggingOut] = useState(false);
const { units } = useUnitsStore();

// Track analytics when view becomes visible
Expand Down Expand Up @@ -83,9 +89,54 @@ export default function Settings() {
trackEvent('settings_logout_pressed', {
timestamp: new Date().toISOString(),
});
signOut();
setShowLogoutConfirm(true);
}, [trackEvent]);

const handleLogoutConfirm = useCallback(async () => {
setIsLoggingOut(true);
trackEvent('settings_logout_confirmed', {
timestamp: new Date().toISOString(),
});

logger.info({
message: 'User confirmed logout, clearing all app data',
});

try {
// Clear all app data (stores, storage, cached values)
await clearAllAppData({
resetStores: true,
clearStorage: true,
clearFilters: true,
clearSecure: false, // Keep secure keys for re-login
});

// Sign out the user
await signOut();

logger.info({
message: 'Logout completed successfully, all data cleared',
});
} catch (error) {
logger.error({
message: 'Error during logout data cleanup',
context: { error },
});
// Still sign out even if cleanup fails
await signOut();
} finally {
setIsLoggingOut(false);
setShowLogoutConfirm(false);
}
}, [trackEvent, signOut]);

const handleLogoutCancel = useCallback(() => {
trackEvent('settings_logout_cancelled', {
timestamp: new Date().toISOString(),
});
setShowLogoutConfirm(false);
}, [trackEvent]);

const handleSupportLinkPress = useCallback(
(linkType: string, url: string) => {
trackEvent('settings_support_link_pressed', {
Expand Down Expand Up @@ -160,6 +211,27 @@ export default function Settings() {

<LoginInfoBottomSheet isOpen={showLoginInfo} onClose={() => setShowLoginInfo(false)} onSubmit={handleLoginInfoSubmit} />
<ServerUrlBottomSheet isOpen={showServerUrl} onClose={() => setShowServerUrl(false)} />

{/* Logout Confirmation Dialog */}
<AlertDialog isOpen={showLogoutConfirm} onClose={handleLogoutCancel}>
<AlertDialogBackdrop />
<AlertDialogContent>
<AlertDialogHeader>
<Heading size="lg">{t('settings.logout_confirm_title')}</Heading>
</AlertDialogHeader>
<AlertDialogBody className="mb-4 mt-3">
<Text>{t('settings.logout_confirm_message')}</Text>
</AlertDialogBody>
<AlertDialogFooter>
<Button variant="outline" action="secondary" onPress={handleLogoutCancel} disabled={isLoggingOut} className="mr-3">
<ButtonText>{t('settings.logout_confirm_cancel')}</ButtonText>
</Button>
<Button action="negative" onPress={handleLogoutConfirm} disabled={isLoggingOut}>
<ButtonText>{isLoggingOut ? '...' : t('settings.logout_confirm_yes')}</ButtonText>
</Button>
Comment on lines +226 to +231
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read the relevant section
head -250 "src/app/(app)/settings.tsx" | tail -30

Repository: Resgrid/Responder

Length of output: 1368


🏁 Script executed:

# Check for any translation files and existing logout-related keys
fd -t f -e json -e ts -e tsx | grep -i "translation\|locale\|i18n" | head -20

Repository: Resgrid/Responder

Length of output: 261


🏁 Script executed:

# Search for existing logout confirmation translation keys
rg "logout_confirm" --type ts --type tsx

Repository: Resgrid/Responder

Length of output: 88


🏁 Script executed:

# Check imports and i18n setup in the settings file
head -50 "src/app/(app)/settings.tsx"

Repository: Resgrid/Responder

Length of output: 2750


🏁 Script executed:

# Check the English translation file for logout confirmation keys
cat "src/translations/en.json" | grep -A 20 "logout_confirm"

Repository: Resgrid/Responder

Length of output: 1409


🏁 Script executed:

# Get the full logout_confirm section from translation file
rg "logout_confirm" "src/translations/en.json" -A 3 -B 1

Repository: Resgrid/Responder

Length of output: 463


🏁 Script executed:

# Check the full structure of settings keys in en.json
python3 << 'EOF'
import json
with open('src/translations/en.json', 'r') as f:
    data = json.load(f)
    if 'settings' in data:
        settings = data['settings']
        logout_keys = {k: v for k, v in settings.items() if 'logout' in k}
        print("Logout-related keys found:")
        for key, value in sorted(logout_keys.items()):
            print(f"  {key}: {value}")
    else:
        print("No 'settings' key found in translation file")
EOF

Repository: Resgrid/Responder

Length of output: 147


🏁 Script executed:

# Search for any existing loading or status indicator keys in settings
rg "loading\|loading_state\|please_wait" "src/translations/en.json" | head -20

Repository: Resgrid/Responder

Length of output: 43


🏁 Script executed:

# Double-check the exact code in the file around line 226-231
sed -n '220,235p' "src/app/(app)/settings.tsx"

Repository: Resgrid/Responder

Length of output: 874


Localize the logout loading indicator.
The literal '...' is user-facing and bypasses i18n. Per coding guidelines, wrap it in t() and add a corresponding translation key.

Suggested change
-              <ButtonText>{isLoggingOut ? '...' : t('settings.logout_confirm_yes')}</ButtonText>
+              <ButtonText>{isLoggingOut ? t('settings.logout_confirm_loading') : t('settings.logout_confirm_yes')}</ButtonText>

Then add the key "logout_confirm_loading" to all translation files (en.json, es.json, ar.json).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button variant="outline" action="secondary" onPress={handleLogoutCancel} disabled={isLoggingOut} className="mr-3">
<ButtonText>{t('settings.logout_confirm_cancel')}</ButtonText>
</Button>
<Button action="negative" onPress={handleLogoutConfirm} disabled={isLoggingOut}>
<ButtonText>{isLoggingOut ? '...' : t('settings.logout_confirm_yes')}</ButtonText>
</Button>
<Button variant="outline" action="secondary" onPress={handleLogoutCancel} disabled={isLoggingOut} className="mr-3">
<ButtonText>{t('settings.logout_confirm_cancel')}</ButtonText>
</Button>
<Button action="negative" onPress={handleLogoutConfirm} disabled={isLoggingOut}>
<ButtonText>{isLoggingOut ? t('settings.logout_confirm_loading') : t('settings.logout_confirm_yes')}</ButtonText>
</Button>
🤖 Prompt for AI Agents
In `@src/app/`(app)/settings.tsx around lines 226 - 231, The logout confirmation
uses a hardcoded loading label '...' which bypasses i18n; update the ButtonText
inside the Button that renders isLoggingOut to call
t('settings.logout_confirm_loading') instead of the literal, add the new
"logout_confirm_loading" key to all translation JSONs (e.g., en.json, es.json,
ar.json) and ensure translations are provided, keeping existing keys like
'settings.logout_confirm_yes' and functions such as handleLogoutConfirm and
ButtonText unchanged.

</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Box>
);
}
Loading