Fix: Changing URL for backend requiring restart.
Fix: Add better feedback so user know if the url invalid or unreachable
This commit is contained in:
@@ -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 (
|
||||
<Container component='main' maxWidth='xs'>
|
||||
<Box
|
||||
@@ -38,7 +138,6 @@ const LoginSettings = () => {
|
||||
sx={{
|
||||
mt: 1,
|
||||
width: '100%',
|
||||
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
@@ -51,13 +150,7 @@ const LoginSettings = () => {
|
||||
|
||||
<Typography level='h2'>
|
||||
Done
|
||||
<span
|
||||
style={{
|
||||
color: '#06b6d4',
|
||||
}}
|
||||
>
|
||||
tick
|
||||
</span>
|
||||
<span style={{ color: '#06b6d4' }}>tick</span>
|
||||
</Typography>
|
||||
|
||||
<Typography level='body2' alignSelf={'start'} mt={4}>
|
||||
@@ -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' ? (
|
||||
<CheckCircleOutlineIcon color='success' fontSize='small' />
|
||||
) : status === 'error' ? (
|
||||
<ErrorOutlineIcon color='error' fontSize='small' />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
|
||||
<Typography mt={1} level='body-xs'>
|
||||
@@ -81,72 +187,68 @@ const LoginSettings = () => {
|
||||
own self-hosted Donetick server.
|
||||
</Typography>
|
||||
<Typography mt={1} level='body-xs'>
|
||||
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).
|
||||
</Typography>
|
||||
|
||||
{status === 'error' && (
|
||||
<Alert
|
||||
color='danger'
|
||||
variant='soft'
|
||||
startDecorator={<ErrorOutlineIcon />}
|
||||
sx={{ mt: 2, width: '100%' }}
|
||||
>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{status === 'success' && (
|
||||
<Alert
|
||||
color='success'
|
||||
variant='soft'
|
||||
startDecorator={<CheckCircleOutlineIcon />}
|
||||
sx={{ mt: 2, width: '100%' }}
|
||||
>
|
||||
Connected! Redirecting to login...
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{status === 'testing' && (
|
||||
<Alert
|
||||
color='neutral'
|
||||
variant='soft'
|
||||
startDecorator={<WifiIcon />}
|
||||
sx={{ mt: 2, width: '100%' }}
|
||||
>
|
||||
Testing connection to server...
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
variant='solid'
|
||||
sx={{
|
||||
width: '100%',
|
||||
mt: 3,
|
||||
mb: 2,
|
||||
border: 'moccasin',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (serverURL === '') {
|
||||
Preferences.set({
|
||||
key: 'customServerUrl',
|
||||
value: API_URL,
|
||||
}).then(() => {
|
||||
Navigate('/login')
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!isValidServerURL()) {
|
||||
showError({
|
||||
title: 'Invalid Server URL',
|
||||
message:
|
||||
'Please enter a valid server URL with protocol (http:// or https://)',
|
||||
})
|
||||
return
|
||||
}
|
||||
Preferences.set({
|
||||
key: 'customServerUrl',
|
||||
value: serverURL,
|
||||
}).then(async () => {
|
||||
// apiClient.customServerURL = serverURL + '/api/v1's
|
||||
// Force re-initialization to reload from Preferences
|
||||
await apiClient.init(true)
|
||||
// refetch resource queries to update the API URL
|
||||
refetchResource()
|
||||
Navigate('/login')
|
||||
})
|
||||
}}
|
||||
disabled={isTesting || status === 'success'}
|
||||
sx={{ width: '100%', mt: 2, mb: 2, borderRadius: '8px' }}
|
||||
onClick={handleSave}
|
||||
startDecorator={
|
||||
isTesting ? <CircularProgress size='sm' /> : undefined
|
||||
}
|
||||
>
|
||||
Save
|
||||
{isTesting ? 'Testing...' : 'Save & Connect'}
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
size='lg'
|
||||
variant='soft'
|
||||
color='danger'
|
||||
sx={{
|
||||
width: '100%',
|
||||
|
||||
mb: 2,
|
||||
border: 'moccasin',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
onClick={() => {
|
||||
Preferences.set({ key: 'customServerUrl', value: API_URL }).then(
|
||||
() => {
|
||||
refetchResource()
|
||||
Navigate('/login')
|
||||
},
|
||||
)
|
||||
disabled={isTesting}
|
||||
sx={{ width: '100%', mb: 2, borderRadius: '8px' }}
|
||||
onClick={async () => {
|
||||
await Preferences.set({ key: 'customServerUrl', value: API_URL })
|
||||
await apiClient.init(true)
|
||||
refetchResource()
|
||||
Navigate('/login')
|
||||
}}
|
||||
>
|
||||
Cancel and Reset
|
||||
|
||||
Reference in New Issue
Block a user