Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 43 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { StrictMode, Suspense } from 'react';
import React, { StrictMode, Suspense, useState, useEffect } from 'react';
import ReactDOM from 'react-dom/client';
import Main from './basic/Main';
import { CssBaseline } from '@mui/material';
import { CssBaseline, Snackbar, Alert } from '@mui/material';
import HostAlert from './components/HostAlert';

import { I18nextProvider } from 'react-i18next';
Expand All @@ -15,6 +15,31 @@ import { FederationContextProvider } from './contexts/FederationContext';

const App = (): React.JSX.Element => {
const [client] = window.RobosatsSettings.split('-');

const [errorOpen, setErrorOpen] = useState(false);
const [errorMessage, setErrorMessage] = useState('');

useEffect(() => {
const handleApiError = (event: Event) => {
const customEvent = event as CustomEvent;
setErrorMessage(customEvent.detail);
setErrorOpen(true);
};

window.addEventListener('ROBOSATS_API_ERROR', handleApiError);

return () => {
window.removeEventListener('ROBOSATS_API_ERROR', handleApiError);
};
}, []);

const handleClose = (event?: React.SyntheticEvent | Event, reason?: string) => {
if (reason === 'clickaway') {
return;
}
setErrorOpen(false);
};

return (
<StrictMode>
<ErrorBoundary>
Expand All @@ -26,6 +51,22 @@ const App = (): React.JSX.Element => {
<CssBaseline />
{client !== 'mobile' && <HostAlert />}
<Main />

<Snackbar
open={errorOpen}
autoHideDuration={6000}
onClose={handleClose}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert
onClose={handleClose}
severity='error'
sx={{ width: '100%' }}
variant='filled'
>
{errorMessage}
</Alert>
</Snackbar>
</GarageContextProvider>
</FederationContextProvider>
</AppContextProvider>
Expand All @@ -37,7 +78,6 @@ const App = (): React.JSX.Element => {
};

const loadApp = (): void => {
// waits until the environment is ready for the Android WebView app
if (systemClient.loading) {
setTimeout(loadApp, 200);
} else {
Expand Down
53 changes: 32 additions & 21 deletions frontend/src/services/api/ApiAndroidClient/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { type ApiClient, type Auth } from '..';
import { v4 as uuidv4 } from 'uuid';

const dispatchError = (message: string) => {
if (typeof window !== 'undefined') {
const event = new CustomEvent('ROBOSATS_API_ERROR', { detail: message });
window.dispatchEvent(event);
}
};

class ApiAndroidClient implements ApiClient {
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
let headers = {
Expand Down Expand Up @@ -30,6 +37,28 @@ class ApiAndroidClient implements ApiClient {
return JSON.parse(response).json;
};

private async request(
method: 'GET' | 'POST' | 'DELETE',
baseUrl: string,
path: string,
headers: string,
body: string = '',
): Promise<object> {
try {
const result = await new Promise<string>((resolve, reject) => {
const uuid: string = uuidv4();
window.AndroidAppRobosats?.sendRequest(uuid, method, baseUrl + path, headers, body);
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
});

return this.parseResponse(result);
} catch (error) {
console.error('API Error:', error);
dispatchError('Coordinator unreachable! Please check your connection.');
throw error;
}
}

public put: (baseUrl: string, path: string, body: object) => Promise<object | undefined> = async (
_baseUrl,
_path,
Expand All @@ -44,13 +73,7 @@ class ApiAndroidClient implements ApiClient {
async (baseUrl, path, auth) => {
const jsonHeaders = JSON.stringify(this.getHeaders(auth));

const result = await new Promise<string>((resolve, reject) => {
const uuid: string = uuidv4();
window.AndroidAppRobosats?.sendRequest(uuid, 'DELETE', baseUrl + path, jsonHeaders, '');
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
});

return this.parseResponse(result);
return await this.request('DELETE', baseUrl, path, jsonHeaders);
};

public post: (
Expand All @@ -62,13 +85,7 @@ class ApiAndroidClient implements ApiClient {
const jsonHeaders = JSON.stringify(this.getHeaders(auth));
const jsonBody = JSON.stringify(body);

const result = await new Promise<string>((resolve, reject) => {
const uuid: string = uuidv4();
window.AndroidAppRobosats?.sendRequest(uuid, 'POST', baseUrl + path, jsonHeaders, jsonBody);
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
});

return this.parseResponse(result);
return await this.request('POST', baseUrl, path, jsonHeaders, jsonBody);
};

public get: (baseUrl: string, path: string, auth?: Auth) => Promise<object | undefined> = async (
Expand All @@ -78,13 +95,7 @@ class ApiAndroidClient implements ApiClient {
) => {
const jsonHeaders = JSON.stringify(this.getHeaders(auth));

const result = await new Promise<string>((resolve, reject) => {
const uuid: string = uuidv4();
window.AndroidAppRobosats?.sendRequest(uuid, 'GET', baseUrl + path, jsonHeaders, '');
window.AndroidRobosats?.storePromise(uuid, resolve, reject);
});

return this.parseResponse(result);
return await this.request('GET', baseUrl, path, jsonHeaders);
};
}

Expand Down
40 changes: 27 additions & 13 deletions frontend/src/services/api/ApiWebClient/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { type ApiClient, type Auth } from '..';

const dispatchError = (message: string) => {
if (typeof window !== 'undefined') {
const event = new CustomEvent('ROBOSATS_API_ERROR', { detail: message });
window.dispatchEvent(event);
}
};

class ApiWebClient implements ApiClient {
private readonly getHeaders: (auth?: Auth) => HeadersInit = (auth) => {
let headers = {
Expand All @@ -25,17 +32,30 @@ class ApiWebClient implements ApiClient {
return headers;
};

private async request(url: string, options: RequestInit): Promise<object> {
try {
const response = await fetch(url, options);

if (!response.ok) {
dispatchError(`Request failed: ${response.status} ${response.statusText}`);
}

return await response.json();
} catch (error) {
console.error('API Error:', error);
dispatchError('Coordinator unreachable! Please check your connection.');
throw error;
}
}

public post: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object> =
async (baseUrl, path, body, auth) => {
const requestOptions = {
method: 'POST',
headers: this.getHeaders(auth),
body: JSON.stringify(body),
};

return await fetch(baseUrl + path, requestOptions).then(
async (response) => await response.json(),
);
return await this.request(baseUrl + path, requestOptions);
};

public put: (baseUrl: string, path: string, body: object, auth?: Auth) => Promise<object> =
Expand All @@ -45,9 +65,7 @@ class ApiWebClient implements ApiClient {
headers: this.getHeaders(auth),
body: JSON.stringify(body),
};
return await fetch(baseUrl + path, requestOptions).then(
async (response) => await response.json(),
);
return await this.request(baseUrl + path, requestOptions);
};

public delete: (baseUrl: string, path: string, auth?: Auth) => Promise<object> = async (
Expand All @@ -59,19 +77,15 @@ class ApiWebClient implements ApiClient {
method: 'DELETE',
headers: this.getHeaders(auth),
};
return await fetch(baseUrl + path, requestOptions).then(
async (response) => await response.json(),
);
return await this.request(baseUrl + path, requestOptions);
};

public get: (baseUrl: string, path: string, auth?: Auth) => Promise<object> = async (
baseUrl,
path,
auth,
) => {
return await fetch(baseUrl + path, { headers: this.getHeaders(auth) }).then(
async (response) => await response.json(),
);
return await this.request(baseUrl + path, { headers: this.getHeaders(auth) });
};
}

Expand Down
Loading