From 359202a59449256cca737439b7b90a933adbd30c Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 14 Jul 2026 11:08:46 -0400 Subject: [PATCH 1/9] fix: clear offline data on logout, session expiry, server change, and login Offline DB was never wiped during logout, token expiry, or server URL change, allowing stale data to persist across users and servers. Also plugs the refresh-token expiry path which bypassed handleLogout entirely. --- src/hooks/useAuth.jsx | 7 +++++++ src/utils/ApiClient.js | 11 +++++++---- src/views/Authorization/LoginSettings.jsx | 6 ++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/hooks/useAuth.jsx b/src/hooks/useAuth.jsx index 6fcc744..49a8765 100644 --- a/src/hooks/useAuth.jsx +++ b/src/hooks/useAuth.jsx @@ -1,6 +1,7 @@ import { createContext, useContext, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { apiClient } from '../utils/ApiClient' +import { offlineDB } from '../utils/OfflineDB' import { clearAllTokens, saveTokens } from '../utils/TokenStorage' const AuthContext = createContext(null) @@ -67,6 +68,12 @@ export const AuthProvider = ({ children }) => { if (userToken) { setToken(userToken) + try { + await offlineDB.clearAll() + } catch (e) { + console.error('Error clearing offline data on login', e) + } + // Use centralized token storage await saveTokens({ accessToken: userToken, diff --git a/src/utils/ApiClient.js b/src/utils/ApiClient.js index d517c91..a945195 100644 --- a/src/utils/ApiClient.js +++ b/src/utils/ApiClient.js @@ -2,6 +2,7 @@ import { Preferences } from '@capacitor/preferences' import { API_URL } from '../Config' import { networkManager } from '../hooks/NetworkManager' import { logout, RefreshToken } from './Fetcher' +import { offlineDB } from './OfflineDB' import { clearAllTokens, isRefreshTokenExpired, @@ -49,10 +50,7 @@ class ApiClient { const refreshExpired = await isRefreshTokenExpired() if (refreshExpired) { console.log('Refresh token expired, forcing logout') - await clearAllTokens() - if (window.location.pathname !== '/login') { - window.location.href = '/login' - } + await this.handleLogout() return { success: false, error: 'Refresh token expired' } } @@ -135,6 +133,11 @@ class ApiClient { // Helper to avoid repeating cleanup code async handleLogout() { await clearAllTokens() + try { + await offlineDB.clearAll() + } catch (e) { + console.error('Error clearing offline data on logout', e) + } try { await logout() } catch (e) { diff --git a/src/views/Authorization/LoginSettings.jsx b/src/views/Authorization/LoginSettings.jsx index 914e0e6..1de190c 100644 --- a/src/views/Authorization/LoginSettings.jsx +++ b/src/views/Authorization/LoginSettings.jsx @@ -18,6 +18,7 @@ import { API_URL } from '../../Config' import Logo from '../../Logo' import { useResource } from '../../queries/ResourceQueries' import { apiClient } from '../../utils/ApiClient' +import { offlineDB } from '../../utils/OfflineDB' const CONNECTION_TIMEOUT_MS = 8000 @@ -166,6 +167,11 @@ const LoginSettings = () => { } await Preferences.set({ key: 'customServerUrl', value: trimmedURL }) + try { + await offlineDB.clearAll() + } catch (e) { + console.error('Error clearing offline data on server change', e) + } await apiClient.init(true) refetchResource() setStatus('success') From bbad583e539d3304381d70ac4efdbd7c22be0130 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 14 Jul 2026 11:15:50 -0400 Subject: [PATCH 2/9] fix: correct day-of-month recurrence parsing to use monthly interval and set next due date --- src/views/components/AddTaskModal.jsx | 2 ++ src/views/components/CustomParsers.js | 35 ++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx index 8f5a08d..6d1b506 100644 --- a/src/views/components/AddTaskModal.jsx +++ b/src/views/components/AddTaskModal.jsx @@ -422,6 +422,8 @@ const TaskInput = ({ onChoreUpdate, isModalOpen, onClose }) => { if (dueDateParsed.result) { syncDueDateStates(dueDateParsed.result) dueDateHighlight = dueDateParsed.highlight[0] + } else if (repeat.dueDate) { + syncDueDateStates(repeat.dueDate) } // Create the cleaned sentence by sequentially applying all cleanups diff --git a/src/views/components/CustomParsers.js b/src/views/components/CustomParsers.js index 8e5cb4e..df9a5d1 100644 --- a/src/views/components/CustomParsers.js +++ b/src/views/components/CustomParsers.js @@ -412,14 +412,37 @@ export const parseRepeatV2 = inputSentence => { } case 'day_of_the_month:every': - result.frequency = parseInt(match[1], 10) - result.frequencyMetadata.months = ALL_MONTHS - result.frequencyMetadata.unit = 'days' + const dayOfMonth = parseInt(match[1], 10) + result.frequencyType = 'interval' + result.frequency = 1 + result.frequencyMetadata.unit = 'months' + + // Calculate the next occurrence of this day of the month + const todayEvery = new Date() + let suggestedDueDate = new Date( + todayEvery.getFullYear(), + todayEvery.getMonth(), + dayOfMonth, + 23, + 59, + 0, + ) + if (suggestedDueDate <= todayEvery) { + // Day has already passed this month, move to next month + suggestedDueDate = new Date( + todayEvery.getFullYear(), + todayEvery.getMonth() + 1, + dayOfMonth, + 23, + 59, + 0, + ) + } + return { result, - name: pattern.name - .replace('{day}', result.frequency) - .replace('{months}', result.frequencyMetadata.months.join(', ')), + name: pattern.name.replace('{day}', dayOfMonth), + dueDate: suggestedDueDate.toISOString(), highlight: [ { text: pattern.name, From 4bfff4b7e973d4affa23f3118b12889ea8ba32e9 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 14 Jul 2026 11:57:29 -0400 Subject: [PATCH 3/9] Add script to handle secrets fetching --- scripts/pull-secrets.sh | 71 +++++++++++++++++++++++++++++++++++++++++ scripts/push-secrets.sh | 69 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100755 scripts/pull-secrets.sh create mode 100755 scripts/push-secrets.sh diff --git a/scripts/pull-secrets.sh b/scripts/pull-secrets.sh new file mode 100755 index 0000000..d0fee1f --- /dev/null +++ b/scripts/pull-secrets.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BW_SERVER="${BW_SERVER:-https://bitwarden.com}" + +# ── Auth ────────────────────────────────────────────────────────────────────── +echo "→ Connecting to Vaultwarden at $BW_SERVER" +CURRENT_SERVER=$(bw status | jq -r '.serverUrl // empty') + +if [ "$CURRENT_SERVER" != "$BW_SERVER" ]; then + bw logout || true + bw config server "$BW_SERVER" +fi + +if [ -z "${BW_SESSION:-}" ]; then + BW_LOGIN_STATUS=$(bw status | jq -r '.status') + + if [ "$BW_LOGIN_STATUS" = "unauthenticated" ]; then + if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then + bw login --apikey + else + bw login + fi + fi + + export BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw) +fi + +bw sync --session "$BW_SESSION" > /dev/null + +# ── Helper ──────────────────────────────────────────────────────────────────── +get_note() { + bw get item "$1" --session "$BW_SESSION" | jq -r '.notes' +} + +get_password() { + bw get item "$1" --session "$BW_SESSION" | jq -r '.login.password // .notes' +} + +# ── Android ─────────────────────────────────────────────────────────────────── +echo "→ Writing android/app/google-services.json" +get_note "Donetick Google Services Android" > "$REPO_ROOT/android/app/google-services.json" + +echo "→ Writing android keystore" +get_note "Donetick Android Keystore" | base64 --decode > "$REPO_ROOT/android/app/release/donetick.jks" + +KEYSTORE_PASSWORD=$(get_password "Donetick Keystore Password") + +cat > "$REPO_ROOT/android/keystore.properties" < "$REPO_ROOT/android/play-service-account.json" + +# ── iOS ─────────────────────────────────────────────────────────────────────── +echo "→ Writing ios/App/App/GoogleService-Info.plist" +get_note "Donetick Google Services iOS" > "$REPO_ROOT/ios/App/App/GoogleService-Info.plist" + +echo "→ Writing App Store Connect key" +get_note "Donetick App Store Connect Key" | base64 --decode > "$REPO_ROOT/ios/AuthKey_84F695CDQ3.p8" + +# ── Env ─────────────────────────────────────────────────────────────────────── +echo "→ Writing .env.production" +get_note "Donetick Env Production" > "$REPO_ROOT/.env.production" + +echo "✓ All secrets pulled successfully" diff --git a/scripts/push-secrets.sh b/scripts/push-secrets.sh new file mode 100755 index 0000000..5b496ee --- /dev/null +++ b/scripts/push-secrets.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# One-time script to upload local secrets into Vaultwarden. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# BW_SERVER="${BW_SERVER:-https://www.bitwareden.com}" + +# ── Auth ────────────────────────────────────────────────────────────────────── +# bw config server "$BW_SERVER" +# bw login +export BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw) + +# ── Helper ──────────────────────────────────────────────────────────────────── +upsert_secure_note() { + local name="$1" + local content="$2" + local existing_id + existing_id=$(bw list items --session "$BW_SESSION" | jq -r --arg n "$name" '.[] | select(.name == $n) | .id' | head -1) + + if [[ -n "$existing_id" ]]; then + bw get item "$existing_id" --session "$BW_SESSION" \ + | jq --arg c "$content" '.notes = $c' \ + | bw encode \ + | bw edit item "$existing_id" --session "$BW_SESSION" > /dev/null + echo " ✓ Updated: $name" + else + bw get template item --session "$BW_SESSION" \ + | jq --arg n "$name" --arg c "$content" \ + '.name = $n | .type = 2 | .secureNote = {"type":0} | .notes = $c' \ + | bw encode \ + | bw create item --session "$BW_SESSION" > /dev/null + echo " ✓ Created: $name" + fi +} + +# ── Upload ──────────────────────────────────────────────────────────────────── +echo "→ Uploading Android google-services.json" +upsert_secure_note \ + "Donetick Google Services Android" \ + "$(cat "$REPO_ROOT/android/app/google-services.json")" + +echo "→ Uploading Fastline google-services.json" +upsert_secure_note \ + "Donetick Google Play Service Account" \ + "$(cat "$REPO_ROOT/donetick-5f910-5688a280a65a--fastline.json")" + +echo "→ Uploading Android keystore (base64)" +upsert_secure_note \ + "Donetick Android Keystore" \ + "$(base64 < /Users/mohamad-macbook-air/donetick-android-ley)" + +echo "→ Uploading iOS GoogleService-Info.plist" +upsert_secure_note \ + "Donetick Google Services iOS" \ + "$(cat "$REPO_ROOT/ios/App/App/GoogleService-Info.plist")" + +echo "→ Uploading App Store Connect key (base64)" +upsert_secure_note \ + "Donetick App Store Connect Key" \ + "$(base64 < /Users/mohamad-macbook-air/Downloads/AuthKey_84F695CDQ3.p8)" + +echo "→ Uploading .env.production" +upsert_secure_note \ + "Donetick Env Production" \ + "$(cat "$REPO_ROOT/.env.production")" + +echo "" +echo "✓ All secrets uploaded. Verify in Vaultwarden, then you can safely delete local copies outside the repo." +echo " NOTE: 'Donetick Keystore Password' should already exist — if not, create it manually as a Login item." From 08fba15feec21a2146746bb0cf4957a86677e32b Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 14 Jul 2026 12:24:40 -0400 Subject: [PATCH 4/9] fix https://github.com/donetick/frontend/issues/95 --- src/views/Things/ThingsView.jsx | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/views/Things/ThingsView.jsx b/src/views/Things/ThingsView.jsx index 4964f6f..3ab615a 100644 --- a/src/views/Things/ThingsView.jsx +++ b/src/views/Things/ThingsView.jsx @@ -355,6 +355,31 @@ const ThingsView = () => { }) } + const handleSetThingState = thing => { + UpdateThingState(thing) + .then(result => { + result.json().then(data => { + const currentThings = [...things] + const thingIndex = currentThings.findIndex( + currentThing => currentThing.id === thing.id, + ) + currentThings[thingIndex] = data.res + setThings(currentThings) + showNotification({ + type: 'success', + title: 'Updated', + message: 'Thing state updated successfully', + }) + }) + }) + .catch(error => { + showError({ + title: 'Unable to update thing state', + message: 'An error occurred while updating the thing state', + }) + }) + } + return ( @@ -550,7 +575,7 @@ const ThingsView = () => { setIsShowEditStateModal(false) setCreateModalThing(null) }} - onSave={handleStateChangeRequest} + onSave={handleSetThingState} currentThing={createModalThing} /> )} From 40270f219abfc293b29af6df7bd870fbe9e24593 Mon Sep 17 00:00:00 2001 From: Mo Tarbin Date: Tue, 14 Jul 2026 12:29:32 -0400 Subject: [PATCH 5/9] Fix: make sure the calendar doesn't take the full screen on desktop --- src/views/components/DueDatePickerField.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/views/components/DueDatePickerField.jsx b/src/views/components/DueDatePickerField.jsx index bb0685a..af6ba3a 100644 --- a/src/views/components/DueDatePickerField.jsx +++ b/src/views/components/DueDatePickerField.jsx @@ -231,6 +231,7 @@ const DueDatePickerField = ({ open={isOpen} onClose={() => setIsOpen(false)} title='Due Date' + fullWidth={false} footer={ {hasDueDate && ( @@ -266,7 +267,7 @@ const DueDatePickerField = ({ } > - + {/* Date shortcuts */} Date: Tue, 14 Jul 2026 12:59:47 -0400 Subject: [PATCH 6/9] fix: add ability to create label in label picker --- src/views/components/BaseOptionPicker.jsx | 4 ++ src/views/components/LabelsPickerField.jsx | 83 ++++++++++++++-------- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/src/views/components/BaseOptionPicker.jsx b/src/views/components/BaseOptionPicker.jsx index c176020..bac0434 100644 --- a/src/views/components/BaseOptionPicker.jsx +++ b/src/views/components/BaseOptionPicker.jsx @@ -23,6 +23,7 @@ const BaseOptionPicker = ({ getItemColor, getTriggerText, onClear, + menuFooter, }) => { const [isOpen, setIsOpen] = useState(false) const buttonRef = useRef(null) @@ -242,6 +243,9 @@ const BaseOptionPicker = ({ ) })} + {menuFooter && ( + 0 ? 0.5 : 0 }}>{menuFooter} + )} diff --git a/src/views/components/LabelsPickerField.jsx b/src/views/components/LabelsPickerField.jsx index bf74602..ff27a4a 100644 --- a/src/views/components/LabelsPickerField.jsx +++ b/src/views/components/LabelsPickerField.jsx @@ -1,4 +1,7 @@ -import { Label } from '@mui/icons-material' +import { Add, Label } from '@mui/icons-material' +import { Button } from '@mui/joy' +import { useState } from 'react' +import LabelModal from '../Modals/Inputs/LabelModal' import BaseOptionPicker from './BaseOptionPicker' const LabelsPickerField = ({ @@ -8,6 +11,8 @@ const LabelsPickerField = ({ labels = [], emptyDisplay = 'icon-text', }) => { + const [createOpen, setCreateOpen] = useState(false) + const options = labels.map(label => ({ id: label.id, name: label.name, @@ -15,33 +20,55 @@ const LabelsPickerField = ({ })) return ( - item.id} - getItemLabel={item => item.name} - getItemColor={item => item.color} - renderTriggerIcon={() => - + {activeCount > 0 && ( onRequireApprovalChange(!requireApproval)} > onIsPrivateChange(!isPrivate) : undefined + } >