feat: Add unarchive functionality to ChoreView and enhance project management features

- Implemented unarchive button for archived chores in ChoreView.
- Updated chore status handling to disable actions for archived chores.
- Enhanced MyChores component to filter tasks based on project selection, including a default project.
- Improved ProjectModal to streamline project creation and editing with a new footer layout.
- Refactored ProjectView to accurately count tasks for default and user projects.
- Added project selection dropdown in AddTaskModal with default project handling.
- Updated NavBar to handle logout using apiClient.
- Enhanced ProjectSelector to manage projects with a new menu item for project management.
This commit is contained in:
Mo Tarbin
2026-01-17 19:35:36 -05:00
parent b326b1b3d7
commit 7c8fa27aaf
19 changed files with 1132 additions and 462 deletions

188
src/utils/TokenStorage.js Normal file
View File

@@ -0,0 +1,188 @@
import { Preferences } from '@capacitor/preferences'
// Token storage keys
const TOKEN_KEYS = {
ACCESS_TOKEN: 'token',
ACCESS_TOKEN_EXPIRY: 'token_expiry',
REFRESH_TOKEN: 'refresh_token',
REFRESH_TOKEN_EXPIRY: 'refresh_token_expiry',
}
// Cache platform detection to avoid repeated checks
let _isNativePlatform = null
// Platform detection
const isNativePlatform = () => {
if (_isNativePlatform === null) {
try {
_isNativePlatform =
typeof window !== 'undefined' &&
window.Capacitor?.isNativePlatform?.()
} catch (error) {
console.warn('Platform detection failed, defaulting to web:', error)
_isNativePlatform = false
}
}
return _isNativePlatform
}
// Track if we're currently clearing tokens to prevent race conditions
let clearingTokens = false
/**
* Save tokens based on platform
* Web: Only access tokens to localStorage
* Native: Access tokens to localStorage + refresh tokens to Capacitor Preferences
*/
export const saveTokens = async ({
accessToken,
accessTokenExpiry,
refreshToken,
refreshTokenExpiry,
}) => {
try {
// Always save access tokens to localStorage
if (accessToken) {
localStorage.setItem(TOKEN_KEYS.ACCESS_TOKEN, accessToken)
}
if (accessTokenExpiry) {
localStorage.setItem(TOKEN_KEYS.ACCESS_TOKEN_EXPIRY, accessTokenExpiry)
}
// On native platforms, also save refresh tokens to Capacitor Preferences
if (isNativePlatform()) {
try {
if (refreshToken) {
await Preferences.set({
key: TOKEN_KEYS.REFRESH_TOKEN,
value: refreshToken,
})
}
if (refreshTokenExpiry) {
await Preferences.set({
key: TOKEN_KEYS.REFRESH_TOKEN_EXPIRY,
value: refreshTokenExpiry,
})
}
} catch (error) {
console.error(
'Failed to save refresh tokens to Capacitor Preferences:',
error,
)
// Don't throw - access token is still saved to localStorage
}
}
} catch (error) {
console.error('Error saving tokens:', error)
throw error
}
}
/**
* Get refresh token from storage
* Web: Returns null (uses HTTP-only cookies)
* Native: Returns from Capacitor Preferences
*/
export const getRefreshToken = async () => {
if (!isNativePlatform()) {
return null // Web uses HTTP-only cookies
}
try {
const { value } = await Preferences.get({ key: TOKEN_KEYS.REFRESH_TOKEN })
return value
} catch (error) {
console.error('Error reading refresh token from Preferences:', error)
return null
}
}
/**
* Get refresh token expiry from storage
* Web: Returns null (uses HTTP-only cookies)
* Native: Returns from Capacitor Preferences
*/
export const getRefreshTokenExpiry = async () => {
if (!isNativePlatform()) {
return null // Web uses HTTP-only cookies
}
try {
const { value } = await Preferences.get({
key: TOKEN_KEYS.REFRESH_TOKEN_EXPIRY,
})
return value
} catch (error) {
console.error('Error reading refresh token expiry from Preferences:', error)
return null
}
}
/**
* Check if refresh token is expired
* Web: Returns false (backend handles cookie expiration)
* Native: Checks expiry from Capacitor Preferences
*/
export const isRefreshTokenExpired = async () => {
if (!isNativePlatform()) {
return false // Web uses cookies, backend handles expiration
}
const expiry = await getRefreshTokenExpiry()
if (!expiry) {
return true // No expiry means no token
}
try {
const expiryDate = new Date(expiry)
if (isNaN(expiryDate.getTime())) {
console.error('Invalid refresh token expiry date:', expiry)
return true // Treat invalid date as expired
}
return new Date() >= expiryDate
} catch (error) {
console.error('Error parsing refresh token expiry:', error)
return true // Treat parse error as expired
}
}
/**
* Clear all tokens from all storage locations
* Idempotent - safe to call multiple times
*/
export const clearAllTokens = async () => {
if (clearingTokens) {
return // Already clearing, don't run concurrently
}
clearingTokens = true
try {
// Clear localStorage
localStorage.removeItem(TOKEN_KEYS.ACCESS_TOKEN)
localStorage.removeItem(TOKEN_KEYS.ACCESS_TOKEN_EXPIRY)
// Clean up legacy keys
localStorage.removeItem('ca_token')
localStorage.removeItem('ca_expiration')
localStorage.removeItem('access_token')
// Clear Capacitor Preferences on native
if (isNativePlatform()) {
try {
await Preferences.remove({ key: TOKEN_KEYS.REFRESH_TOKEN })
await Preferences.remove({ key: TOKEN_KEYS.REFRESH_TOKEN_EXPIRY })
} catch (error) {
console.error('Error clearing tokens from Preferences:', error)
}
}
} catch (error) {
console.error('Error clearing tokens:', error)
} finally {
clearingTokens = false
}
}
/**
* Export platform detection helper
*/
export const isNative = isNativePlatform