Files
donetick/src/utils/TokenManager.jsx
Mo Tarbin f27bbd318f Refactor chore management to use React Query hooks for timer and chore actions
- Replaced direct API calls with React Query hooks in ChoreView, ChoreCard, CompactChoreCard, and TimerDetails components for better state management and error handling.
- Updated ArchivedTasks to utilize useUnArchiveChore hook for restoring archived chores.
- Enhanced NotificationSetting to include device registration logic and improved user feedback for push notifications.
- Refactored TimerEditModal and TimerDetails to streamline timer session updates and deletions using hooks.
- Improved ChoreActionMenu to handle archiving and unarchiving chores with hooks.
- Adjusted various components to use getSafeBottomStyles for consistent bottom padding.
- Cleaned up unused imports and optimized loading states across components.
2025-09-21 12:46:03 -04:00

248 lines
6.8 KiB
JavaScript

import { Preferences } from '@capacitor/preferences'
import Cookies from 'js-cookie'
import murmurhash from 'murmurhash'
import { API_URL } from '../Config'
import { FEATURES, isFeatureEnabled } from '../utils/FeatureToggle'
import { networkManager } from '../hooks/NetworkManager'
import { RefreshToken } from './Fetcher'
import { localStore } from './LocalStore'
class ApiManager {
constructor() {
this.customServerURL = `${API_URL}/api/v1`
this.initialized = false
this.initPromise = null
this.navigateToLogin = () => {}
}
async init() {
if (this.initPromise) {
return this.initPromise
}
if (this.initialized) {
return Promise.resolve()
}
this.initPromise = this._doInit()
return this.initPromise
}
async _doInit() {
const { value: serverURL } = await Preferences.get({
key: 'customServerUrl',
})
this.customServerURL = `${serverURL || API_URL}/api/v1`
this.initialized = true
await localStore.initDatabase()
}
getApiURL() {
return this.customServerURL
}
updateApiURL(url) {
this.customServerURL = url
this.init()
}
setNavigateToLogin(callback) {
this.navigateToLogin = callback
}
}
export const apiManager = new ApiManager()
export const getAssetURL = path => {
const baseURL = apiManager.getApiURL()
return `${baseURL}/assets/${path}`
}
export async function UploadFile(url, options) {
await apiManager.init()
if (!isTokenValid()) {
Cookies.set('ca_redirect', window.location.pathname)
window.location.href = '/login'
}
if (!options) {
options = {}
}
const headers = HEADERS()
options.headers = { Authorization: headers['Authorization'] }
const baseURL = apiManager.getApiURL()
const fullURL = `${baseURL}${url}`
return fetch(fullURL, options)
}
export async function Fetch(url, options) {
await apiManager.init()
if (!isTokenValid()) {
Cookies.set('ca_redirect', window.location.pathname)
if (window.location.pathname !== '/login') {
window.location.href = '/login'
}
}
if (!options) {
options = {}
}
// clone options to avoid mutation
options.headers = { ...options.headers, ...HEADERS() }
const baseURL = apiManager.getApiURL()
const fullURL = `${baseURL}${url}`
// const networkStatus = await Network.getStatus()
// if (!networkStatus.connected) {
// return handleOfflineRequest(fullURL, options)
// }
// Online: Perform the fetch
try {
const response = await fetch(fullURL, options)
if (response.ok) {
// Only cache data if offline mode is enabled
if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
const data = await response.clone().json()
const optionWithoutToken = { ...options }
delete optionWithoutToken.headers.Authorization
const optionsHash = murmurhash.v3(JSON.stringify(optionWithoutToken))
await localStore.saveToCache(fullURL + optionsHash, data)
}
networkManager.setOnline()
} else if (response.status === 401) {
// Handle 401 Unauthorized
const errorData = await response.json()
console.error('Unauthorized:', errorData)
localStorage.removeItem('ca_token')
localStorage.removeItem('ca_expiration')
apiManager.navigateToLogin()
} else if (
response.status === 503 ||
response.type === 'opaque' ||
response.status === 0
) {
networkManager.setOffline()
// Only handle offline requests if offline mode is enabled
if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
return handleOfflineRequest(fullURL, options)
}
// If offline mode is disabled, just throw the error
throw new Error(`Request failed with status ${response.status}`)
}
// return promise that resolves to response object:
return Promise.resolve(response)
} catch (error) {
networkManager.setOffline()
console.error('Fetch error:', error)
// Only handle offline requests if offline mode is enabled
if (isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
return handleOfflineRequest(fullURL, options)
}
// If offline mode is disabled, just throw the error
throw error
}
}
export const HEADERS = () => {
// Import here to avoid circular dependency issues
const impersonateUserId = localStorage.getItem('impersonatedUserId')
return {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + localStorage.getItem('ca_token'),
...(impersonateUserId && { 'X-Impersonate-User-ID': impersonateUserId }),
}
}
export const isTokenValid = () => {
const expiration = localStorage.getItem('ca_expiration')
const token = localStorage.getItem('ca_token')
if (token) {
const now = new Date()
const expire = new Date(expiration)
if (now < expire) {
if (now.getTime() + 24 * 60 * 60 * 1000 > expire.getTime()) {
refreshAccessToken()
}
return true
} else {
localStorage.removeItem('ca_token')
localStorage.removeItem('ca_expiration')
}
return false
}
return false
}
export const refreshAccessToken = () => {
RefreshToken().then(res => {
if (res.status === 200) {
res.json().then(data => {
localStorage.setItem('ca_token', data.token)
localStorage.setItem('ca_expiration', data.expire)
})
} else {
return res.json().then(error => {
console.log(error)
})
}
})
}
async function handleOfflineRequest(url, options) {
// Only handle offline requests if offline mode is enabled
if (!isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
throw new Error('Network request failed and offline mode is disabled')
}
// if get request then attempt to fetch from cache otherewise queue it :
if (options.method === 'GET') {
return attemptFetchFromCache(url, options)
} else {
// Queue the request for later processing
const requestId = murmurhash.v3(JSON.stringify({ url, options }))
await localStore.queueRequest(requestId, { url, options })
console.log('Request queued for later processing:', requestId)
return Promise.reject({
error: 'Offline and request queued',
requestId,
queued: true,
})
}
}
async function attemptFetchFromCache(url, options) {
// Only attempt cache fetch if offline mode is enabled
if (!isFeatureEnabled(FEATURES.OFFLINE_MODE)) {
throw new Error('Cache access disabled - offline mode is not enabled')
}
const optionsHash = murmurhash.v3(JSON.stringify(options))
const cachedData = await localStore.getFromCache(url + optionsHash)
networkManager.setOffline()
if (cachedData) {
return Promise.resolve({
ok: true,
status: 200,
json: async () => cachedData,
})
} else {
// TODO: change this to throw error instead of returning promise
return Promise.reject(
new Error(
'No cached data found for URL: ' +
url +
' with options hash: ' +
optionsHash,
),
)
}
}