Browse Source

added popup for api errors

Viktoriia 1 năm trước cách đây
mục cha
commit
c6bfe536a6

+ 31 - 10
App.tsx

@@ -7,18 +7,39 @@ import Route from './Route';
 import { ConnectionProvider } from 'src/contexts/ConnectionContext';
 import ConnectionBanner from 'src/components/ConnectionBanner/ConnectionBanner';
 import { RegionProvider } from 'src/contexts/RegionContext';
+import { ErrorProvider, useError } from 'src/contexts/ErrorContext';
+import { useEffect } from 'react';
+import { setupInterceptors } from 'src/utils/request';
+import { ErrorModal } from 'src/components';
 
-export default function App() {
+const App = () => {
   return (
     <QueryClientProvider client={queryClient}>
-      <ConnectionProvider>
-        <RegionProvider>
-          <NavigationContainer>
-            <ConnectionBanner />
-            <Route />
-          </NavigationContainer>
-        </RegionProvider>
-      </ConnectionProvider>
+      <ErrorProvider>
+        <InnerApp />
+      </ErrorProvider>
     </QueryClientProvider>
   );
-}
+};
+
+const InnerApp = () => {
+  const errorContext = useError();
+
+  useEffect(() => {
+    setupInterceptors(errorContext);
+  }, [errorContext]);
+
+  return (
+    <ConnectionProvider>
+      <RegionProvider>
+        <NavigationContainer>
+          <ConnectionBanner />
+          <Route />
+          <ErrorModal />
+        </NavigationContainer>
+      </RegionProvider>
+    </ConnectionProvider>
+  );
+};
+
+export default App;

+ 61 - 0
src/components/ErrorModal/index.tsx

@@ -0,0 +1,61 @@
+import React from 'react';
+import { View, Text, TouchableOpacity } from 'react-native';
+import Modal from 'react-native-modal';
+import { useError } from 'src/contexts/ErrorContext';
+
+import { styles } from '../WarningModal/styles';
+import { Colors } from 'src/theme';
+
+import CloseIcon from 'assets/icons/close.svg';
+import { ButtonVariants } from 'src/types/components';
+import { Button } from '../Button';
+import { CommonActions, useNavigation } from '@react-navigation/native';
+import { NAVIGATION_PAGES } from 'src/types';
+
+export const ErrorModal = () => {
+  const { error, hideError } = useError();
+  const navigation = useNavigation();
+
+  const handleClose = () => {
+    navigation.dispatch(
+      CommonActions.reset({
+        index: 1,
+        routes: [{ name: NAVIGATION_PAGES.IN_APP_MAP_TAB }]
+      })
+    );
+    hideError();
+  };
+
+  return (
+    <Modal isVisible={!!error}>
+      <View style={styles.centeredView}>
+        <View style={styles.modalView}>
+          <View style={{ alignSelf: 'flex-end' }}>
+            <TouchableOpacity onPress={handleClose}>
+              <CloseIcon fill={Colors.LIGHT_GRAY} />
+            </TouchableOpacity>
+          </View>
+          <View style={styles.modalContent}>
+            <Text style={styles.modalTitle}>Oops!</Text>
+            <Text style={styles.modalText}>An error occurred: {error}</Text>
+            <View style={styles.buttonContainer}>
+              <Button
+                variant={ButtonVariants.OPACITY}
+                containerStyles={{
+                  borderColor: Colors.DARK_BLUE,
+                  backgroundColor: Colors.DARK_BLUE,
+                  width: '60%'
+                }}
+                textStyles={{
+                  color: Colors.WHITE
+                }}
+                onPress={handleClose}
+                children="OK"
+              />
+            </View>
+          </View>
+        </View>
+      </View>
+    </Modal>
+  );
+};

+ 1 - 0
src/components/index.ts

@@ -17,3 +17,4 @@ export * from './WarningModal';
 export * from './HorizontalTabView';
 export * from './EditNmModal';
 export * from './MenuDrawer';
+export * from './ErrorModal';

+ 31 - 0
src/contexts/ErrorContext.tsx

