Improve request error handling

This commit is contained in:
Arnaud Vergnet 2021-05-13 19:10:28 +02:00
parent a1cfb0385a
commit d55c692bd3
8 changed files with 100 additions and 78 deletions

View file

@ -1,10 +1,17 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import ErrorView from './ErrorView'; import ErrorView from './ErrorView';
import { useRequestLogic } from '../../utils/customHooks'; import { useRequestLogic } from '../../utils/customHooks';
import { useFocusEffect } from '@react-navigation/native'; import {
useFocusEffect,
useNavigation,
useRoute,
} from '@react-navigation/native';
import BasicLoadingScreen from './BasicLoadingScreen'; import BasicLoadingScreen from './BasicLoadingScreen';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests'; import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
import { StackNavigationProp } from '@react-navigation/stack';
import { MainRoutes } from '../../navigation/MainNavigator';
import ConnectionManager from '../../managers/ConnectionManager';
export type RequestScreenProps<T> = { export type RequestScreenProps<T> = {
request: () => Promise<T>; request: () => Promise<T>;
@ -37,6 +44,8 @@ type Props<T> = RequestScreenProps<T>;
const MIN_REFRESH_TIME = 5 * 1000; const MIN_REFRESH_TIME = 5 * 1000;
export default function RequestScreen<T>(props: Props<T>) { export default function RequestScreen<T>(props: Props<T>) {
const navigation = useNavigation<StackNavigationProp<any>>();
const route = useRoute();
const refreshInterval = useRef<number>(); const refreshInterval = useRef<number>();
const [ const [
loading, loading,
@ -89,22 +98,42 @@ export default function RequestScreen<T>(props: Props<T>) {
}, [props.cache, props.refreshOnFocus]) }, [props.cache, props.refreshOnFocus])
); );
const isErrorCritical = (e: API_REQUEST_CODES | undefined) => {
return e === API_REQUEST_CODES.BAD_TOKEN;
};
useEffect(() => {
if (isErrorCritical(code)) {
ConnectionManager.getInstance()
.disconnect()
.then(() => {
navigation.replace(MainRoutes.Login, { nextScreen: route.name });
});
}
}, [code, navigation, route]);
if (data === undefined && loading && props.showLoading !== false) { if (data === undefined && loading && props.showLoading !== false) {
return <BasicLoadingScreen />; return <BasicLoadingScreen />;
} else if ( } else if (
data === undefined && data === undefined &&
status !== REQUEST_STATUS.SUCCESS && (status !== REQUEST_STATUS.SUCCESS ||
(status === REQUEST_STATUS.SUCCESS && code !== undefined)) &&
props.showError !== false props.showError !== false
) { ) {
return ( return (
<ErrorView <ErrorView
status={status} status={status}
code={code}
loading={loading} loading={loading}
button={{ button={
icon: 'refresh', isErrorCritical(code)
text: i18n.t('general.retry'), ? undefined
onPress: () => refreshData(), : {
}} icon: 'refresh',
text: i18n.t('general.retry'),
onPress: () => refreshData(),
}
}
/> />
); );
} else { } else {

View file

@ -156,7 +156,7 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
this.categories = data?.categories; this.categories = data?.categories;
return [{ title: '', data: data.clubs }]; return [{ title: '', data: data.clubs }];
} else { } else {
return [{ title: '', data: [] }]; return [];
} }
}; };

View file

@ -136,7 +136,7 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
} }
return [{ title: '', data: data.devices }]; return [{ title: '', data: data.devices }];
} else { } else {
return [{ title: '', data: [] }]; return [];
} }
}; };

View file

