Compare commits

...

2 commits

Author SHA1 Message Date
Arnaud Vergnet
742643b9e2 Replace Authenticated screen by RequestScreen 2021-05-13 13:19:28 +02:00
Arnaud Vergnet
9b4caade00 Update api error codes 2021-05-13 10:58:47 +02:00
19 changed files with 460 additions and 722 deletions

View file

@ -1,245 +0,0 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { StackNavigationProp } from '@react-navigation/stack';
import ConnectionManager from '../../managers/ConnectionManager';
import { ERROR_TYPE } from '../../utils/WebData';
import ErrorView from '../Screens/ErrorView';
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
import i18n from 'i18n-js';
type PropsType<T> = {
navigation: StackNavigationProp<any>;
requests: Array<{
link: string;
params: object;
mandatory: boolean;
}>;
renderFunction: (data: Array<T | null>) => React.ReactNode;
errorViewOverride?: Array<{
errorCode: number;
message: string;
icon: string;
showRetryButton: boolean;
}> | null;
};
type StateType = {
loading: boolean;
};
class AuthenticatedScreen<T> extends React.Component<PropsType<T>, StateType> {
static defaultProps = {
errorViewOverride: null,
};
currentUserToken: string | null;
connectionManager: ConnectionManager;
errors: Array<number>;
fetchedData: Array<T | null>;
constructor(props: PropsType<T>) {
super(props);
this.state = {
loading: true,
};
this.currentUserToken = null;
this.connectionManager = ConnectionManager.getInstance();
props.navigation.addListener('focus', this.onScreenFocus);
this.fetchedData = new Array(props.requests.length);
this.errors = new Array(props.requests.length);
}
/**
* Refreshes screen if user changed
*/
onScreenFocus = () => {
if (this.currentUserToken !== this.connectionManager.getToken()) {
this.currentUserToken = this.connectionManager.getToken();
this.fetchData();
}
};
/**
* Callback used when a request finishes, successfully or not.
* Saves data and error code.
* If the token is invalid, logout the user and open the login screen.
* If the last request was received, stop the loading screen.
*
* @param data The data fetched from the server
* @param index The index for the data
* @param error The error code received
*/
onRequestFinished(data: T | null, index: number, error?: number) {
const { props } = this;
if (index >= 0 && index < props.requests.length) {
this.fetchedData[index] = data;
this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS;
}
// Token expired, logout user
if (error === ERROR_TYPE.BAD_TOKEN) {
this.connectionManager.disconnect();
}
if (this.allRequestsFinished()) {
this.setState({ loading: false });
}
}
/**
* Gets the error to render.
* Non-mandatory requests are ignored.
*
*
* @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found
*/
getError(): number {
const { props } = this;
for (let i = 0; i < this.errors.length; i += 1) {
if (
this.errors[i] !== ERROR_TYPE.SUCCESS &&
props.requests[i].mandatory
) {
return this.errors[i];
}
}
return ERROR_TYPE.SUCCESS;
}
/**
* Gets the error view to display in case of error
*
* @return {*}
*/
getErrorRender() {
const { props } = this;
const errorCode = this.getError();
let shouldOverride = false;
let override = null;
const overrideList = props.errorViewOverride;
if (overrideList != null) {
for (let i = 0; i < overrideList.length; i += 1) {
if (overrideList[i].errorCode === errorCode) {
shouldOverride = true;
override = overrideList[i];
break;
}
}
}
if (shouldOverride && override != null) {
return (
<ErrorView
icon={override.icon}
message={override.message}
button={
override.showRetryButton
? {
icon: 'refresh',
text: i18n.t('general.retry'),
onPress: this.fetchData,
}
: undefined
}
/>
);
}
return (
<ErrorView
status={errorCode}
button={{
icon: 'refresh',
text: i18n.t('general.retry'),
onPress: this.fetchData,
}}
/>
);
}
/**
* Fetches the data from the server.
*
* If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
*
* If the user is logged in, send all requests.
*/
fetchData = () => {
const { state, props } = this;
if (!state.loading) {
this.setState({ loading: true });
}
if (this.connectionManager.isLoggedIn()) {
for (let i = 0; i < props.requests.length; i += 1) {
this.connectionManager
.authenticatedRequest<T>(
props.requests[i].link,
props.requests[i].params
)
.then((response: T): void => this.onRequestFinished(response, i))
.catch((error: number): void =>
this.onRequestFinished(null, i, error)
);
}
} else {
for (let i = 0; i < props.requests.length; i += 1) {
this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN);
}
}
};
/**
* Checks if all requests finished processing
*
* @return {boolean} True if all finished
*/
allRequestsFinished(): boolean {
let finished = true;
this.errors.forEach((error: number | null) => {
if (error == null) {
finished = false;
}
});
return finished;
}
/**
* Reloads the data, to be called using ref by parent components
*/
reload() {
this.fetchData();
}
render() {
const { state, props } = this;
if (state.loading) {
return <BasicLoadingScreen />;
}
if (this.getError() === ERROR_TYPE.SUCCESS) {
return props.renderFunction(this.fetchedData);
}
return this.getErrorRender();
}
}
export default AuthenticatedScreen;

View file