@@ -0,0 +1,31 @@
+import React, { createContext, useState, useContext } from 'react';
+
+const ErrorContext = createContext<{
+  error: string | null;
+  showError: (message: string) => void;
+  hideError: () => void;
+}>({
+  error: null,
+  showError: (message: string) => {},
+  hideError: () => {}
+});
+
+export const ErrorProvider = ({ children }: { children: React.ReactNode }) => {
+  const [error, setError] = useState<string | null>(null);
+
+  const showError = (message: string) => {
+    setError(message);
+  };
+
+  const hideError = () => {
+    setError(null);
+  };
+
+  return (
+    <ErrorContext.Provider value={{ error, showError, hideError }}>
+      {children}
+    </ErrorContext.Provider>
+  );
+};
+
+export const useError = () => useContext(ErrorContext);

+ 3 - 2
src/modules/api/search/queries/use-post-universal.tsx

@@ -5,12 +5,13 @@ import { searchApi, type PostGetUniversalReturn } from '../search-api';
 
 import type { BaseAxiosError } from '../../../../types';
 
-export const useGetUniversalSearch = (search: string) => {
+export const useGetUniversalSearch = (search: string, enabled: boolean) => {
   return useQuery<PostGetUniversalReturn, BaseAxiosError>({
     queryKey: searchQueryKeys.getUniversal(search),
     queryFn: async () => {
       const response = await searchApi.getUniversal(search);
       return response.data;
-    }
+    },
+    enabled
   });
 };

+ 1 - 1
src/screens/InAppScreens/MapScreen/index.tsx

@@ -110,7 +110,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
   });
   const [search, setSearch] = useState('');
   const [searchInput, setSearchInput] = useState('');
