Skip to content

Commit 05a4e7f

Browse files
authored
Merge pull request #193 from Resgrid/develop
Develop
2 parents 145c646 + 923226b commit 05a4e7f

32 files changed

+1854
-412
lines changed

.github/workflows/react-native-cicd.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,13 @@ jobs:
319319
# Third pass: Remove any remaining HTML comment lines
320320
cleaned_body="$(printf '%s\n' "$cleaned_body" | sed '/^<!--.*-->$/d' | sed '/^<!--/d' | sed '/^-->$/d')"
321321
322-
# Fourth pass: Trim leading and trailing whitespace/empty lines
322+
# Fourth pass: Remove specific CodeRabbit lines
323+
cleaned_body="$(printf '%s\n' "$cleaned_body" \
324+
| (grep -v '✏️ Tip: You can customize this high-level summary in your review settings\.' || true) \
325+
| (grep -v '<!-- This is an auto-generated comment: release notes by coderabbit.ai -->' || true) \
326+
| (grep -v '<!-- end of auto-generated comment: release notes by coderabbit.ai -->' || true))"
327+
328+
# Fifth pass: Trim leading and trailing whitespace/empty lines
323329
cleaned_body="$(printf '%s\n' "$cleaned_body" | sed '/^$/d' | awk 'NF {p=1} p')"
324330
325331
# Try to extract content under "## Release Notes" heading if it exists
@@ -429,13 +435,10 @@ jobs:
429435
PAYLOAD=$(jq -n \
430436
--arg version "$VERSION" \
431437
--arg notes "$RELEASE_NOTES" \
432-
--arg buildNumber "${{ github.run_number }}" \
433-
--arg commitSha "${{ github.sha }}" \
434-
--arg buildUrl "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
435438
'{
436439
"version": $version,
437440
"title": ("Release v" + $version),
438-
"content": "$notes",
441+
"content": $notes
439442
}')
440443
441444
echo "Sending release notes to Changerawr..."

.vscode/settings.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,24 @@
4646
"editor.codeActionsOnSave": {
4747

4848
},
49+
"workbench.colorCustomizations": {
50+
"activityBar.activeBackground": "#5372eb",
51+
"activityBar.background": "#5372eb",
52+
"activityBar.foreground": "#e7e7e7",
53+
"activityBar.inactiveForeground": "#e7e7e799",
54+
"activityBarBadge.background": "#8f112a",
55+
"activityBarBadge.foreground": "#e7e7e7",
56+
"commandCenter.border": "#e7e7e799",
57+
"sash.hoverBorder": "#5372eb",
58+
"statusBar.background": "#254de6",
59+
"statusBar.foreground": "#e7e7e7",
60+
"statusBarItem.hoverBackground": "#5372eb",
61+
"statusBarItem.remoteBackground": "#254de6",
62+
"statusBarItem.remoteForeground": "#e7e7e7",
63+
"titleBar.activeBackground": "#254de6",
64+
"titleBar.activeForeground": "#e7e7e7",
65+
"titleBar.inactiveBackground": "#254de699",
66+
"titleBar.inactiveForeground": "#e7e7e799"
67+
},
68+
"peacock.color": "#254de6",
4969
}

src/api/common/client.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios, { type AxiosError, type AxiosInstance, type InternalAxiosRequestConfig } from 'axios';
1+
import axios, { type AxiosError, type AxiosInstance, type InternalAxiosRequestConfig, isAxiosError } from 'axios';
22

