From e85220d727e28855f5cd54bf52bea96b34655780 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 30 Jun 2026 01:17:52 -0400 Subject: [PATCH] Fix: Changing URL for backend requiring restart. Fix: Add better feedback so user know if the url invalid or unreachable --- src/views/Authorization/LoginSettings.jsx | 242 +++++++++++++++------- 1 file changed, 172 insertions(+), 70 deletions(-) diff --git a/src/views/Authorization/LoginSettings.jsx b/src/views/Authorization/LoginSettings.jsx index 1a85de3..e8fe85e 100644 --- a/src/views/Authorization/LoginSettings.jsx +++ b/src/views/Authorization/LoginSettings.jsx @@ -1,17 +1,32 @@ import { Preferences } from '@capacitor/preferences' -import { Box, Button, Container, Input, Sheet, Typography } from '@mui/joy' +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' +import WifiIcon from '@mui/icons-material/Wifi' +import { + Alert, + Box, + Button, + CircularProgress, + Container, + Input, + Sheet, + Typography, +} from '@mui/joy' import React from 'react' import { useNavigate } from 'react-router-dom' import { API_URL } from '../../Config' import Logo from '../../Logo' import { useResource } from '../../queries/ResourceQueries' -import { useNotification } from '../../service/NotificationProvider' import { apiClient } from '../../utils/ApiClient' + +const CONNECTION_TIMEOUT_MS = 8000 + const LoginSettings = () => { const Navigate = useNavigate() const { refetch: refetchResource } = useResource() const [serverURL, setServerURL] = React.useState('') - const { showError } = useNotification() + const [status, setStatus] = React.useState('idle') // 'idle' | 'testing' | 'success' | 'error' + const [errorMessage, setErrorMessage] = React.useState('') React.useEffect(() => { Preferences.get({ key: 'customServerUrl' }).then(result => { @@ -19,10 +34,95 @@ const LoginSettings = () => { }) }, []) - const isValidServerURL = () => { - return serverURL.match(/^(http|https):\/\/[^ "]+$/) + const isValidURL = url => { + return /^(http|https):\/\/[^ "]+$/.test(url.trim()) } + const testConnection = async url => { + const controller = new AbortController() + const timeoutId = setTimeout( + () => controller.abort(), + CONNECTION_TIMEOUT_MS, + ) + try { + const testURL = url.replace(/\/+$/, '') + '/api/v1/resource' + const response = await fetch(testURL, { + method: 'GET', + signal: controller.signal, + }) + clearTimeout(timeoutId) + // Any HTTP response (even 401/404) means the server is reachable + if (response.status < 500) { + return { ok: true } + } + return { + ok: false, + message: `Server responded with error ${response.status}. Please check your Donetick server.`, + } + } catch (err) { + clearTimeout(timeoutId) + if (err.name === 'AbortError') { + return { + ok: false, + message: `Connection timed out after ${CONNECTION_TIMEOUT_MS / 1000}s. Check the URL and ensure the server is running.`, + } + } + return { + ok: false, + message: + 'Unable to reach the server. Check the URL, port, and network connection.', + } + } + } + + const handleSave = async () => { + const trimmedURL = serverURL.trim() + + if (trimmedURL === '') { + await Preferences.set({ key: 'customServerUrl', value: API_URL }) + Navigate('/login') + return + } + + if (!isValidURL(trimmedURL)) { + setStatus('error') + setErrorMessage( + 'Invalid URL format. Include the protocol (http:// or https://) and port if needed.', + ) + return + } + + setStatus('testing') + setErrorMessage('') + + const result = await testConnection(trimmedURL) + + if (!result.ok) { + setStatus('error') + setErrorMessage(result.message) + return + } + + await Preferences.set({ key: 'customServerUrl', value: trimmedURL }) + await apiClient.init(true) + refetchResource() + setStatus('success') + + setTimeout(() => { + Navigate('/login') + }, 1200) + } + + const handleURLChange = e => { + setServerURL(e.target.value) + if (status !== 'idle') { + setStatus('idle') + setErrorMessage('') + } + } + + const isTesting = status === 'testing' + return ( { sx={{ mt: 1, width: '100%', - display: 'flex', flexDirection: 'column', alignItems: 'center', @@ -51,13 +150,7 @@ const LoginSettings = () => { Done - - tick - + tick @@ -71,9 +164,22 @@ const LoginSettings = () => { name='serverURL' autoFocus value={serverURL} - onChange={e => { - setServerURL(e.target.value) - }} + onChange={handleURLChange} + disabled={isTesting} + color={ + status === 'success' + ? 'success' + : status === 'error' + ? 'danger' + : 'neutral' + } + endDecorator={ + status === 'success' ? ( + + ) : status === 'error' ? ( + + ) : null + } /> @@ -81,72 +187,68 @@ const LoginSettings = () => { own self-hosted Donetick server. - Please ensure to include the protocol (http:// or https://) and the - port number if necessary (default Donetick port is 2021). + Include the protocol (http:// or https://) and port if necessary + (default Donetick port is 2021). + + {status === 'error' && ( + } + sx={{ mt: 2, width: '100%' }} + > + {errorMessage} + + )} + + {status === 'success' && ( + } + sx={{ mt: 2, width: '100%' }} + > + Connected! Redirecting to login... + + )} + + {status === 'testing' && ( + } + sx={{ mt: 2, width: '100%' }} + > + Testing connection to server... + + )} +