import { Preferences } from '@capacitor/preferences' 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 { apiClient } from '../../utils/ApiClient' const CONNECTION_TIMEOUT_MS = 8000 const LoginSettings = () => { const Navigate = useNavigate() const { refetch: refetchResource } = useResource() const [serverURL, setServerURL] = React.useState('') const [status, setStatus] = React.useState('idle') // 'idle' | 'testing' | 'success' | 'error' const [errorMessage, setErrorMessage] = React.useState('') React.useEffect(() => { Preferences.get({ key: 'customServerUrl' }).then(result => { setServerURL(result.value || API_URL) }) }, []) const isValidURL = url => { return /^(http|https):\/\/[^ "]+$/.test(url.trim()) } const testConnection = async url => { const testURL = url.replace(/\/+$/, '') + '/api/v1/resource' const controller = new AbortController() const timeoutId = setTimeout( () => controller.abort(), CONNECTION_TIMEOUT_MS, ) try { 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 } } if (response.status === 503) { return { ok: false, message: 'Server is starting up or temporarily unavailable (503). Try again in a moment.', } } 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. The host may be unreachable or behind a firewall — check the IP/hostname and network.`, } } // Try no-cors to distinguish CORS misconfiguration from server being down const noCorsController = new AbortController() const noCorsTimeout = setTimeout(() => noCorsController.abort(), 3000) try { const probeStart = Date.now() const probe = await fetch(testURL, { method: 'GET', mode: 'no-cors', signal: noCorsController.signal, }) clearTimeout(noCorsTimeout) if (probe.type === 'opaque') { // Server responded but CORS headers blocked the real request return { ok: false, message: 'Server is reachable but blocked the request (CORS). Ensure your Donetick server allows requests from this origin, or check the server CORS config.', } } // Opaque is the only expected type for no-cors success; anything else is odd void probeStart } catch (probeErr) { clearTimeout(noCorsTimeout) if (probeErr.name !== 'AbortError') { // Both normal and no-cors fetch threw immediately → port refused const msg = probeErr.message?.toLowerCase() ?? '' if ( msg.includes('getaddrinfo') || msg.includes('name not resolved') || msg.includes('err_name') ) { return { ok: false, message: 'Hostname could not be resolved. Check the URL for typos or verify DNS.', } } return { ok: false, message: 'Connection refused. The port may be wrong or nothing is listening — verify the URL and port (default Donetick port is 2021).', } } // no-cors also timed out → server/host truly unreachable return { ok: false, message: 'Unable to reach the server. Check the URL, port, and network connection.', } } // Fallback (should rarely hit) 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 ( Done tick Server URL ) : status === 'error' ? ( ) : null } /> Change the server URL to connect to a different server, such as your own self-hosted Donetick server. 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... )} ) } export default LoginSettings