Merge branch 'dev'
This commit is contained in:
@@ -19,6 +19,14 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deep link intent filter for OAuth callback -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="donetick" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
|
||||
@@ -42,10 +42,16 @@
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string></string>
|
||||
<string>com.donetick.app</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>donetick</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
|
||||
@@ -1,10 +1,44 @@
|
||||
import { App as mobileApp } from '@capacitor/app'
|
||||
import { Browser } from '@capacitor/browser'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Device } from '@capacitor/device'
|
||||
import { LocalNotifications } from '@capacitor/local-notifications'
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
import { PushNotifications } from '@capacitor/push-notifications'
|
||||
import { RegisterDeviceToken } from './utils/Fetcher'
|
||||
|
||||
// OAuth callback handler for deep links
|
||||
const handleOAuthDeepLink = async url => {
|
||||
console.log('OAuth deep link received:', url)
|
||||
|
||||
try {
|
||||
// Parse the URL to extract code and state
|
||||
const urlObj = new URL(url)
|
||||
const code = urlObj.searchParams.get('code')
|
||||
const state = urlObj.searchParams.get('state')
|
||||
|
||||
if (code && state) {
|
||||
// Store the OAuth params for the app to pick up
|
||||
await Preferences.set({
|
||||
key: 'oauth_callback',
|
||||
value: JSON.stringify({ code, state, timestamp: Date.now() }),
|
||||
})
|
||||
|
||||
// Close the browser if it's still open
|
||||
try {
|
||||
await Browser.close()
|
||||
} catch (e) {
|
||||
// Browser might already be closed
|
||||
}
|
||||
|
||||
// Navigate to the OAuth handler page
|
||||
window.location.href = `/auth/oauth2?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling OAuth deep link:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const localNotificationListenerRegistration = () => {
|
||||
LocalNotifications.addListener('localNotificationReceived', notification => {
|
||||
console.log('Notification received', notification)
|
||||
@@ -180,6 +214,17 @@ const registerCapacitorListeners = () => {
|
||||
return
|
||||
}
|
||||
localNotificationListenerRegistration()
|
||||
|
||||
// Register deep link handler for OAuth and other deep links
|
||||
mobileApp.addListener('appUrlOpen', event => {
|
||||
console.log('App URL opened:', event.url)
|
||||
|
||||
// Handle OAuth callback
|
||||
if (event.url.startsWith('donetick://auth/')) {
|
||||
handleOAuthDeepLink(event.url)
|
||||
}
|
||||
})
|
||||
|
||||
mobileApp.addListener('backButton', ({ canGoBack }) => {
|
||||
if (canGoBack) {
|
||||
window.history.back()
|
||||
|
||||
@@ -10,7 +10,6 @@ import Settings from '@/views/Settings/Settings'
|
||||
import SettingsOverview from '@/views/Settings/SettingsOverview'
|
||||
import SettingsRoutes from '@/views/Settings/SettingsRoutes'
|
||||
import ThemeSettings from '@/views/Settings/ThemeSettings'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
||||
import AuthenticationLoading from '../views/Authorization/Authenticating'
|
||||
import ForgotPasswordView from '../views/Authorization/ForgotPasswordView'
|
||||
@@ -46,8 +45,9 @@ import UserActivities from '../views/User/UserActivities'
|
||||
import UserPoints from '../views/User/UserPoints'
|
||||
const getMainRoute = () => {
|
||||
if (
|
||||
import.meta.env.VITE_IS_LANDING_DEFAULT === 'true' &&
|
||||
!Capacitor.isNativePlatform()
|
||||
// if domain is www.donetick.com or donetick.com then show landing page:
|
||||
window.location.hostname === 'www.donetick.com' ||
|
||||
window.location.hostname === 'donetick.com'
|
||||
) {
|
||||
return <Landing />
|
||||
}
|
||||
@@ -183,7 +183,7 @@ const Router = createBrowserRouter([
|
||||
element: <AuthenticationLoading />,
|
||||
},
|
||||
{
|
||||
path: '/landing',
|
||||
path: '/welcome',
|
||||
element: <Landing />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Box, Button, CircularProgress, Container, Typography } from '@mui/joy'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Logo from '../../Logo'
|
||||
import { apiClient } from '../../utils/ApiClient'
|
||||
|
||||
import Cookies from 'js-cookie'
|
||||
import { useRef } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { apiClient } from '../../utils/ApiClient'
|
||||
import { GetUserProfile } from '../../utils/Fetcher'
|
||||
|
||||
const AuthenticationLoading = () => {
|
||||
@@ -58,8 +58,9 @@ const AuthenticationLoading = () => {
|
||||
}
|
||||
|
||||
if (code) {
|
||||
const baseURL = apiClient.baseURL
|
||||
fetch(`${baseURL}/auth/${provider}/callback`, {
|
||||
const baseURL = apiClient.getApiURL()
|
||||
|
||||
fetch(`${baseURL}/auth/oauth2/callback`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -67,6 +68,7 @@ const AuthenticationLoading = () => {
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
state: returnedState,
|
||||
redirect_uri: `${window.location.origin}/auth/oauth2`,
|
||||
}),
|
||||
}).then(response => {
|
||||
if (response.status === 200) {
|
||||
|
||||
@@ -182,11 +182,19 @@ const LoginView = () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.post(`/auth/${provider}/callback`, {
|
||||
provider: provider,
|
||||
token: getAccessToken(data),
|
||||
data: data,
|
||||
})
|
||||
const response = await apiClient.post(
|
||||
`/auth/${provider}/callback`,
|
||||
JSON.stringify({
|
||||
provider: provider,
|
||||
token: getAccessToken(data),
|
||||
data: data,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (response.ok) {
|
||||
const responseData = await response.json()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
Archive,
|
||||
CalendarMonth,
|
||||
CancelScheduleSend,
|
||||
Check,
|
||||
Checklist,
|
||||
CloseFullscreen,
|
||||
@@ -35,7 +34,6 @@ import {
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
Sheet,
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { Divider } from '@mui/material'
|
||||
@@ -54,6 +52,7 @@ import {
|
||||
useStartChore,
|
||||
} from '../../queries/TimeQueries'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { ChoreStatus, notInCompletionWindow } from '../../utils/Chores.jsx'
|
||||
import { getTextColorFromBackgroundColor } from '../../utils/Colors.jsx'
|
||||
import {
|
||||
@@ -63,6 +62,7 @@ import {
|
||||
RejectChore,
|
||||
SkipChore,
|
||||
UnArchiveChore,
|
||||
UndoChoreAction,
|
||||
UpdateChorePriority,
|
||||
} from '../../utils/Fetcher'
|
||||
import Priorities from '../../utils/Priorities'
|
||||
@@ -82,12 +82,11 @@ const ChoreView = () => {
|
||||
const { choreId } = useParams()
|
||||
const [note, setNote] = useState(null)
|
||||
const queryClient = useQueryClient()
|
||||
const { showSuccess, showError, showUndo } = useNotification()
|
||||
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
const [isPendingCompletion, setIsPendingCompletion] = useState(false)
|
||||
const [timeoutId, setTimeoutId] = useState(null)
|
||||
const [secondsLeftToCancel, setSecondsLeftToCancel] = useState(null)
|
||||
const [completedDate, setCompletedDate] = useState(null)
|
||||
const [confirmModelConfig, setConfirmModelConfig] = useState({
|
||||
isOpen: false,
|
||||
@@ -189,20 +188,7 @@ const ChoreView = () => {
|
||||
setInfoCards(cards)
|
||||
}
|
||||
const handleTaskCompletion = () => {
|
||||
setIsPendingCompletion(true)
|
||||
let seconds = 3 // Starting countdown from 3 seconds
|
||||
setSecondsLeftToCancel(seconds)
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
seconds -= 1
|
||||
setSecondsLeftToCancel(seconds)
|
||||
|
||||
if (seconds <= 0) {
|
||||
clearInterval(countdownInterval) // Stop the countdown when it reaches 0
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
const id = setTimeout(() => {
|
||||
let id = setTimeout(() => {
|
||||
MarkChoreComplete(
|
||||
choreId,
|
||||
impersonatedUser
|
||||
@@ -220,11 +206,8 @@ const ChoreView = () => {
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
setIsPendingCompletion(false)
|
||||
clearTimeout(id)
|
||||
clearInterval(countdownInterval) // Ensure to clear this interval as well
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null)
|
||||
// Invalidate chores cache to refetch data
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
})
|
||||
@@ -241,6 +224,16 @@ const ChoreView = () => {
|
||||
}, 3000)
|
||||
|
||||
setTimeoutId(id)
|
||||
|
||||
// Show undo notification
|
||||
showSuccess({
|
||||
title: 'Task Completed',
|
||||
message: 'Your task has been marked as complete',
|
||||
undoAction: () => {
|
||||
clearTimeout(id)
|
||||
setTimeoutId(null)
|
||||
},
|
||||
})
|
||||
}
|
||||
const handleSkippingTask = () => {
|
||||
SkipChore(choreId).then(response => {
|
||||
@@ -250,6 +243,36 @@ const ChoreView = () => {
|
||||
setChore(newChore)
|
||||
// Invalidate chores cache to refetch data
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
|
||||
// Show undo notification
|
||||
showSuccess({
|
||||
message: 'Task skipped',
|
||||
undoAction: async () => {
|
||||
try {
|
||||
const undoResponse = await UndoChoreAction(choreId)
|
||||
if (undoResponse.ok) {
|
||||
// Refetch chore details after undo
|
||||
const detailResponse = await GetChoreDetailById(choreId)
|
||||
if (detailResponse.ok) {
|
||||
const detailData = await detailResponse.json()
|
||||
setChore(detailData.res)
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
}
|
||||
showUndo({
|
||||
title: 'Undo Successful',
|
||||
message: 'Task skip has been undone.',
|
||||
})
|
||||
} else {
|
||||
throw new Error('Failed to undo')
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Undo Failed',
|
||||
message: 'Unable to undo the action. Please try again.',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -924,12 +947,11 @@ const ChoreView = () => {
|
||||
size='lg'
|
||||
onClick={handleTaskCompletion}
|
||||
disabled={
|
||||
isPendingCompletion ||
|
||||
notInCompletionWindow(chore) ||
|
||||
(chore.lastCompletedDate !== null &&
|
||||
chore.frequencyType === 'once')
|
||||
}
|
||||
color={isPendingCompletion ? 'danger' : 'success'}
|
||||
color='success'
|
||||
startDecorator={<Check />}
|
||||
sx={{
|
||||
flex: 4,
|
||||
@@ -1017,31 +1039,6 @@ const ChoreView = () => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Snackbar
|
||||
open={isPendingCompletion}
|
||||
endDecorator={
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
setIsPendingCompletion(false)
|
||||
setTimeoutId(null)
|
||||
setSecondsLeftToCancel(null) // Reset or adjust as needed
|
||||
}
|
||||
}}
|
||||
size='lg'
|
||||
variant='outlined'
|
||||
color='danger'
|
||||
startDecorator={<CancelScheduleSend />}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography level='body-md' textAlign={'center'}>
|
||||
Task will be marked as completed in {secondsLeftToCancel} seconds
|
||||
</Typography>
|
||||
</Snackbar>
|
||||
<ConfirmationModal config={confirmModelConfig} />
|
||||
<ConfirmationModal config={timerActionConfig} />
|
||||
</Card>
|
||||
|
||||
@@ -3,6 +3,8 @@ import { LocalNotifications } from '@capacitor/local-notifications'
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
import murmurhash from 'murmurhash'
|
||||
|
||||
const MAX_LOCAL_NOTIFICATIONS = 64
|
||||
|
||||
const getNotificationPreferences = async () => {
|
||||
const ret = await Preferences.get({ key: 'notificationPreferences' })
|
||||
return JSON.parse(ret.value)
|
||||
@@ -206,6 +208,13 @@ const scheduleChoreNotification = async (
|
||||
continue
|
||||
}
|
||||
}
|
||||
// sort from soonest to latest:
|
||||
notifications.sort((a, b) => a.schedule.at - b.schedule.at)
|
||||
|
||||
// cap it for 64 notifications for Android:
|
||||
if (notifications.length > MAX_LOCAL_NOTIFICATIONS) {
|
||||
notifications.splice(MAX_LOCAL_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
LocalNotifications.schedule({
|
||||
notifications,
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useArchiveChore, useDeleteChores } from '../../../queries/ChoreQueries'
|
||||
import { useCallback } from 'react'
|
||||
import { useArchiveChore } from '../../../queries/ChoreQueries'
|
||||
import { usePauseChore, useStartChore } from '../../../queries/TimeQueries'
|
||||
import {
|
||||
ApproveChore,
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
NudgeChore,
|
||||
RejectChore,
|
||||
SkipChore,
|
||||
UndoChoreAction,
|
||||
UpdateChoreAssignee,
|
||||
UpdateDueDate,
|
||||
ApproveChore,
|
||||
DeleteChore,
|
||||
MarkChoreComplete,
|
||||
NudgeChore,
|
||||
RejectChore,
|
||||
SkipChore,
|
||||
UndoChoreAction,
|
||||
UpdateChoreAssignee,
|
||||
UpdateDueDate,
|
||||
} from '../../../utils/Fetcher'
|
||||
|
||||
export const useChoreActions = ({
|
||||
@@ -682,6 +682,23 @@ export const useChoreActions = ({
|
||||
showSuccess({
|
||||
title: '⏭️ Tasks Skipped',
|
||||
message: `Successfully skipped ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`,
|
||||
undoAction: async () => {
|
||||
try {
|
||||
for (const chore of skippedTasks) {
|
||||
await UndoChoreAction(chore.id)
|
||||
}
|
||||
refetchChores()
|
||||
showUndo({
|
||||
title: 'Undo Successful',
|
||||
message: `Undo skip for ${skippedTasks.length} task${skippedTasks.length > 1 ? 's' : ''}.`,
|
||||
})
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Undo Failed',
|
||||
message: 'Unable to undo the action. Please try again.',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -704,7 +721,7 @@ export const useChoreActions = ({
|
||||
setConfirmModelConfig({})
|
||||
},
|
||||
})
|
||||
}, [getSelectedChoresData, showSuccess, showError, refetchChores, clearSelection, setConfirmModelConfig])
|
||||
}, [getSelectedChoresData, showSuccess, showError, showUndo, refetchChores, clearSelection, setConfirmModelConfig])
|
||||
|
||||
return {
|
||||
handleChoreAction,
|
||||
|
||||
Reference in New Issue
Block a user