-  const { data: searchData } = useGetUniversalSearch(search);
+  const { data: searchData } = useGetUniversalSearch(search, search.length > 0);
   const [isFilterVisible, setIsFilterVisible] = useState(false);
   const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 });
   const tilesTypes = [

+ 5 - 1
src/screens/InAppScreens/ProfileScreen/index.tsx

@@ -59,6 +59,7 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
         navigation.getParent()?.setOptions({
           tabBarStyle: {
             display: 'flex',
+            position: 'absolute',
             ...Platform.select({
               android: {
                 height: 58
@@ -140,7 +141,10 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
   return (
     <PageWrapper>
       {isPublicView && <Header label="Profile" />}
-      <ScrollView showsVerticalScrollIndicator={false}>
+      <ScrollView
+        showsVerticalScrollIndicator={false}
+        contentContainerStyle={{ paddingBottom: 58 }}
+      >
         <TouchableOpacity
           style={[styles.usersMap, { backgroundColor: '#EBF2F5' }]}
           onPress={handleGoToMap}

+ 8 - 2
src/screens/InAppScreens/TravelsScreen/AddNewTripScreen/index.tsx

@@ -35,6 +35,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
   const [qualitySelectorVisible, setQualitySelectorVisible] = useState(false);
   const [selectedRegionId, setSelectedRegionId] = useState<number | null>(null);
   const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
+  const [tripDeleted, setTripDeleted] = useState(false);
 
   const { mutate: saveNewTrip } = usePostSetNewTripMutation();
   const { mutate: updateTrip } = usePostUpdateTripMutation();
@@ -136,6 +137,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
   };
 
   const handleDeleteTrip = () => {
+    setIsWarningModalVisible(false);
     deleteTrip(
       {
         token,
@@ -143,8 +145,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
       },
       {
         onSuccess: () => {
-          setIsWarningModalVisible(false);
-          navigation.navigate(...([NAVIGATION_PAGES.TRIPS, { deleted: true }] as never));
+          setTripDeleted(true);
         }
       }
     );
@@ -349,6 +350,11 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
       <WarningModal
         type={'delete'}
         isVisible={isWarningModalVisible}
+        onModalHide={() => {
+          if (tripDeleted) {
+            navigation.navigate(...([NAVIGATION_PAGES.TRIPS, { deleted: true }] as never));
+          }
+        }}
         onClose={() => setIsWarningModalVisible(false)}
         title="Delete Trip"
         message="Are you sure you want to delete your trip?"

+ 5 - 7
src/screens/InAppScreens/TravelsScreen/DareScreen/index.tsx

@@ -42,13 +42,11 @@ const DareScreen = () => {
     setUserData
   } = useRegion();
 
-  useFocusEffect(
-    useCallback(() => {
-      if (megaregions && megaregions.result === 'OK') {
-        setSelectedMega(megaregions.data[1]);
-      }
-    }, [megaregions])
-  );
+  useEffect(() => {
+    if (megaregions && megaregions.result === 'OK') {
+      setSelectedMega(megaregions.data[1]);
+    }
+  }, [megaregions])
 
   useFocusEffect(
     useCallback(() => {

+ 5 - 7
src/screens/InAppScreens/TravelsScreen/RegionsScreen/index.tsx

@@ -153,13 +153,11 @@ const RegionsScreen = () => {
     }
   }, [contentIndex, nmRegions, tccRegions]);
 
-  useFocusEffect(
-    useCallback(() => {
-      if (megaregions && megaregions.result === 'OK') {
-        setSelectedMega(megaregions.data[1]);
-      }
-    }, [megaregions])
-  );
+  useEffect(() => {
+    if (megaregions && megaregions.result === 'OK') {
+      setSelectedMega(megaregions.data[1]);
+    }
+  }, [megaregions])
 
   const calcTotalCountries = () => {
     const visited = nmRegions?.filter((item: NmRegion) => item.visits > 0).length || 0;

+ 4 - 4
src/screens/InAppScreens/TravelsScreen/TripsScreen/index.tsx

@@ -22,11 +22,11 @@ const TripsScreen = ({ route }: { route: any }) => {
   const navigation = useNavigation();
   const [isDatePickerVisible, setDatePickerVisible] = useState(false);
   const { data: years, refetch } = useGetTripsYearsQuery(token, true);
-  const [selectedYear, setSelectedYear] = useState<string>('');
+  const [selectedYear, setSelectedYear] = useState<string | null>(null);
   const { data: tripsData, refetch: refetchTrips } = useGetTripsForYearQuery(
     token,
-    selectedYear,
-    true
+    selectedYear as string,
+    selectedYear ? true : false
   );
   const [trips, setTrips] = useState<TripsData[]>([]);
   const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
@@ -50,7 +50,7 @@ const TripsScreen = ({ route }: { route: any }) => {
   );
 
   useEffect(() => {
-    if (years && years.data && selectedYear.length === 0) {
+    if (years && years.data && !selectedYear) {
       setSelectedYear(years.data[0]);
     }
   }, [years]);

+ 33 - 17
src/utils/request.ts

@@ -5,23 +5,39 @@ import { showBanner } from './bannerUtils';
 
 export const request = axios.create({
   baseURL: API_URL,
-  timeout: 10000,
+  timeout: 10000
 });
 
-request.interceptors.request.use(config => {
-  config.headers['App-Version'] = APP_VERSION;
-  config.headers['Platform'] = Platform.OS;
-  return config;
-}, error => {
-  return Promise.reject(error);
-});
+export const setupInterceptors = ({ showError }: { showError: (message: string) => void }) => {
+  request.interceptors.request.use(
+    (config) => {
+      config.headers['App-Version'] = APP_VERSION;
+      config.headers['Platform'] = Platform.OS;
+      return config;
+    },
+    (error) => {
+      return Promise.reject(error);
+    }
+  );
 
-request.interceptors.response.use(response => {
-  return response;
-}, error => {
-  if (error.code === 'ECONNABORTED') {
-    error.isTimeout = true;
-    showBanner('Slow internet connection!');
-  }
-  return Promise.reject(error);
-});
+  request.interceptors.response.use(
+    (response) => {
+      if (response.data.result === 'ERROR' && response.data.result_description) {
+        showError(response.data.result_description);
+      }
+      return response;
+    },
+    (error) => {
+      if (error.code === 'ECONNABORTED' || error.message === 'Network Error') {
+        error.isTimeout = true;
+        showBanner('Slow internet connection!');
+
+        return Promise.reject(error);
+      }
+
+      showError(error.message);
+
+      return Promise.reject(error);
+    }
+  );
+};