diff --git a/docs/android-tablet-safe-area-fix.md b/docs/android-tablet-safe-area-fix.md
new file mode 100644
index 00000000..aa64663d
--- /dev/null
+++ b/docs/android-tablet-safe-area-fix.md
@@ -0,0 +1,87 @@
+# Android Tablet Safe Area Handling Fix
+
+## Problem
+On Android tablets, the new call view did not hide or properly handle the bottom Android system navigation bar, causing it to overlap with the bottom buttons of the form.
+
+## Root Cause
+The new call screen was not using proper safe area handling for Android devices, specifically:
+1. Missing `FocusAwareStatusBar` component for edge-to-edge experience
+2. No safe area insets applied to prevent overlap with system UI
+3. Bottom buttons were positioned without considering system navigation bar
+
+## Solution
+
+### 1. Added FocusAwareStatusBar Component
+Added `FocusAwareStatusBar` import and usage to ensure proper edge-to-edge handling on Android:
+
+```tsx
+import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar';
+
+// In component render:
+
+```
+
+The `FocusAwareStatusBar` component automatically:
+- Makes the navigation bar transparent with overlay behavior
+- Sets system UI flags to hide navigation bar when needed
+- Provides a seamless edge-to-edge experience
+
+### 2. Added Safe Area Insets
+Imported and used `useSafeAreaInsets` from `react-native-safe-area-context`:
+
+```tsx
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+
+const insets = useSafeAreaInsets();
+```
+
+### 3. Applied Safe Area Padding
+Applied safe area insets to both top and bottom of the screen:
+
+**Top Padding (ScrollView):**
+```tsx
+
+```
+
+**Bottom Padding (Button Container):**
+```tsx
+
+```
+
+### 4. Safe Area Implementation Details
+
+- **Minimum Padding**: Uses `Math.max(insets.bottom, 16)` to ensure at least 16px of padding even when insets are smaller
+- **Dynamic Padding**: Adapts to different device configurations and orientations
+- **Android Tablets**: Typical navigation bar height is ~48px, which gets properly handled
+- **Cross-Platform**: Works on both iOS and Android devices
+
+## Benefits
+
+1. **No UI Overlap**: Bottom buttons are no longer hidden behind the system navigation bar
+2. **Professional Appearance**: Provides a seamless edge-to-edge experience
+3. **Device Compatibility**: Works across different Android tablet sizes and configurations
+4. **Accessibility**: Ensures all interactive elements are accessible to users
+5. **Consistent UX**: Matches the behavior of other screens in the app
+
+## Files Modified
+
+- `/src/app/call/new/index.tsx`: Added safe area handling and FocusAwareStatusBar
+
+## Testing
+
+The fix should be tested on:
+1. Android tablets with different screen sizes
+2. Devices with different navigation bar heights
+3. Both portrait and landscape orientations
+4. Light and dark themes
+
+## Future Considerations
+
+This pattern should be applied to other screens that might have similar issues with system UI overlap on Android devices.
\ No newline at end of file
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/jest-platform-setup.ts b/jest-platform-setup.ts
index d19ff6dc..411610cb 100644
--- a/jest-platform-setup.ts
+++ b/jest-platform-setup.ts
@@ -19,4 +19,4 @@ Object.defineProperty(global, 'Platform', {
jest.doMock('react-native/Libraries/Utilities/Platform', () => mockPlatform);
// Ensure Platform is available in the global scope for React Navigation and other libs
-(global as any).Platform = mockPlatform;
\ No newline at end of file
+(global as any).Platform = mockPlatform;
diff --git a/src/api/calls/calls.ts b/src/api/calls/calls.ts
index 62513c3e..dfae5528 100644
--- a/src/api/calls/calls.ts
+++ b/src/api/calls/calls.ts
@@ -87,16 +87,20 @@ export const createCall = async (callData: CreateCallRequest) => {
const dispatchEntries: string[] = [];
if (callData.dispatchUsers) {
- dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`));
+ //dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`));
+ dispatchEntries.push(...callData.dispatchUsers);
}
if (callData.dispatchGroups) {
- dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`));
+ //dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`));
+ dispatchEntries.push(...callData.dispatchGroups);
}
if (callData.dispatchRoles) {
- dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`));
+ //dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`));
+ dispatchEntries.push(...callData.dispatchRoles);
}
if (callData.dispatchUnits) {
- dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`));
+ //dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`));
+ dispatchEntries.push(...callData.dispatchUnits);
}
dispatchList = dispatchEntries.join('|');
@@ -130,16 +134,20 @@ export const updateCall = async (callData: UpdateCallRequest) => {
const dispatchEntries: string[] = [];
if (callData.dispatchUsers) {
- dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`));
+ //dispatchEntries.push(...callData.dispatchUsers.map((user) => `U:${user}`));
+ dispatchEntries.push(...callData.dispatchUsers);
}
if (callData.dispatchGroups) {
- dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`));
+ //dispatchEntries.push(...callData.dispatchGroups.map((group) => `G:${group}`));
+ dispatchEntries.push(...callData.dispatchGroups);
}
if (callData.dispatchRoles) {
- dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`));
+ //dispatchEntries.push(...callData.dispatchRoles.map((role) => `R:${role}`));
+ dispatchEntries.push(...callData.dispatchRoles);
}
if (callData.dispatchUnits) {
- dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`));
+ //dispatchEntries.push(...callData.dispatchUnits.map((unit) => `U:${unit}`));
+ dispatchEntries.push(...callData.dispatchUnits);
}
dispatchList = dispatchEntries.join('|');
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index e6ecf903..e49fffdf 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -22,6 +22,7 @@ import { APIProvider } from '@/api';
import { CountlyProvider } from '@/components/common/countly-provider';
import { LiveKitBottomSheet } from '@/components/livekit';
import { PushNotificationModal } from '@/components/push-notification/push-notification-modal';
+import { ToastContainer } from '@/components/toast/toast-container';
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider';
import { loadKeepAliveState } from '@/lib/hooks/use-keep-alive';
import { loadSelectedTheme } from '@/lib/hooks/use-selected-theme';
@@ -181,6 +182,7 @@ function Providers({ children }: { children: React.ReactNode }) {
+
diff --git a/src/app/call/new/index.tsx b/src/app/call/new/index.tsx
index a8fbe2a0..ad623221 100644
--- a/src/app/call/new/index.tsx
+++ b/src/app/call/new/index.tsx
@@ -9,6 +9,7 @@ import React, { useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { ScrollView, View } from 'react-native';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
import * as z from 'zod';
import { createCall } from '@/api/calls/calls';
@@ -20,13 +21,14 @@ import { CustomBottomSheet } from '@/components/ui/bottom-sheet';
import { Box } from '@/components/ui/box';
import { Button, ButtonText } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
+import { FocusAwareStatusBar } from '@/components/ui/focus-aware-status-bar';
import { FormControl, FormControlError, FormControlLabel, FormControlLabelText } from '@/components/ui/form-control';
import { Input, InputField } from '@/components/ui/input';
import { Select, SelectBackdrop, SelectContent, SelectIcon, SelectInput, SelectItem, SelectPortal, SelectTrigger } from '@/components/ui/select';
import { Text } from '@/components/ui/text';
import { Textarea, TextareaInput } from '@/components/ui/textarea';
-import { useToast } from '@/components/ui/toast';
import { useAnalytics } from '@/hooks/use-analytics';
+import { useToast } from '@/hooks/use-toast';
import { useCoreStore } from '@/stores/app/core-store';
import { useCallsStore } from '@/stores/calls/store';
import { type DispatchSelection } from '@/stores/dispatch/store';
@@ -102,6 +104,7 @@ interface What3WordsResponse {
export default function NewCall() {
const { t } = useTranslation();
const { colorScheme } = useColorScheme();
+ const insets = useSafeAreaInsets();
const { callPriorities, callTypes, isLoading, error, fetchCallPriorities, fetchCallTypes } = useCallsStore();
const { config } = useCoreStore();
const { trackEvent } = useAnalytics();
@@ -179,17 +182,27 @@ export default function NewCall() {
data.longitude = selectedLocation.longitude;
}
- // TODO: Implement the API call to create a new call
- console.log('Creating new call with data:', data);
-
+ // Validate priority and type before proceeding
const priority = callPriorities.find((p) => p.Name === data.priority);
const type = callTypes.find((t) => t.Name === data.type);
+ if (!priority) {
+ toast.error(t('calls.invalid_priority'));
+ return;
+ }
+
+ if (!type) {
+ toast.error(t('calls.invalid_type'));
+ return;
+ }
+
+ console.log('Creating new call with data:', data);
+
const response = await createCall({
name: data.name,
nature: data.nature,
- priority: priority?.Id || 0,
- type: type?.Id || '',
+ priority: priority.Id,
+ type: type.Id,
note: data.note,
address: data.address,
latitude: data.latitude,
@@ -204,16 +217,7 @@ export default function NewCall() {
});
// Show success toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.create_success')}
-
- );
- },
- });
+ toast.success(t('calls.create_success'));
// Navigate back to calls list
router.push('/calls');
@@ -221,16 +225,7 @@ export default function NewCall() {
console.error('Error creating call:', error);
// Show error toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.create_error')}
-
- );
- },
- });
+ toast.error(t('calls.create_error'));
}
};
@@ -288,16 +283,7 @@ export default function NewCall() {
*/
const handleAddressSearch = async (address: string) => {
if (!address.trim()) {
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.address_required')}
-
- );
- },
- });
+ toast.warning(t('calls.address_required'));
return;
}
@@ -329,16 +315,7 @@ export default function NewCall() {
handleLocationSelected(newLocation);
// Show success toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.address_found')}
-
- );
- },
- });
+ toast.success(t('calls.address_found'));
} else {
// Multiple results - show selection bottom sheet
setAddressResults(results);
@@ -346,31 +323,13 @@ export default function NewCall() {
}
} else {
// Show error toast for no results
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.address_not_found')}
-
- );
- },
- });
+ toast.error(t('calls.address_not_found'));
}
} catch (error) {
console.error('Error geocoding address:', error);
// Show error toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.geocoding_error')}
-
- );
- },
- });
+ toast.error(t('calls.geocoding_error'));
} finally {
setIsGeocodingAddress(false);
}
@@ -389,16 +348,7 @@ export default function NewCall() {
setShowAddressSelection(false);
// Show success toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.address_found')}
-
- );
- },
- });
+ toast.success(t('calls.address_found'));
};
/**
@@ -416,32 +366,14 @@ export default function NewCall() {
*/
const handleWhat3WordsSearch = async (what3words: string) => {
if (!what3words.trim()) {
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.what3words_required')}
-
- );
- },
- });
+ toast.warning(t('calls.what3words_required'));
return;
}
// Validate what3words format - should be 3 words separated by dots
const w3wRegex = /^[a-z]+\.[a-z]+\.[a-z]+$/;
if (!w3wRegex.test(what3words.trim().toLowerCase())) {
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.what3words_invalid_format')}
-
- );
- },
- });
+ toast.warning(t('calls.what3words_invalid_format'));
return;
}
@@ -468,43 +400,16 @@ export default function NewCall() {
handleLocationSelected(newLocation);
// Show success toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.what3words_found')}
-
- );
- },
- });
+ toast.success(t('calls.what3words_found'));
} else {
// Show error toast for no results
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.what3words_not_found')}
-
- );
- },
- });
+ toast.error(t('calls.what3words_not_found'));
}
} catch (error) {
console.error('Error geocoding what3words:', error);
// Show error toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.what3words_geocoding_error')}
-
- );
- },
- });
+ toast.error(t('calls.what3words_geocoding_error'));
} finally {
setIsGeocodingWhat3Words(false);
}
@@ -525,16 +430,7 @@ export default function NewCall() {
*/
const handlePlusCodeSearch = async (plusCode: string) => {
if (!plusCode.trim()) {
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.plus_code_required')}
-
- );
- },
- });
+ toast.warning(t('calls.plus_code_required'));
return;
}
@@ -562,43 +458,16 @@ export default function NewCall() {
handleLocationSelected(newLocation);
// Show success toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.plus_code_found')}
-
- );
- },
- });
+ toast.success(t('calls.plus_code_found'));
} else {
// Show error toast for no results
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.plus_code_not_found')}
-
- );
- },
- });
+ toast.error(t('calls.plus_code_not_found'));
}
} catch (error) {
console.error('Error geocoding plus code:', error);
// Show error toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.plus_code_geocoding_error')}
-
- );
- },
- });
+ toast.error(t('calls.plus_code_geocoding_error'));
} finally {
setIsGeocodingPlusCode(false);
}
@@ -619,16 +488,7 @@ export default function NewCall() {
*/
const handleCoordinatesSearch = async (coordinates: string) => {
if (!coordinates.trim()) {
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.coordinates_required')}
-
- );
- },
- });
+ toast.warning(t('calls.coordinates_required'));
return;
}
@@ -637,16 +497,7 @@ export default function NewCall() {
const match = coordinates.trim().match(coordRegex);
if (!match) {
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.coordinates_invalid_format')}
-
- );
- },
- });
+ toast.warning(t('calls.coordinates_invalid_format'));
return;
}
@@ -655,16 +506,7 @@ export default function NewCall() {
// Validate coordinate ranges
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.coordinates_out_of_range')}
-
- );
- },
- });
+ toast.warning(t('calls.coordinates_out_of_range'));
return;
}
@@ -692,16 +534,7 @@ export default function NewCall() {
handleLocationSelected(newLocation);
// Show success toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.coordinates_found')}
-
- );
- },
- });
+ toast.success(t('calls.coordinates_found'));
} else {
// Even if no address found, still set the location on the map
const newLocation = {
@@ -713,16 +546,7 @@ export default function NewCall() {
handleLocationSelected(newLocation);
// Show info toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.coordinates_no_address')}
-
- );
- },
- });
+ toast.info(t('calls.coordinates_no_address'));
}
} catch (error) {
console.error('Error reverse geocoding coordinates:', error);
@@ -737,16 +561,7 @@ export default function NewCall() {
handleLocationSelected(newLocation);
// Show warning toast
- toast.show({
- placement: 'top',
- render: () => {
- return (
-
- {t('calls.coordinates_geocoding_error')}
-
- );
- },
- });
+ toast.warning(t('calls.coordinates_geocoding_error'));
} finally {
setIsGeocodingCoordinates(false);
}
@@ -768,6 +583,7 @@ export default function NewCall() {
return (
<>
+
-
+
{t('calls.create_new_call')}
@@ -1055,7 +871,7 @@ export default function NewCall() {
-
+
diff --git a/src/app/onboarding.tsx b/src/app/onboarding.tsx
index e6989c03..7a96679f 100644
--- a/src/app/onboarding.tsx
+++ b/src/app/onboarding.tsx
@@ -41,10 +41,12 @@ const onboardingData: OnboardingItemProps[] = [
const OnboardingItem: React.FC = ({ title, description, icon }) => {
return (
-
+
{icon}
- {title}
- {description}
+ {title}
+
+ {description}
+
);
};
@@ -64,7 +66,7 @@ export default function Onboarding() {
const { status, setIsOnboarding } = useAuthStore();
const router = useRouter();
const [currentIndex, setCurrentIndex] = useState(0);
- const flatListRef = useRef(null); // FlashList ref type
+ const flatListRef = useRef>(null);
const buttonOpacity = useSharedValue(0);
const { colorScheme } = useColorScheme();
@@ -108,18 +110,21 @@ export default function Onboarding() {
- }
- horizontal
- showsHorizontalScrollIndicator={false}
- pagingEnabled
- bounces={false}
- keyExtractor={(item) => item.title}
- onScroll={handleScroll}
- scrollEventThrottle={16}
- />
+
+ }
+ horizontal
+ showsHorizontalScrollIndicator={false}
+ pagingEnabled
+ bounces={false}
+ keyExtractor={(item) => item.title}
+ onScroll={handleScroll}
+ scrollEventThrottle={16}
+ className="flex-1"
+ />
+
diff --git a/src/components/toast/__tests__/toast-container.test.tsx b/src/components/toast/__tests__/toast-container.test.tsx
new file mode 100644
index 00000000..47b1d2d2
--- /dev/null
+++ b/src/components/toast/__tests__/toast-container.test.tsx
@@ -0,0 +1,78 @@
+import { render } from '@testing-library/react-native';
+import React from 'react';
+
+import { useToastStore } from '@/stores/toast/store';
+
+import { ToastContainer } from '../toast-container';
+
+// Mock the toast store
+jest.mock('@/stores/toast/store', () => ({
+ useToastStore: jest.fn(),
+}));
+
+// Mock react-native-safe-area-context
+jest.mock('react-native-safe-area-context', () => ({
+ useSafeAreaInsets: jest.fn(() => ({
+ top: 44,
+ bottom: 0,
+ left: 0,
+ right: 0,
+ })),
+}));
+
+// Mock the ToastMessage component
+jest.mock('../toast', () => ({
+ ToastMessage: ({ type, message, title }: any) => {
+ const { Text } = require('react-native');
+ return (
+
+ {title && `${title}: `}
+ {message}
+
+ );
+ },
+}));
+
+describe('ToastContainer', () => {
+ const mockUseToastStore = useToastStore as jest.MockedFunction;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders nothing when no toasts are present', () => {
+ mockUseToastStore.mockReturnValue([]);
+
+ const { queryByTestId } = render();
+
+ expect(queryByTestId('toast-success')).toBeNull();
+ expect(queryByTestId('toast-error')).toBeNull();
+ });
+
+ it('renders toasts when they are present in the store', () => {
+ const mockToasts = [
+ { id: '1', type: 'success' as const, message: 'Success message' },
+ { id: '2', type: 'error' as const, message: 'Error message', title: 'Error Title' },
+ ];
+
+ mockUseToastStore.mockReturnValue(mockToasts);
+
+ const { getByTestId } = render();
+
+ expect(getByTestId('toast-success')).toBeTruthy();
+ expect(getByTestId('toast-error')).toBeTruthy();
+ });
+
+ it('renders toast with title and message correctly', () => {
+ const mockToasts = [
+ { id: '1', type: 'warning' as const, message: 'Warning message', title: 'Warning Title' },
+ ];
+
+ mockUseToastStore.mockReturnValue(mockToasts);
+
+ const { getByTestId } = render();
+
+ const toastElement = getByTestId('toast-warning');
+ expect(toastElement.props.children).toEqual(['Warning Title: ', 'Warning message']);
+ });
+});
\ No newline at end of file
diff --git a/src/components/toast/toast-container.tsx b/src/components/toast/toast-container.tsx
index 171cf2a5..3d063244 100644
--- a/src/components/toast/toast-container.tsx
+++ b/src/components/toast/toast-container.tsx
@@ -1,14 +1,21 @@
import React from 'react';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { VStack } from '@/components/ui/vstack';
import { useToastStore } from '../../stores/toast/store';
import { ToastMessage } from './toast';
+
export const ToastContainer: React.FC = () => {
const toasts = useToastStore((state) => state.toasts);
+ const insets = useSafeAreaInsets();
+
+ // Position below status bar and navigation header
+ // Use a larger offset to ensure toasts appear below any navigation content
+ const topOffset = insets.top + 70; // Status bar height + generous navigation header height
return (
-
+
{toasts.map((toast) => (
))}
diff --git a/src/components/toast/toast.tsx b/src/components/toast/toast.tsx
index f789b3d7..95b88f7e 100644
--- a/src/components/toast/toast.tsx
+++ b/src/components/toast/toast.tsx
@@ -6,29 +6,6 @@ import { VStack } from '@/components/ui/vstack';
import { type ToastType, useToastStore } from '../../stores/toast/store';
import { Toast, ToastDescription, ToastTitle } from '../ui/toast';
-const toastStyles = {
- info: {
- bg: '$info700',
- borderColor: '$info800',
- },
- success: {
- bg: '$success700',
- borderColor: '$success800',
- },
- warning: {
- bg: '$warning700',
- borderColor: '$warning800',
- },
- error: {
- bg: '$error700',
- borderColor: '$error800',
- },
- muted: {
- bg: '$muted700',
- borderColor: '$muted800',
- },
-};
-
export const ToastMessage: React.FC<{
//id: string;
type: ToastType;
@@ -39,7 +16,7 @@ export const ToastMessage: React.FC<{
const { t } = useTranslation();
return (
-
+
{title && {t(title)}}
{t(message)}
diff --git a/src/components/ui/bottomsheet/index.tsx b/src/components/ui/bottomsheet/index.tsx
index e83bc302..06800a6f 100644
--- a/src/components/ui/bottomsheet/index.tsx
+++ b/src/components/ui/bottomsheet/index.tsx
@@ -42,8 +42,8 @@ const BottomSheetContext = createContext<{
}>({
visible: false,
bottomSheetRef: { current: null },
- handleClose: () => { },
- handleOpen: () => { },
+ handleClose: () => {},
+ handleOpen: () => {},
});
type IBottomSheetProps = React.ComponentProps;
@@ -166,14 +166,14 @@ export const BottomSheetContent = ({ ...props }: IBottomSheetContent) => {
const keyDownHandlers = useMemo(() => {
return Platform.OS === 'web'
? {
- onKeyDown: (e: React.KeyboardEvent) => {
- if (e.key === 'Escape') {
- e.preventDefault();
- handleClose();
- return;
- }
- },
- }
+ onKeyDown: (e: React.KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ handleClose();
+ return;
+ }
+ },
+ }
: {};
}, [handleClose]);
diff --git a/src/hooks/__tests__/use-toast.test.tsx b/src/hooks/__tests__/use-toast.test.tsx
new file mode 100644
index 00000000..5ce94243
--- /dev/null
+++ b/src/hooks/__tests__/use-toast.test.tsx
@@ -0,0 +1,69 @@
+import { renderHook } from '@testing-library/react-native';
+
+import { useToast } from '@/hooks/use-toast';
+import { useToastStore } from '@/stores/toast/store';
+
+// Mock the toast store
+jest.mock('@/stores/toast/store', () => ({
+ useToastStore: jest.fn(),
+}));
+
+describe('useToast', () => {
+ const mockShowToast = jest.fn();
+ const mockUseToastStore = useToastStore as jest.MockedFunction;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockUseToastStore.mockReturnValue({
+ showToast: mockShowToast,
+ });
+ });
+
+ it('should provide show method that calls showToast', () => {
+ const { result } = renderHook(() => useToast());
+
+ result.current.show('info', 'Test message', 'Test title');
+
+ expect(mockShowToast).toHaveBeenCalledWith('info', 'Test message', 'Test title');
+ });
+
+ it('should provide success method that calls showToast with success type', () => {
+ const { result } = renderHook(() => useToast());
+
+ result.current.success('Success message', 'Success title');
+
+ expect(mockShowToast).toHaveBeenCalledWith('success', 'Success message', 'Success title');
+ });
+
+ it('should provide error method that calls showToast with error type', () => {
+ const { result } = renderHook(() => useToast());
+
+ result.current.error('Error message', 'Error title');
+
+ expect(mockShowToast).toHaveBeenCalledWith('error', 'Error message', 'Error title');
+ });
+
+ it('should provide warning method that calls showToast with warning type', () => {
+ const { result } = renderHook(() => useToast());
+
+ result.current.warning('Warning message', 'Warning title');
+
+ expect(mockShowToast).toHaveBeenCalledWith('warning', 'Warning message', 'Warning title');
+ });
+
+ it('should provide info method that calls showToast with info type', () => {
+ const { result } = renderHook(() => useToast());
+
+ result.current.info('Info message', 'Info title');
+
+ expect(mockShowToast).toHaveBeenCalledWith('info', 'Info message', 'Info title');
+ });
+
+ it('should work without title parameter', () => {
+ const { result } = renderHook(() => useToast());
+
+ result.current.success('Success message');
+
+ expect(mockShowToast).toHaveBeenCalledWith('success', 'Success message', undefined);
+ });
+});
\ No newline at end of file
diff --git a/src/translations/ar.json b/src/translations/ar.json
index 160b5c50..9ab68a22 100644
--- a/src/translations/ar.json
+++ b/src/translations/ar.json
@@ -231,6 +231,8 @@
},
"geocoding_error": "فشل البحث عن العنوان، يرجى المحاولة مرة أخرى",
"groups": "المجموعات",
+ "invalid_priority": "أولوية غير صحيحة محددة. يرجى اختيار أولوية صحيحة.",
+ "invalid_type": "نوع غير صحيح محدد. يرجى اختيار نوع مكالمة صحيح.",
"loading": "جاري تحميل المكالمات...",
"loading_calls": "جاري تحميل المكالمات...",
"name": "الاسم",
diff --git a/src/translations/en.json b/src/translations/en.json
index 9ed96f77..d37836e2 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -231,6 +231,8 @@
},
"geocoding_error": "Failed to search for address, please try again",
"groups": "Groups",
+ "invalid_priority": "Invalid priority selected. Please select a valid priority.",
+ "invalid_type": "Invalid type selected. Please select a valid call type.",
"loading": "Loading calls...",
"loading_calls": "Loading calls...",
"name": "Name",
diff --git a/src/translations/es.json b/src/translations/es.json
index ac432e47..ee1b4a29 100644
--- a/src/translations/es.json
+++ b/src/translations/es.json
@@ -231,6 +231,8 @@
},
"geocoding_error": "Error al buscar la dirección, por favor inténtelo de nuevo",
"groups": "Grupos",
+ "invalid_priority": "Prioridad inválida seleccionada. Por favor seleccione una prioridad válida.",
+ "invalid_type": "Tipo inválido seleccionado. Por favor seleccione un tipo de llamada válido.",
"loading": "Cargando llamadas...",
"loading_calls": "Cargando llamadas...",
"name": "Nombre",