@ -25,6 +25,8 @@ import ConnectionManager from '../../../managers/ConnectionManager';
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../Dialogs/ErrorDialog';
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
import { ApiRejectType } from '../../../utils/WebData';
import { REQUEST_STATUS } from '../../../utils/Requests';
type PropsType = {
teams: Array<VoteTeamType>;
@ -36,7 +38,7 @@ type StateType = {
selectedTeam: string;
voteDialogVisible: boolean;
errorDialogVisible: boolean;
currentError: number;
currentError: ApiRejectType;
};
const styles = StyleSheet.create({
@ -58,7 +60,7 @@ export default class VoteSelect extends React.PureComponent<
selectedTeam: 'none',
voteDialogVisible: false,
errorDialogVisible: false,
currentError: 0,
currentError: { status: REQUEST_STATUS.SUCCESS },
};
}
@ -88,7 +90,7 @@ export default class VoteSelect extends React.PureComponent<
props.onVoteSuccess();
resolve();
})
.catch((error: number) => {
.catch((error: ApiRejectType) => {
this.onVoteDialogDismiss();
this.showErrorDialog(error);
resolve();
@ -96,7 +98,7 @@ export default class VoteSelect extends React.PureComponent<
});
};
showErrorDialog = (error: number): void =>
showErrorDialog = (error: ApiRejectType): void =>
this.setState({
errorDialogVisible: true,
currentError: error,
@ -156,7 +158,8 @@ export default class VoteSelect extends React.PureComponent<
<ErrorDialog
visible={state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss}
errorCode={state.currentError}
status={state.currentError.status}
code={state.currentError.code}
/>
</View>
);

View file

@ -19,60 +19,27 @@
import * as React from 'react';
import i18n from 'i18n-js';
import { ERROR_TYPE } from '../../utils/WebData';
import AlertDialog from './AlertDialog';
import {
API_REQUEST_CODES,
getErrorMessage,
REQUEST_STATUS,
} from '../../utils/Requests';
type PropsType = {
visible: boolean;
onDismiss: () => void;
errorCode: number;
status?: REQUEST_STATUS;
code?: API_REQUEST_CODES;
};
function ErrorDialog(props: PropsType) {
let title: string;
let message: string;
title = i18n.t('errors.title');
switch (props.errorCode) {
case ERROR_TYPE.BAD_CREDENTIALS:
message = i18n.t('errors.badCredentials');
break;
case ERROR_TYPE.BAD_TOKEN:
message = i18n.t('errors.badToken');
break;
case ERROR_TYPE.NO_CONSENT:
message = i18n.t('errors.noConsent');
break;
case ERROR_TYPE.TOKEN_SAVE:
message = i18n.t('errors.tokenSave');
break;
case ERROR_TYPE.TOKEN_RETRIEVE:
message = i18n.t('errors.unknown');
break;
case ERROR_TYPE.BAD_INPUT:
message = i18n.t('errors.badInput');
break;
case ERROR_TYPE.FORBIDDEN:
message = i18n.t('errors.forbidden');
break;
case ERROR_TYPE.CONNECTION_ERROR:
message = i18n.t('errors.connectionError');
break;
case ERROR_TYPE.SERVER_ERROR:
message = i18n.t('errors.serverError');
break;
default:
message = i18n.t('errors.unknown');
break;
}
message += `\n\nCode ${props.errorCode}`;
return (
<AlertDialog
visible={props.visible}
onDismiss={props.onDismiss}
title={title}
message={message}
title={i18n.t('errors.title')}
message={getErrorMessage(props).message}
/>
);
}

View file

@ -21,13 +21,16 @@ import * as React from 'react';
import { Button, Subheading, useTheme } from 'react-native-paper';
import { StyleSheet, View, ViewStyle } from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable';
import { REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
import {
API_REQUEST_CODES,
getErrorMessage,
REQUEST_STATUS,
} from '../../utils/Requests';
type Props = {
status?: REQUEST_STATUS;
code?: REQUEST_CODES;
code?: API_REQUEST_CODES;
icon?: string;
message?: string;
loading?: boolean;
@ -63,92 +66,9 @@ const styles = StyleSheet.create({
},
});
function getMessage(props: Props) {
let fullMessage = {
message: '',
icon: '',
};
if (props.code === undefined) {
switch (props.status) {
case REQUEST_STATUS.BAD_INPUT:
fullMessage.message = i18n.t('errors.badInput');
fullMessage.icon = 'alert-circle-outline';
break;
case REQUEST_STATUS.FORBIDDEN:
fullMessage.message = i18n.t('errors.forbidden');
fullMessage.icon = 'lock';
break;
case REQUEST_STATUS.CONNECTION_ERROR:
fullMessage.message = i18n.t('errors.connectionError');
fullMessage.icon = 'access-point-network-off';
break;
case REQUEST_STATUS.SERVER_ERROR:
fullMessage.message = i18n.t('errors.serverError');
fullMessage.icon = 'server-network-off';
break;
default:
fullMessage.message = i18n.t('errors.unknown');
fullMessage.icon = 'alert-circle-outline';
break;
}
} else {
switch (props.code) {
case REQUEST_CODES.BAD_CREDENTIALS:
fullMessage.message = i18n.t('errors.badCredentials');
fullMessage.icon = 'account-alert-outline';
break;
case REQUEST_CODES.BAD_TOKEN:
fullMessage.message = i18n.t('errors.badToken');
fullMessage.icon = 'account-alert-outline';
break;
case REQUEST_CODES.NO_CONSENT:
fullMessage.message = i18n.t('errors.noConsent');
fullMessage.icon = 'account-remove-outline';
break;
case REQUEST_CODES.TOKEN_SAVE:
fullMessage.message = i18n.t('errors.tokenSave');
fullMessage.icon = 'alert-circle-outline';
break;
case REQUEST_CODES.BAD_INPUT:
fullMessage.message = i18n.t('errors.badInput');
fullMessage.icon = 'alert-circle-outline';
break;
case REQUEST_CODES.FORBIDDEN:
fullMessage.message = i18n.t('errors.forbidden');
fullMessage.icon = 'lock';
break;
case REQUEST_CODES.CONNECTION_ERROR:
fullMessage.message = i18n.t('errors.connectionError');
fullMessage.icon = 'access-point-network-off';
break;
case REQUEST_CODES.SERVER_ERROR:
fullMessage.message = i18n.t('errors.serverError');
fullMessage.icon = 'server-network-off';
break;
default:
fullMessage.message = i18n.t('errors.unknown');
fullMessage.icon = 'alert-circle-outline';
break;
}
}
if (props.code !== undefined) {
fullMessage.message += `\n\nCode {${props.status}:${props.code}}`;
} else {
fullMessage.message += `\n\nCode {${props.status}}`;
}
if (props.message != null) {
fullMessage.message = props.message;
}
if (props.icon != null) {
fullMessage.icon = props.icon;
}
return fullMessage;
}
function ErrorView(props: Props) {
const theme = useTheme();
const fullMessage = getMessage(props);
const fullMessage = getErrorMessage(props, props.message, props.icon);
const { button } = props;
return (

View file

@ -4,7 +4,7 @@ import { useRequestLogic } from '../../utils/customHooks';
import { useFocusEffect } from '@react-navigation/native';
import BasicLoadingScreen from './BasicLoadingScreen';
import i18n from 'i18n-js';
import { REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
export type RequestScreenProps<T> = {
request: () => Promise<T>;
@ -14,7 +14,7 @@ export type RequestScreenProps<T> = {
lastRefreshDate: Date | undefined,
refreshData: (newRequest?: () => Promise<T>) => void,
status: REQUEST_STATUS,
code?: REQUEST_CODES
code?: API_REQUEST_CODES
) => React.ReactElement;
cache?: T;
onCacheUpdate?: (newCache: T) => void;

View file

@ -29,7 +29,7 @@ import ErrorView from './ErrorView';
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
import RequestScreen, { RequestScreenProps } from './RequestScreen';
import { CollapsibleComponentPropsType } from '../Collapsible/CollapsibleComponent';
import { REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
export type SectionListDataType<ItemT> = Array<{
title: string;
@ -61,7 +61,7 @@ type Props<ItemT, RawData> = Omit<
lastRefreshDate: Date | undefined,
refreshData: (newRequest?: () => Promise<RawData>) => void,
status: REQUEST_STATUS,
code?: REQUEST_CODES
code?: API_REQUEST_CODES
) => SectionListDataType<ItemT>;
renderListHeaderComponent?: (
data: RawData | undefined,
@ -69,7 +69,7 @@ type Props<ItemT, RawData> = Omit<
lastRefreshDate: Date | undefined,
refreshData: (newRequest?: () => Promise<RawData>) => void,
status: REQUEST_STATUS,
code?: REQUEST_CODES
code?: API_REQUEST_CODES
) => React.ComponentType<any> | React.ReactElement | null;
itemHeight?: number | null;
};
@ -103,7 +103,7 @@ function WebSectionList<ItemT, RawData>(props: Props<ItemT, RawData>) {
lastRefreshDate: Date | undefined,
refreshData: (newRequest?: () => Promise<RawData>) => void,
status: REQUEST_STATUS,
code?: REQUEST_CODES
code?: API_REQUEST_CODES
) => {
const { itemHeight } = props;
const dataset = props.createDataset(

View file

@ -43,11 +43,11 @@ import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityI
import { useTheme } from 'react-native-paper';
import { useCollapsibleHeader } from 'react-navigation-collapsible';
import MaterialHeaderButtons, { Item } from '../Overrides/CustomHeaderButton';
import { ERROR_TYPE } from '../../utils/WebData';
import ErrorView from './ErrorView';
import BasicLoadingScreen from './BasicLoadingScreen';
import { useFocusEffect, useNavigation } from '@react-navigation/core';
import { useCollapsible } from '../../utils/CollapsibleContext';
import { REQUEST_STATUS } from '../../utils/Requests';
type Props = {
url: string;
@ -259,7 +259,7 @@ function WebViewScreen(props: Props) {
renderLoading={getRenderLoading}
renderError={() => (
<ErrorView
status={ERROR_TYPE.CONNECTION_ERROR}
status={REQUEST_STATUS.CONNECTION_ERROR}
button={{
icon: 'refresh',
text: i18n.t('general.retry'),

View file

@ -33,7 +33,7 @@ const APP_IMAGES_ENDPOINT = STUDENT_SERVER + '~amicale_app/images/';
export default {
amicale: {
api: APP_ENDPOINT,
api: AMICALE_ENDPOINT,
resetPassword: AMICALE_SERVER + 'password/reset',
events: AMICALE_ENDPOINT + 'event/list',
},

View file

@ -18,8 +18,9 @@
*/
import * as Keychain from 'react-native-keychain';
import type { ApiDataLoginType } from '../utils/WebData';
import { apiRequest, ERROR_TYPE } from '../utils/WebData';
import { REQUEST_STATUS } from '../utils/Requests';
import type { ApiDataLoginType, ApiRejectType } from '../utils/WebData';
import { apiRequest } from '../utils/WebData';
/**
* champ: error
@ -141,7 +142,7 @@ export default class ConnectionManager {
*/
async connect(email: string, password: string): Promise<void> {
return new Promise(
(resolve: () => void, reject: (error: number) => void) => {
(resolve: () => void, reject: (error: ApiRejectType) => void) => {
const data = {
email,
password,
@ -150,13 +151,21 @@ export default class ConnectionManager {
.then((response: ApiDataLoginType) => {
if (response.token != null) {
this.saveLogin(email, response.token)
.then((): void => resolve())
.catch((): void => reject(ERROR_TYPE.TOKEN_SAVE));
.then(() => resolve())
.catch(() =>
reject({
status: REQUEST_STATUS.TOKEN_SAVE,
})
);
} else {
reject(ERROR_TYPE.SERVER_ERROR);
reject({
status: REQUEST_STATUS.SERVER_ERROR,
});
}
})
.catch((error: number): void => reject(error));
.catch((err) => {
reject(err);
});
}
);
}
@ -170,20 +179,25 @@ export default class ConnectionManager {
*/
async authenticatedRequest<T>(
path: string,
params: { [key: string]: any }
params?: { [key: string]: any }
): Promise<T> {
return new Promise(
(resolve: (response: T) => void, reject: (error: number) => void) => {
(
resolve: (response: T) => void,
reject: (error: ApiRejectType) => void
) => {
if (this.getToken() !== null) {
const data = {
...params,
token: this.getToken(),
};
apiRequest<T>(path, 'POST', data)
.then((response: T): void => resolve(response))
.catch((error: number): void => reject(error));
.then((response: T) => resolve(response))
.catch(reject);
} else {
reject(ERROR_TYPE.TOKEN_RETRIEVE);
reject({
status: REQUEST_STATUS.TOKEN_RETRIEVE,
});
}
}
);

View file

@ -29,13 +29,13 @@ import {
} from 'react-native-paper';
import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack';
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import CustomHTML from '../../../components/Overrides/CustomHTML';
import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar';
import type { ClubCategoryType, ClubType } from './ClubListScreen';
import { ERROR_TYPE } from '../../../utils/WebData';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import ImageGalleryButton from '../../../components/Media/ImageGalleryButton';
import RequestScreen from '../../../components/Screens/RequestScreen';
import ConnectionManager from '../../../managers/ConnectionManager';
type PropsType = {
navigation: StackNavigationProp<any>;
@ -49,6 +49,8 @@ type PropsType = {
theme: ReactNativePaper.Theme;
};
type ResponseType = ClubType;
const AMICALE_MAIL = 'clubs@amicale-insat.fr';
const styles = StyleSheet.create({
@ -88,7 +90,7 @@ const styles = StyleSheet.create({
* If called with clubId parameter, will fetch the information on the server
*/
class ClubDisplayScreen extends React.Component<PropsType> {
displayData: ClubType | null;
displayData: ClubType | undefined;
categories: Array<ClubCategoryType> | null;
@ -98,7 +100,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
constructor(props: PropsType) {
super(props);
this.displayData = null;
this.displayData = undefined;
this.categories = null;
this.clubId = props.route.params?.clubId ? props.route.params.clubId : 0;
this.shouldFetchData = true;
@ -236,9 +238,8 @@ class ClubDisplayScreen extends React.Component<PropsType> {
);
}
getScreen = (response: Array<ClubType | null>) => {
let data: ClubType | null = response[0];
if (data != null) {
getScreen = (data: ResponseType | undefined) => {
if (data) {
this.updateHeaderTitle(data);
return (
<CollapsibleScrollView style={styles.scroll} hasTab>
@ -264,7 +265,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
</CollapsibleScrollView>
);
}
return null;
return <View />;
};
/**
@ -278,31 +279,20 @@ class ClubDisplayScreen extends React.Component<PropsType> {
}
render() {
const { props } = this;
if (this.shouldFetchData) {
return (
<AuthenticatedScreen
navigation={props.navigation}
requests={[
{
link: 'clubs/info',
params: { id: this.clubId },
mandatory: true,
},
]}
renderFunction={this.getScreen}
errorViewOverride={[
{
errorCode: ERROR_TYPE.BAD_INPUT,
message: i18n.t('screens.clubs.invalidClub'),
icon: 'account-question',
showRetryButton: false,
},
]}
<RequestScreen
request={() =>
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
'clubs/info',
{ id: this.clubId }
)
}
render={this.getScreen}
/>
);
}
return this.getScreen([this.displayData]);
return this.getScreen(this.displayData);
}
}

View file

@ -22,7 +22,6 @@ import { Platform } from 'react-native';
import { Searchbar } from 'react-native-paper';
import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack';
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
import {
isItemInCategoryFilter,
@ -32,7 +31,8 @@ import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
import MaterialHeaderButtons, {
Item,
} from '../../../components/Overrides/CustomHeaderButton';
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
import ConnectionManager from '../../../managers/ConnectionManager';
import WebSectionList from '../../../components/Screens/WebSectionList';
export type ClubCategoryType = {
id: number;
@ -58,6 +58,11 @@ type StateType = {
currentSearchString: string;
};
type ResponseType = {
categories: Array<ClubCategoryType>;
clubs: Array<ClubType>;
};
const LIST_ITEM_HEIGHT = 96;
class ClubListScreen extends React.Component<PropsType, StateType> {
@ -146,30 +151,13 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
);
};
getScreen = (
data: Array<{
categories: Array<ClubCategoryType>;
clubs: Array<ClubType>;
} | null>
) => {
let categoryList: Array<ClubCategoryType> = [];
let clubList: Array<ClubType> = [];
if (data[0] != null) {
categoryList = data[0].categories;
clubList = data[0].clubs;
createDataset = (data: ResponseType | undefined) => {
if (data) {
this.categories = data?.categories;
return [{ title: '', data: data.clubs }];
} else {
return [{ title: '', data: [] }];
}
this.categories = categoryList;
return (
<CollapsibleFlatList
data={clubList}
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews
getItemLayout={this.itemLayout}
/>
);
};
/**
@ -177,15 +165,19 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
*
* @returns {*}
*/
getListHeader() {
getListHeader(data: ResponseType | undefined) {
const { state } = this;
return (
<ClubListHeader
categories={this.categories}
selectedCategories={state.currentlySelectedCategories}
onChipSelect={this.onChipSelect}
/>
);
if (data) {
return (
<ClubListHeader
categories={this.categories}
selectedCategories={state.currentlySelectedCategories}
onChipSelect={this.onChipSelect}
/>
);
} else {
return null;
}
}
/**
@ -223,15 +215,6 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
keyExtractor = (item: ClubType): string => item.id.toString();
itemLayout = (
_data: Array<ClubType> | null | undefined,
index: number
): { length: number; offset: number; index: number } => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
});
/**
* Updates the search string and category filter, saving them to the State.
*
@ -282,18 +265,20 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
}
render() {
const { props } = this;
return (
<AuthenticatedScreen
navigation={props.navigation}
requests={[
{
link: 'clubs/list',
params: {},
mandatory: true,
},
]}
renderFunction={this.getScreen}
<WebSectionList
request={() =>
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
'clubs/list'
)
}
createDataset={this.createDataset}
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
renderListHeaderComponent={(data) => this.getListHeader(data)}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews={true}
itemHeight={LIST_ITEM_HEIGHT}
/>
);
}

View file

@ -22,13 +22,14 @@ import { StyleSheet, View } from 'react-native';
import { Button } from 'react-native-paper';
import { StackNavigationProp } from '@react-navigation/stack';
import i18n from 'i18n-js';
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
import MascotPopup from '../../../components/Mascot/MascotPopup';
import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import AsyncStorageManager from '../../../managers/AsyncStorageManager';
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
import GENERAL_STYLES from '../../../constants/Styles';
import ConnectionManager from '../../../managers/ConnectionManager';
import { ApiRejectType } from '../../../utils/WebData';
import WebSectionList from '../../../components/Screens/WebSectionList';
type PropsType = {
navigation: StackNavigationProp<any>;
@ -52,6 +53,11 @@ export type RentedDeviceType = {
end: string;
};
type ResponseType = {
devices: Array<DeviceType>;
locations?: Array<RentedDeviceType>;
};
const LIST_ITEM_HEIGHT = 64;
const styles = StyleSheet.create({
@ -65,10 +71,6 @@ const styles = StyleSheet.create({
class EquipmentListScreen extends React.Component<PropsType, StateType> {
userRents: null | Array<RentedDeviceType>;
authRef: { current: null | AuthenticatedScreen<any> };
canRefresh: boolean;
constructor(props: PropsType) {
super(props);
this.userRents = null;
@ -77,22 +79,8 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
AsyncStorageManager.PREFERENCES.equipmentShowMascot.key
),
};
this.canRefresh = false;
this.authRef = React.createRef();
props.navigation.addListener('focus', this.onScreenFocus);
}
onScreenFocus = () => {
if (
this.canRefresh &&
this.authRef.current &&
this.authRef.current.reload
) {
this.authRef.current.reload();
}
this.canRefresh = true;
};
getRenderItem = ({ item }: { item: DeviceType }) => {
const { navigation } = this.props;
return (
@ -139,37 +127,17 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
keyExtractor = (item: DeviceType): string => item.id.toString();
/**
* Gets the main screen component with the fetched data
*
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (
data: Array<
| { devices: Array<DeviceType> }
| { locations: Array<RentedDeviceType> }
| null
>
) => {
const [allDevices, userRents] = data;
if (userRents) {
this.userRents = (userRents as {
locations: Array<RentedDeviceType>;
}).locations;
createDataset = (data: ResponseType | undefined) => {
if (data) {
const userRents = data.locations;
if (userRents) {
this.userRents = userRents;
}
return [{ title: '', data: data.devices }];
} else {
return [{ title: '', data: [] }];
}
return (
<CollapsibleFlatList
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
data={
allDevices
? (allDevices as { devices: Array<DeviceType> }).devices
: null
}
/>
);
};
showMascotDialog = () => {
@ -184,26 +152,46 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
this.setState({ mascotDialogVisible: false });
};
request = () => {
return new Promise(
(
resolve: (data: ResponseType) => void,
reject: (error: ApiRejectType) => void
) => {
ConnectionManager.getInstance()
.authenticatedRequest<{ devices: Array<DeviceType> }>('location/all')
.then((devicesData) => {
ConnectionManager.getInstance()
.authenticatedRequest<{
locations: Array<RentedDeviceType>;
}>('location/my')
.then((rentsData) => {
resolve({
devices: devicesData.devices,
locations: rentsData.locations,
});
})
.catch(() =>
resolve({
devices: devicesData.devices,
})
);
})
.catch(reject);
}
);
};
render() {
const { props, state } = this;
const { state } = this;
return (
<View style={GENERAL_STYLES.flex}>
<AuthenticatedScreen
navigation={props.navigation}
ref={this.authRef}
requests={[
{
link: 'location/all',
params: {},
mandatory: true,
},
{
link: 'location/my',
params: {},
mandatory: false,
},
]}
renderFunction={this.getScreen}
<WebSectionList
request={this.request}
createDataset={this.createDataset}
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
renderListHeaderComponent={() => this.getListHeader()}
/>
<MascotPopup
visible={state.mascotDialogVisible}

View file

@ -46,6 +46,8 @@ import ConnectionManager from '../../../managers/ConnectionManager';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import { MainStackParamsList } from '../../../navigation/MainNavigator';
import GENERAL_STYLES from '../../../constants/Styles';
import { ApiRejectType } from '../../../utils/WebData';
import { REQUEST_STATUS } from '../../../utils/Requests';
type EquipmentRentScreenNavigationProp = StackScreenProps<
MainStackParamsList,
@ -65,7 +67,7 @@ type StateType = {
dialogVisible: boolean;
errorDialogVisible: boolean;
markedDates: MarkedDatesObjectType;
currentError: number;
currentError: ApiRejectType;
};
const styles = StyleSheet.create({
@ -133,7 +135,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
dialogVisible: false,
errorDialogVisible: false,
markedDates: {},
currentError: 0,
currentError: { status: REQUEST_STATUS.SUCCESS },
};
this.resetSelection();
this.bookRef = React.createRef();
@ -231,7 +233,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
});
resolve();
})
.catch((error: number) => {
.catch((error: ApiRejectType) => {
this.onDialogDismiss();
this.showErrorDialog(error);
resolve();
@ -284,7 +286,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
}
};
showErrorDialog = (error: number) => {
showErrorDialog = (error: ApiRejectType) => {
this.setState({
errorDialogVisible: true,
currentError: error,
@ -460,7 +462,8 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
<ErrorDialog
visible={state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss}
errorCode={state.currentError}
status={state.currentError.status}
code={state.currentError.code}
/>
<Animatable.View
ref={this.bookRef}

View file

@ -38,6 +38,8 @@ import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrol
import { MainStackParamsList } from '../../navigation/MainNavigator';
import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls';
import { ApiRejectType } from '../../utils/WebData';
import { REQUEST_STATUS } from '../../utils/Requests';
type LoginScreenNavigationProp = StackScreenProps<MainStackParamsList, 'login'>;
@ -53,7 +55,7 @@ type StateType = {
isPasswordValidated: boolean;
loading: boolean;
dialogVisible: boolean;
dialogError: number;
dialogError: ApiRejectType;
mascotDialogVisible: boolean;
};
@ -108,14 +110,15 @@ class LoginScreen extends React.Component<Props, StateType> {
this.onInputChange(false, value);
};
props.navigation.addListener('focus', this.onScreenFocus);
// TODO remove
this.state = {
email: '',
password: '',
email: 'vergnet@etud.insa-toulouse.fr',
password: 'IGtt25ùj',
isEmailValidated: false,
isPasswordValidated: false,
loading: false,
dialogVisible: false,
dialogError: 0,
dialogError: { status: REQUEST_STATUS.SUCCESS },
mascotDialogVisible: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.loginShowMascot.key
),
@ -335,7 +338,9 @@ class LoginScreen extends React.Component<Props, StateType> {
*
* @param error The error given by the login request
*/
showErrorDialog = (error: number) => {
showErrorDialog = (error: ApiRejectType) => {
console.log(error);
this.setState({
dialogVisible: true,
dialogError: error,
@ -433,10 +438,10 @@ class LoginScreen extends React.Component<Props, StateType> {
end={{ x: 0.1, y: 1 }}
>
<KeyboardAvoidingView
behavior="height"
behavior={'height'}
contentContainerStyle={GENERAL_STYLES.flex}
style={GENERAL_STYLES.flex}
enabled
enabled={true}
keyboardVerticalOffset={100}
>
<CollapsibleScrollView headerColors={'transparent'}>
@ -445,7 +450,7 @@ class LoginScreen extends React.Component<Props, StateType> {
visible={mascotDialogVisible}
title={i18n.t('screens.login.mascotDialog.title')}
message={i18n.t('screens.login.mascotDialog.message')}
icon="help"
icon={'help'}
buttons={{
cancel: {
message: i18n.t('screens.login.mascotDialog.button'),
@ -458,7 +463,8 @@ class LoginScreen extends React.Component<Props, StateType> {
<ErrorDialog
visible={dialogVisible}
onDismiss={this.hideErrorDialog}
errorCode={dialogError}
status={dialogError.status}
code={dialogError.code}
/>
</CollapsibleScrollView>
</KeyboardAvoidingView>

View file

@ -30,7 +30,6 @@ import {
} from 'react-native-paper';
import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack';
import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
import LogoutDialog from '../../components/Amicale/LogoutDialog';
import MaterialHeaderButtons, {
Item,
@ -42,6 +41,8 @@ import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatLis
import type { ServiceItemType } from '../../managers/ServicesManager';
import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls';
import RequestScreen from '../../components/Screens/RequestScreen';
import ConnectionManager from '../../managers/ConnectionManager';
type PropsType = {
navigation: StackNavigationProp<any>;
@ -89,7 +90,7 @@ const styles = StyleSheet.create({
});
class ProfileScreen extends React.Component<PropsType, StateType> {
data: ProfileDataType | null;
data: ProfileDataType | undefined;
flatListData: Array<{ id: string }>;
@ -97,7 +98,7 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
constructor(props: PropsType) {
super(props);
this.data = null;
this.data = undefined;
this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }];
const services = new ServicesManager(props.navigation);
this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
@ -134,21 +135,25 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (data: Array<ProfileDataType | null>) => {
getScreen = (data: ProfileDataType | undefined) => {
const { dialogVisible } = this.state;
this.data = data[0];
return (
<View style={GENERAL_STYLES.flex}>
<CollapsibleFlatList
renderItem={this.getRenderItem}
data={this.flatListData}
/>
<LogoutDialog
visible={dialogVisible}
onDismiss={this.hideDisconnectDialog}
/>
</View>
);
if (data) {
this.data = data;
return (
<View style={GENERAL_STYLES.flex}>
<CollapsibleFlatList
renderItem={this.getRenderItem}
data={this.flatListData}
/>
<LogoutDialog
visible={dialogVisible}
onDismiss={this.hideDisconnectDialog}
/>
</View>
);
} else {
return <View />;
}
};
getRenderItem = ({ item }: { item: { id: string } }) => {
@ -482,18 +487,12 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
}
render() {
const { navigation } = this.props;
return (
<AuthenticatedScreen
navigation={navigation}
requests={[
{
link: 'user/profile',
params: {},
mandatory: true,
},
]}
renderFunction={this.getScreen}
<RequestScreen<ProfileDataType>
request={() =>
ConnectionManager.getInstance().authenticatedRequest('user/profile')
}
render={this.getScreen}
/>
);
}

View file

@ -18,11 +18,9 @@
*/
import * as React from 'react';
import { RefreshControl, StyleSheet, View } from 'react-native';
import { StackNavigationProp } from '@react-navigation/stack';
import { StyleSheet, View } from 'react-native';
import i18n from 'i18n-js';
import { Button } from 'react-native-paper';
import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
import { getTimeOnlyString, stringToDate } from '../../utils/Planning';
import VoteTease from '../../components/Amicale/Vote/VoteTease';
import VoteSelect from '../../components/Amicale/Vote/VoteSelect';
@ -32,8 +30,11 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import GENERAL_STYLES from '../../constants/Styles';
import ConnectionManager from '../../managers/ConnectionManager';
import WebSectionList, {
SectionListDataType,
} from '../../components/Screens/WebSectionList';
export type VoteTeamType = {
id: number;
@ -60,6 +61,11 @@ type VoteDatesObjectType = {
date_result_end: Date;
};
type ResponseType = {
teams?: TeamResponseType;
dates?: VoteDatesStringType;
};
// const FAKE_DATE = {
// "date_begin": "2020-08-19 15:50",
// "date_end": "2020-08-19 15:50",
@ -108,11 +114,7 @@ type VoteDatesObjectType = {
// ],
// };
const MIN_REFRESH_TIME = 5 * 1000;
type PropsType = {
navigation: StackNavigationProp<any>;
};
type PropsType = {};
type StateType = {
hasVoted: boolean;
@ -135,23 +137,21 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
hasVoted: boolean;
datesString: null | VoteDatesStringType;
datesString: undefined | VoteDatesStringType;
dates: null | VoteDatesObjectType;
dates: undefined | VoteDatesObjectType;
today: Date;
mainFlatListData: Array<{ key: string }>;
mainFlatListData: SectionListDataType<{ key: string }>;
lastRefresh: Date | null;
authRef: { current: null | AuthenticatedScreen<any> };
refreshData: () => void;
constructor(props: PropsType) {
super(props);
this.teams = [];
this.datesString = null;
this.dates = null;
this.datesString = undefined;
this.dates = undefined;
this.state = {
hasVoted: false,
mascotDialogVisible: AsyncStorageManager.getBool(
@ -160,9 +160,10 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
};
this.hasVoted = false;
this.today = new Date();
this.authRef = React.createRef();
this.lastRefresh = null;
this.mainFlatListData = [{ key: 'main' }, { key: 'info' }];
this.refreshData = () => undefined;
this.mainFlatListData = [
{ title: '', data: [{ key: 'main' }, { key: 'info' }] },
];
}
/**
@ -201,37 +202,32 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
return this.getContent();
};
getScreen = (data: Array<TeamResponseType | VoteDatesStringType | null>) => {
const { state } = this;
createDataset = (
data: ResponseType | undefined,
_loading: boolean,
_lastRefreshDate: Date | undefined,
refreshData: () => void
) => {
// data[0] = FAKE_TEAMS2;
// data[1] = FAKE_DATE;
this.lastRefresh = new Date();
this.refreshData = refreshData;
if (data) {
const { teams, dates } = data;
const teams = data[0] as TeamResponseType | null;
const dateStrings = data[1] as VoteDatesStringType | null;
if (dates && dates.date_begin == null) {
this.datesString = undefined;
} else {
this.datesString = dates;
}
if (dateStrings != null && dateStrings.date_begin == null) {
this.datesString = null;
} else {
this.datesString = dateStrings;
if (teams) {
this.teams = teams.teams;
this.hasVoted = teams.has_voted;
}
this.generateDateObject();
}
if (teams != null) {
this.teams = teams.teams;
this.hasVoted = teams.has_voted;
}
this.generateDateObject();
return (
<CollapsibleFlatList
data={this.mainFlatListData}
refreshControl={
<RefreshControl refreshing={false} onRefresh={this.reloadData} />
}
extraData={state.hasVoted.toString()}
renderItem={this.getMainRenderItem}
/>
);
return this.mainFlatListData;
};
getContent() {
@ -261,7 +257,7 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
<VoteSelect
teams={this.teams}
onVoteSuccess={this.onVoteSuccess}
onVoteError={this.reloadData}
onVoteError={this.refreshData}
/>
);
}
@ -327,23 +323,6 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
);
}
/**
* Reloads vote data if last refresh delta is smaller than the minimum refresh time
*/
reloadData = () => {
let canRefresh;
const { lastRefresh } = this;
if (lastRefresh != null) {
canRefresh =
new Date().getTime() - lastRefresh.getTime() > MIN_REFRESH_TIME;
} else {
canRefresh = true;
}
if (canRefresh && this.authRef.current != null) {
this.authRef.current.reload();
}
};
showMascotDialog = () => {
this.setState({ mascotDialogVisible: true });
};
@ -403,13 +382,36 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
date_result_end: dateResultEnd,
};
} else {
this.dates = null;
this.dates = undefined;
}
} else {
this.dates = null;
this.dates = undefined;
}
}
request = () => {
return new Promise((resolve: (data: ResponseType) => void) => {
ConnectionManager.getInstance()
.authenticatedRequest<VoteDatesStringType>('elections/dates')
.then((datesData) => {
ConnectionManager.getInstance()
.authenticatedRequest<TeamResponseType>('elections/teams')
.then((teamsData) => {
resolve({
dates: datesData,
teams: teamsData,
});
})
.catch(() =>
resolve({
dates: datesData,
})
);
})
.catch(() => resolve({}));
});
};
/**
* Renders the authenticated screen.
*
@ -418,25 +420,14 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
* @returns {*}
*/
render() {
const { props, state } = this;
const { state } = this;
return (
<View style={GENERAL_STYLES.flex}>
<AuthenticatedScreen<TeamResponseType | VoteDatesStringType>
navigation={props.navigation}
ref={this.authRef}
requests={[
{
link: 'elections/teams',
params: {},
mandatory: false,
},
{
link: 'elections/dates',
params: {},
mandatory: false,
},
]}
renderFunction={this.getScreen}
<WebSectionList
request={this.request}
createDataset={this.createDataset}
extraData={state.hasVoted.toString()}
renderItem={this.getMainRenderItem}
/>
<MascotPopup
visible={state.mascotDialogVisible}

View file

@ -1,17 +1,8 @@
import i18n from 'i18n-js';
import { ApiRejectType } from './WebData';
export enum REQUEST_STATUS {
SUCCESS = 200,
BAD_INPUT = 400,
FORBIDDEN = 403,
CONNECTION_ERROR = 404,
SERVER_ERROR = 500,
UNKNOWN = 999,
}
export enum REQUEST_CODES {
SUCCESS = 0,
BAD_CREDENTIALS = 1,
BAD_TOKEN = 2,
NO_CONSENT = 3,
TOKEN_SAVE = 4,
TOKEN_RETRIEVE = 5,
BAD_INPUT = 400,
@ -20,3 +11,92 @@ export enum REQUEST_CODES {
SERVER_ERROR = 500,
UNKNOWN = 999,
}
export enum API_REQUEST_CODES {
SUCCESS = 0,
BAD_CREDENTIALS = 1,
BAD_TOKEN = 2,
NO_CONSENT = 3,
BAD_INPUT = 400,
FORBIDDEN = 403,
UNKNOWN = 999,
}
export function getErrorMessage(
props: Partial<ApiRejectType>,
message?: string,
icon?: string
) {
let fullMessage = {
message: '',
icon: '',
};
if (props.code === undefined) {
switch (props.status) {
case REQUEST_STATUS.BAD_INPUT:
fullMessage.message = i18n.t('errors.badInput');
fullMessage.icon = 'alert-circle-outline';
break;
case REQUEST_STATUS.FORBIDDEN:
fullMessage.message = i18n.t('errors.forbidden');
fullMessage.icon = 'lock';
break;
case REQUEST_STATUS.CONNECTION_ERROR:
fullMessage.message = i18n.t('errors.connectionError');
fullMessage.icon = 'access-point-network-off';
break;
case REQUEST_STATUS.SERVER_ERROR:
fullMessage.message = i18n.t('errors.serverError');
fullMessage.icon = 'server-network-off';
break;
case REQUEST_STATUS.TOKEN_SAVE:
fullMessage.message = i18n.t('errors.tokenSave');
fullMessage.icon = 'alert-circle-outline';
break;
default:
fullMessage.message = i18n.t('errors.unknown');
fullMessage.icon = 'alert-circle-outline';
break;
}
} else {
switch (props.code) {
case API_REQUEST_CODES.BAD_CREDENTIALS:
fullMessage.message = i18n.t('errors.badCredentials');
fullMessage.icon = 'account-alert-outline';
break;
case API_REQUEST_CODES.BAD_TOKEN:
fullMessage.message = i18n.t('errors.badToken');
fullMessage.icon = 'account-alert-outline';
break;
case API_REQUEST_CODES.NO_CONSENT:
fullMessage.message = i18n.t('errors.noConsent');
fullMessage.icon = 'account-remove-outline';
break;
case API_REQUEST_CODES.BAD_INPUT:
fullMessage.message = i18n.t('errors.badInput');
fullMessage.icon = 'alert-circle-outline';
break;
case API_REQUEST_CODES.FORBIDDEN:
fullMessage.message = i18n.t('errors.forbidden');
fullMessage.icon = 'lock';
break;
default:
fullMessage.message = i18n.t('errors.unknown');
fullMessage.icon = 'alert-circle-outline';
break;
}
}
if (props.code !== undefined) {
fullMessage.message += `\n\nCode {${props.status}:${props.code}}`;
} else {
fullMessage.message += `\n\nCode {${props.status}}`;
}
if (message) {
fullMessage.message = message;
}
if (icon) {
fullMessage.icon = icon;
}
return fullMessage;
}

View file

@ -18,28 +18,35 @@
*/
import Urls from '../constants/Urls';
import { API_REQUEST_CODES, REQUEST_STATUS } from './Requests';
export const ERROR_TYPE = {
SUCCESS: 0,
BAD_CREDENTIALS: 1,
BAD_TOKEN: 2,
NO_CONSENT: 3,
TOKEN_SAVE: 4,
TOKEN_RETRIEVE: 5,
BAD_INPUT: 400,
FORBIDDEN: 403,
CONNECTION_ERROR: 404,
SERVER_ERROR: 500,
UNKNOWN: 999,
};
// export const ERROR_TYPE = {
// SUCCESS: 0,
// BAD_CREDENTIALS: 1,
// BAD_TOKEN: 2,
// NO_CONSENT: 3,
// TOKEN_SAVE: 4,
// TOKEN_RETRIEVE: 5,
// BAD_INPUT: 400,
// FORBIDDEN: 403,
// CONNECTION_ERROR: 404,
// SERVER_ERROR: 500,
// UNKNOWN: 999,
// };
export type ApiDataLoginType = {
token: string;
};
type ApiResponseType<T> = {
error: number;
data: T;
status: REQUEST_STATUS;
error?: API_REQUEST_CODES;
data?: T;
};
export type ApiRejectType = {
status: REQUEST_STATUS;
code?: API_REQUEST_CODES;
};
/**
@ -76,11 +83,13 @@ export async function apiRequest<T>(
params?: object
): Promise<T> {
return new Promise(
(resolve: (data: T) => void, reject: (error: number) => void) => {
(resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => {
let requestParams = {};
if (params != null) {
requestParams = { ...params };
}
console.log(Urls.amicale.api + path);
fetch(Urls.amicale.api + path, {
method,
headers: new Headers({
@ -89,22 +98,39 @@ export async function apiRequest<T>(
}),
body: JSON.stringify(requestParams),
})
.then(
async (response: Response): Promise<ApiResponseType<T>> =>
response.json()
)
.then((response: ApiResponseType<T>) => {
if (isApiResponseValid(response)) {
if (response.error === ERROR_TYPE.SUCCESS) {
resolve(response.data);
} else {
reject(response.error);
}
.then((response: Response) => {
const status = response.status;
if (status === REQUEST_STATUS.SUCCESS) {
return response.json().then(
(data): ApiResponseType<T> => {
return { status: status, error: data.error, data: data.data };
}
);
} else {
reject(ERROR_TYPE.SERVER_ERROR);
return { status: status };
}
})
.catch((): void => reject(ERROR_TYPE.CONNECTION_ERROR));
.then((response: ApiResponseType<T>) => {
if (isApiResponseValid(response) && response.data) {
if (response.error === API_REQUEST_CODES.SUCCESS) {
resolve(response.data);
} else {
reject({
status: REQUEST_STATUS.SUCCESS,
code: response.error,
});
}
} else {
reject({
status: response.status,
});
}
})
.catch(() => {
reject({
status: REQUEST_STATUS.CONNECTION_ERROR,
});
});
}
);
}

View file

@ -19,6 +19,7 @@
import { DependencyList, useEffect, useRef, useState } from 'react';
import { REQUEST_STATUS } from './Requests';
import { ApiRejectType } from './WebData';
export function useMountEffect(func: () => void) {
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -97,14 +98,24 @@ export function useRequestLogic<T>(
onCacheUpdate(requestResponse);
}
})
.catch(() => {
setResponse((prevState) => ({
loading: false,
lastRefreshDate: prevState.lastRefreshDate,
status: REQUEST_STATUS.CONNECTION_ERROR,
code: undefined,
data: prevState.data,
}));
.catch((error: ApiRejectType | undefined) => {
if (!error) {
setResponse((prevState) => ({
loading: false,
lastRefreshDate: prevState.lastRefreshDate,
status: REQUEST_STATUS.CONNECTION_ERROR,
code: undefined,
data: prevState.data,
}));
} else {
setResponse((prevState) => ({
loading: false,
lastRefreshDate: prevState.lastRefreshDate,
status: error.status,
code: error.code,
data: prevState.data,
}));
}
});
}
};