33
import { refreshTokenRequest } from '@/lib/auth/api';
44
import { logger } from '@/lib/logging';
@@ -98,12 +98,24 @@ axiosInstance.interceptors.response.use(
9898
return axiosInstance(originalRequest);
9999
} catch (refreshError) {
100100
processQueue(refreshError as Error);
101-
// Handle refresh token failure
102-
useAuthStore.getState().logout();
103-
logger.error({
104-
message: 'Token refresh failed',
105-
context: { error: refreshError },
106-
});
101+
102+
// Check if it's a network error vs an invalid refresh token
103+
const isNetworkError = isAxiosError(refreshError) && !refreshError.response;
104+
105+
if (!isNetworkError) {
106+
// Only logout for non-network errors (e.g., invalid refresh token, 400/401 from token endpoint)
107+
logger.error({
108+
message: 'Token refresh failed with non-recoverable error, logging out user',
109+
context: { error: refreshError },
110+
});
111+
useAuthStore.getState().logout();
112+
} else {
113+
logger.warn({
114+
message: 'Token refresh failed due to network error',
115+
context: { error: refreshError },
116+
});
117+
}
118+
107119
return Promise.reject(refreshError);
108120
} finally {
109121
isRefreshing = false;

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: 52 additions & 2 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,15 +15,19 @@ 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';
2528
import { getBaseApiUrl } from '@/lib/storage/app';
2629
import { openLinkInBrowser } from '@/lib/utils';
30+
import { clearAllAppData } from '@/services/app-reset.service';
2731
import { useCoreStore } from '@/stores/app/core-store';
2832
import { useUnitsStore } from '@/stores/units/store';
2933

@@ -36,6 +40,7 @@ export default function Settings() {
3640
const { login, status, isAuthenticated } = useAuth();
3741
const [showServerUrl, setShowServerUrl] = React.useState(false);
3842
const [showUnitSelection, setShowUnitSelection] = React.useState(false);
43+
const [showLogoutConfirm, setShowLogoutConfirm] = React.useState(false);
3944
const activeUnit = useCoreStore((state) => state.activeUnit);
4045
const { units } = useUnitsStore();
4146

@@ -44,6 +49,30 @@ export default function Settings() {
4449
return activeUnit?.Name || t('common.unknown');
4550
}, [activeUnit, t]);
4651

52+
/**
53+
* Handles logout confirmation - clears all data and signs out
54+
*/
55+
const handleLogoutConfirm = useCallback(async () => {
56+
setShowLogoutConfirm(false);
57+
58+
trackEvent('user_logout_confirmed', {
59+
hadActiveUnit: !!activeUnit,
60+
});
61+
62+
// Clear all app data first using the centralized service
63+
try {
64+
await clearAllAppData();
65+
} catch (error) {
66+
logger.error({
67+
message: 'Error during app data cleanup on logout',
68+
context: { error },
69+
});
70+
}
71+
72+
// Then sign out
73+
await signOut();
74+
}, [signOut, trackEvent, activeUnit]);
75+
4776
const handleLoginInfoSubmit = async (data: { username: string; password: string }) => {
4877
logger.info({
4978
message: 'Updating login info',
@@ -89,7 +118,7 @@ export default function Settings() {
89118
<Item text={t('settings.server')} value={getBaseApiUrl()} onPress={() => setShowServerUrl(true)} textStyle="text-info-600" />
90119
<Item text={t('settings.login_info')} onPress={() => setShowLoginInfo(true)} textStyle="text-info-600" />
91120
<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" />
121+
<Item text={t('settings.logout')} onPress={() => setShowLogoutConfirm(true)} textStyle="text-error-600" />
93122
</VStack>
94123
</Card>
95124

@@ -122,6 +151,27 @@ export default function Settings() {
122151
<LoginInfoBottomSheet isOpen={showLoginInfo} onClose={() => setShowLoginInfo(false)} onSubmit={handleLoginInfoSubmit} />
123152
<ServerUrlBottomSheet isOpen={showServerUrl} onClose={() => setShowServerUrl(false)} />
124153
<UnitSelectionBottomSheet isOpen={showUnitSelection} onClose={() => setShowUnitSelection(false)} />
154+
155+
{/* Logout Confirmation Dialog */}
156+
<AlertDialog isOpen={showLogoutConfirm} onClose={() => setShowLogoutConfirm(false)}>
157+
<AlertDialogBackdrop />
158+
<AlertDialogContent>
159+
<AlertDialogHeader>
160+
<Heading size="lg">{t('settings.logout_confirm_title')}</Heading>
161+
</AlertDialogHeader>
162+
<AlertDialogBody>
163+
<Text>{t('settings.logout_confirm_message')}</Text>
164+
</AlertDialogBody>
165+
<AlertDialogFooter>
166+
<Button variant="outline" action="secondary" onPress={() => setShowLogoutConfirm(false)}>
167+
<ButtonText>{t('common.cancel')}</ButtonText>
168+
</Button>
169+
<Button action="negative" onPress={handleLogoutConfirm}>
170+
<ButtonText>{t('settings.logout')}</ButtonText>
171+
</Button>
172+
</AlertDialogFooter>
173+
</AlertDialogContent>
174+
</AlertDialog>
125175
</Box>
126176
);
127177
}

src/components/calls/__tests__/call-files-modal.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CallFilesModal } from '../call-files-modal';
77

88
// Mock the zustand store
99
const mockFetchCallFiles = jest.fn();
10+
const mockClearFiles = jest.fn();
1011
const defaultMockFiles = [
1112
{
1213
Id: 'file-1',
@@ -41,6 +42,7 @@ let mockStoreState: any = {
4142
isLoadingFiles: false,
4243
errorFiles: null,
4344
fetchCallFiles: mockFetchCallFiles,
45+
clearFiles: mockClearFiles,
4446
};
4547

4648
jest.mock('@/stores/calls/detail-store', () => ({
@@ -304,6 +306,7 @@ describe('CallFilesModal', () => {
304306
isLoadingFiles: false,
305307
errorFiles: null,
306308
fetchCallFiles: mockFetchCallFiles,
309+
clearFiles: mockClearFiles,
307310
};
308311
});
309312

@@ -423,6 +426,7 @@ describe('CallFilesModal', () => {
423426
isLoadingFiles: true,
424427
errorFiles: null,
425428
fetchCallFiles: mockFetchCallFiles,
429+
clearFiles: mockClearFiles,
426430
};
427431
});
428432

@@ -444,6 +448,7 @@ describe('CallFilesModal', () => {
444448
isLoadingFiles: false,
445449
errorFiles: 'Network error occurred',
446450
fetchCallFiles: mockFetchCallFiles,
451+
clearFiles: mockClearFiles,
447452
};
448453
});
449454

@@ -476,6 +481,7 @@ describe('CallFilesModal', () => {
476481
isLoadingFiles: false,
477482
errorFiles: null,
478483
fetchCallFiles: mockFetchCallFiles,
484+
clearFiles: mockClearFiles,
479485
};
480486
});
481487

@@ -590,6 +596,7 @@ describe('CallFilesModal', () => {
590596
isLoadingFiles: false,
591597
errorFiles: null,
592598
fetchCallFiles: mockFetchCallFiles,
599+
clearFiles: mockClearFiles,
593600
};
594601
});
595602

@@ -623,6 +630,7 @@ describe('CallFilesModal', () => {
623630
isLoadingFiles: false,
624631
errorFiles: null,
625632
fetchCallFiles: mockFetchCallFiles,
633+
clearFiles: mockClearFiles,
626634
};
627635
});
628636

src/components/calls/__tests__/close-call-bottom-sheet.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,32 @@ jest.mock('nativewind', () => ({
4040
// Mock cssInterop globally
4141
(global as any).cssInterop = jest.fn();
4242

43+
// Mock actionsheet components
44+
jest.mock('@/components/ui/actionsheet', () => ({
45+
Actionsheet: ({ children, isOpen }: any) => {
46+
const { View } = require('react-native');
47+
return isOpen ? <View testID="actionsheet">{children}</View> : null;
48+
},
49+
ActionsheetBackdrop: () => null,
50+
ActionsheetContent: ({ children }: any) => {
51+
const { View } = require('react-native');
52+
return <View testID="actionsheet-content">{children}</View>;
53+
},
54+
ActionsheetDragIndicator: () => null,
55+
ActionsheetDragIndicatorWrapper: ({ children }: any) => {
56+
const { View } = require('react-native');
57+
return <View>{children}</View>;
58+
},
59+
}));
60+
61+
// Mock keyboard aware scroll view
62+
jest.mock('react-native-keyboard-controller', () => ({
63+
KeyboardAwareScrollView: ({ children }: any) => {
64+
const { View } = require('react-native');
65+
return <View>{children}</View>;
66+
},
67+
}));
68+
4369
// Mock UI components
4470
jest.mock('@/components/ui/bottom-sheet', () => ({
4571
CustomBottomSheet: ({ children, isOpen }: any) => {

0 commit comments

Comments
 (0)