@ -110,10 +110,9 @@ class LoginScreen extends React.Component<Props, StateType> {
this.onInputChange(false, value); this.onInputChange(false, value);
}; };
props.navigation.addListener('focus', this.onScreenFocus); props.navigation.addListener('focus', this.onScreenFocus);
// TODO remove
this.state = { this.state = {
email: 'vergnet@etud.insa-toulouse.fr', email: '',
password: 'IGtt25ùj', password: '',
isEmailValidated: false, isEmailValidated: false,
isPasswordValidated: false, isPasswordValidated: false,
loading: false, loading: false,
@ -373,7 +372,7 @@ class LoginScreen extends React.Component<Props, StateType> {
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters * Saves the screen to navigate to after a successful login if one was provided in navigation parameters
*/ */
handleNavigationParams() { handleNavigationParams() {
this.nextScreen = this.props.route.params.nextScreen; this.nextScreen = this.props.route.params?.nextScreen;
} }
/** /**

View file

@ -121,12 +121,16 @@ function GroupSelectionScreen() {
} }
| undefined | undefined
): Array<{ title: string; data: Array<PlanexGroupCategoryType> }> => { ): Array<{ title: string; data: Array<PlanexGroupCategoryType> }> => {
return [ if (fetchedData) {
{ return [
title: '', {
data: generateData(fetchedData), title: '',
}, data: generateData(fetchedData),
]; },
];
} else {
return [];
}
}; };
/** /**
@ -181,39 +185,34 @@ function GroupSelectionScreen() {
* @returns {[]} * @returns {[]}
*/ */
const generateData = ( const generateData = (
fetchedData: PlanexGroupsType | undefined fetchedData: PlanexGroupsType
): Array<PlanexGroupCategoryType> => { ): Array<PlanexGroupCategoryType> => {
const data: Array<PlanexGroupCategoryType> = []; const data: Array<PlanexGroupCategoryType> = [];
// Convert the object into an array
if (fetchedData) { Object.values(fetchedData).forEach((category: PlanexGroupCategoryType) => {
// Convert the object into an array const content: Array<PlanexGroupType> = [];
Object.values(fetchedData).forEach( // Filter groups matching the search query
(category: PlanexGroupCategoryType) => { category.content.forEach((g: PlanexGroupType) => {
const content: Array<PlanexGroupType> = []; if (stringMatchQuery(g.name, currentSearchString)) {
// Filter groups matching the search query content.push(g);
category.content.forEach((g: PlanexGroupType) => {
if (stringMatchQuery(g.name, currentSearchString)) {
content.push(g);
}
});
// Only add categories with groups matching the query
if (content.length > 0) {
data.push({
id: category.id,
name: category.name,
content: content,
});
}
} }
);
data.sort(sortName);
// Add the favorites at the top
data.unshift({
name: i18n.t('screens.planex.favorites.title'),
id: 0,
content: favoriteGroups,
}); });
} // Only add categories with groups matching the query
if (content.length > 0) {
data.push({
id: category.id,
name: category.name,
content: content,
});
}
});
data.sort(sortName);
// Add the favorites at the top
data.unshift({
name: i18n.t('screens.planex.favorites.title'),
id: 0,
content: favoriteGroups,
});
return data; return data;
}; };

View file

@ -349,13 +349,7 @@ function ProximoListScreen(props: Props) {
}, },
]; ];
} else { } else {
return [ return [];
{
title: '',
data: [],
keyExtractor: keyExtractor,
},
];
} }
}; };

View file

@ -239,13 +239,7 @@ function ProximoMainScreen() {
}, },
]; ];
} else { } else {
return [ return [];
{
title: '',
data: [],
keyExtractor: getKeyExtractor,
},
];
} }
}; };

View file

@ -27,6 +27,7 @@ import WebSectionList from '../../components/Screens/WebSectionList';
import type { SectionListDataType } from '../../components/Screens/WebSectionList'; import type { SectionListDataType } from '../../components/Screens/WebSectionList';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import { readData } from '../../utils/WebData'; import { readData } from '../../utils/WebData';
import { REQUEST_STATUS } from '../../utils/Requests';
type PropsType = { type PropsType = {
navigation: StackNavigationProp<any>; navigation: StackNavigationProp<any>;
@ -109,25 +110,31 @@ class SelfMenuScreen extends React.Component<PropsType> {
* @return {[]} * @return {[]}
*/ */
createDataset = ( createDataset = (
fetchedData: Array<RawRuMenuType> | undefined fetchedData: Array<RawRuMenuType> | undefined,
_loading: boolean,
_lastRefreshDate: Date | undefined,
_refreshData: (newRequest?: () => Promise<Array<RawRuMenuType>>) => void,
status: REQUEST_STATUS
): SectionListDataType<RuFoodCategoryType> => { ): SectionListDataType<RuFoodCategoryType> => {
let result: SectionListDataType<RuFoodCategoryType> = []; let result: SectionListDataType<RuFoodCategoryType> = [];
if (fetchedData == null || fetchedData.length === 0) { if (status === REQUEST_STATUS.SUCCESS) {
result = [ if (fetchedData == null || fetchedData.length === 0) {
{ result = [
title: i18n.t('general.notAvailable'), {
data: [], title: i18n.t('general.notAvailable'),
keyExtractor: this.getKeyExtractor, data: [],
}, keyExtractor: this.getKeyExtractor,
]; },
} else { ];
fetchedData.forEach((item: RawRuMenuType) => { } else {
result.push({ fetchedData.forEach((item: RawRuMenuType) => {
title: DateManager.getInstance().getTranslatedDate(item.date), result.push({
data: item.meal[0].foodcategory, title: DateManager.getInstance().getTranslatedDate(item.date),
keyExtractor: this.getKeyExtractor, data: item.meal[0].foodcategory,
keyExtractor: this.getKeyExtractor,
});
}); });
}); }
} }
return result; return result;
}; };