import { LocalNotifications } from '@capacitor/local-notifications' import { Refresh, Star, Token } from '@mui/icons-material' import { Box, Button, Card, Chip, Divider, Typography } from '@mui/joy' import { useQueryClient } from '@tanstack/react-query' import { useCallback, useEffect, useState } from 'react' import { networkManager } from '../../hooks/NetworkManager' import useConfirmationModal from '../../hooks/useConfirmationModal' import { useSSEContext } from '../../hooks/useSSEContext' import { useUserProfile } from '../../queries/UserQueries' import { evaluatePromptEligibility, isFeedbackSubmissionConfigured, isRawChatWebhookConfigured, requestStoreReview, resetFeedbackState, setDevForcedPrompt, } from '../../service/FeedbackService' import { useNotification } from '../../service/NotificationProvider' import { resetPolicyUpdate } from '../../service/PolicyUpdateService' import { apiClient } from '../../utils/ApiClient' import { commandQueue } from '../../utils/CommandQueue' import { RefreshToken } from '../../utils/Fetcher' import { offlineDB } from '../../utils/OfflineDB' import { syncEngine } from '../../utils/SyncEngine' import { getRefreshTokenExpiry, isNative } from '../../utils/TokenStorage' import FeedbackModal from '../Modals/FeedbackModal' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import PolicyUpdateModal from '../Modals/PolicyUpdateModal' const DeveloperSettings = () => { const queryClient = useQueryClient() const { confirmModalConfig, showConfirmation } = useConfirmationModal() const { data: userProfile } = useUserProfile() const { error: sseError, getConnectionStatus, getDebugInfo, isConnected, isConnecting, lastEvent, } = useSSEContext() const [accessTokenExpiry, setAccessTokenExpiry] = useState(null) const [refreshTokenExpiry, setRefreshTokenExpiry] = useState(null) const [timeLeft, setTimeLeft] = useState({ access: null, refresh: null, }) const [isNativePlatform, setIsNativePlatform] = useState(false) const [sseDebugInfo, setSSEDebugInfo] = useState(null) const [timeSinceLastHeartbeat, setTimeSinceLastHeartbeat] = useState(null) const [isRefreshing, setIsRefreshing] = useState(false) const [isRefreshingDirect, setIsRefreshingDirect] = useState(false) const [scheduledNotifications, setScheduledNotifications] = useState([]) const [isLoadingNotifications, setIsLoadingNotifications] = useState(false) const [isResettingSync, setIsResettingSync] = useState(false) const [feedbackModalOpen, setFeedbackModalOpen] = useState(false) const [policyModalOpen, setPolicyModalOpen] = useState(false) const [feedbackEligibility, setFeedbackEligibility] = useState(null) const [syncDiagnostics, setSyncDiagnostics] = useState({ cursor: null, lastSync: null, pendingCount: 0, failedCount: 0, syncing: false, syncError: null, isOnline: networkManager.isOnline, isNetworkOn: networkManager.isNetworkOn, offlineSince: networkManager.offlineSince, lastChecked: networkManager.lastChecked, }) const { showNotification } = useNotification() const refreshSyncDiagnostics = useCallback(async () => { try { const [cursor, lastSync, pendingCommands, failedCommands] = await Promise.all([ offlineDB.getSyncCursor(), offlineDB.getLastSyncTime(), commandQueue.getPending(), commandQueue.getFailed(), ]) setSyncDiagnostics(prev => ({ ...prev, cursor, lastSync, pendingCount: pendingCommands.length, failedCount: failedCommands.length, isOnline: networkManager.isOnline, isNetworkOn: networkManager.isNetworkOn, offlineSince: networkManager.offlineSince, lastChecked: networkManager.lastChecked, })) } catch (error) { console.error('Failed to load sync diagnostics:', error) } }, []) useEffect(() => { setIsNativePlatform(isNative()) const loadTokenData = async () => { const accessExpiry = localStorage.getItem('token_expiry') setAccessTokenExpiry(accessExpiry) if (isNative()) { const refreshExpiry = await getRefreshTokenExpiry() setRefreshTokenExpiry(refreshExpiry) } } const loadScheduledNotifications = async () => { if (isNative()) { setIsLoadingNotifications(true) try { const pending = await LocalNotifications.getPending() // Sort by schedule time (earliest first) const sorted = pending.notifications.sort((a, b) => { const timeA = a.schedule?.at ? new Date(a.schedule.at).getTime() : 0 const timeB = b.schedule?.at ? new Date(b.schedule.at).getTime() : 0 return timeA - timeB }) setScheduledNotifications(sorted) } catch (error) { console.error('Error loading scheduled notifications:', error) } finally { setIsLoadingNotifications(false) } } } loadTokenData() loadScheduledNotifications() refreshSyncDiagnostics() }, [refreshSyncDiagnostics]) useEffect(() => { const unsubscribeSync = syncEngine.onSyncStateChange(state => { setSyncDiagnostics(prev => ({ ...prev, syncing: typeof state.syncing === 'boolean' ? state.syncing : prev.syncing, syncError: Object.prototype.hasOwnProperty.call(state, 'error') ? state.error : prev.syncError, lastSync: state.lastSync ?? prev.lastSync, })) }) networkManager.registerNetworkListener(() => { setSyncDiagnostics(prev => ({ ...prev, isOnline: networkManager.isOnline, isNetworkOn: networkManager.isNetworkOn, offlineSince: networkManager.offlineSince, lastChecked: networkManager.lastChecked, })) }) const interval = setInterval(() => { refreshSyncDiagnostics() }, 5000) return () => { unsubscribeSync() clearInterval(interval) } }, [refreshSyncDiagnostics]) useEffect(() => { const calculateTimeLeft = () => { const now = new Date() let accessTime = null if (accessTokenExpiry) { const accessExpiryDate = new Date(accessTokenExpiry) const diff = accessExpiryDate - now accessTime = diff > 0 ? diff : 0 } let refreshTime = null if (refreshTokenExpiry) { const refreshExpiryDate = new Date(refreshTokenExpiry) const diff = refreshExpiryDate - now refreshTime = diff > 0 ? diff : 0 } setTimeLeft({ access: accessTime, refresh: refreshTime, }) if (getDebugInfo) { const debugInfo = getDebugInfo() setSSEDebugInfo(debugInfo) setTimeSinceLastHeartbeat(debugInfo.timeSinceLastHeartbeat) } } calculateTimeLeft() const interval = setInterval(calculateTimeLeft, 1000) return () => clearInterval(interval) }, [accessTokenExpiry, refreshTokenExpiry, getDebugInfo]) const formatTimeLeft = milliseconds => { if (milliseconds === null) return 'N/A' if (milliseconds === 0) return 'Expired' const totalSeconds = Math.floor(milliseconds / 1000) const days = Math.floor(totalSeconds / 86400) const hours = Math.floor((totalSeconds % 86400) / 3600) const minutes = Math.floor((totalSeconds % 3600) / 60) const seconds = totalSeconds % 60 const parts = [] if (days > 0) parts.push(`${days}d`) if (hours > 0) parts.push(`${hours}h`) if (minutes > 0) parts.push(`${minutes}m`) if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`) return parts.join(' ') } const getExpiryStatus = milliseconds => { if (milliseconds === null) return 'neutral' if (milliseconds === 0) return 'danger' if (milliseconds < 5 * 60 * 1000) return 'warning' // Less than 5 minutes return 'success' } const handleRefreshToken = async () => { setIsRefreshing(true) try { const result = await apiClient.refreshToken() if (result.success) { showNotification({ type: 'success', message: 'Token refreshed successfully', }) // Reload token expiry data const accessExpiry = localStorage.getItem('token_expiry') setAccessTokenExpiry(accessExpiry) if (isNativePlatform) { const refreshExpiry = await getRefreshTokenExpiry() setRefreshTokenExpiry(refreshExpiry) } } else { showNotification({ type: 'error', message: `Token refresh failed: ${result.error}`, }) } } catch (error) { showNotification({ type: 'error', message: `Token refresh error: ${error.message}`, }) } finally { setIsRefreshing(false) } } const handleDirectRefreshToken = async () => { setIsRefreshingDirect(true) try { const response = await RefreshToken() if (response.ok) { const data = await response.json() showNotification({ type: 'success', message: 'Refresh token endpoint called successfully', }) // Reload token expiry data const accessExpiry = localStorage.getItem('token_expiry') setAccessTokenExpiry(accessExpiry) if (isNativePlatform) { const refreshExpiry = await getRefreshTokenExpiry() setRefreshTokenExpiry(refreshExpiry) } console.log('Refresh token response:', data) } else { const error = await response.text() showNotification({ type: 'error', message: `Refresh token endpoint failed: ${response.status} ${error}`, }) } } catch (error) { showNotification({ type: 'error', message: `Refresh token endpoint error: ${error.message}`, }) } finally { setIsRefreshingDirect(false) } } const handleRefreshNotifications = async () => { if (!isNativePlatform) return setIsLoadingNotifications(true) try { const pending = await LocalNotifications.getPending() // Sort by schedule time (earliest first) const sorted = pending.notifications.sort((a, b) => { const timeA = a.schedule?.at ? new Date(a.schedule.at).getTime() : 0 const timeB = b.schedule?.at ? new Date(b.schedule.at).getTime() : 0 return timeA - timeB }) setScheduledNotifications(sorted) showNotification({ type: 'success', message: `Loaded ${sorted.length} scheduled notifications`, }) } catch (error) { console.error('Error loading scheduled notifications:', error) showNotification({ type: 'error', message: `Error loading notifications: ${error.message}`, }) } finally { setIsLoadingNotifications(false) } } const handleResetDatabaseAndResync = async () => { showConfirmation( 'This will clear local offline data and pending commands, then start a full sync from the beginning. Continue?', 'Clear Local DB & Re-Sync', async () => { setIsResettingSync(true) try { await offlineDB.clearAll() showNotification({ type: 'success', message: 'Local offline database cleared. Starting full sync...', }) const didSync = await syncEngine.sync() if (didSync) { await queryClient.invalidateQueries() showNotification({ type: 'success', message: 'Full sync completed from the beginning', }) } else { showNotification({ type: 'warning', message: 'Database cleared. Full sync did not run (likely offline or already syncing).', }) } } catch (error) { console.error('Failed to reset database and resync:', error) showNotification({ type: 'error', message: `Reset/resync failed: ${error.message}`, }) } finally { await refreshSyncDiagnostics() setIsResettingSync(false) } }, 'Clear & Re-Sync', 'Cancel', 'danger', ) } const refreshFeedbackEligibility = useCallback(async () => { const result = await evaluatePromptEligibility({ userProfile }) setFeedbackEligibility(result) return result }, [userProfile]) useEffect(() => { refreshFeedbackEligibility() }, [refreshFeedbackEligibility]) const handleForceFeedbackPrompt = async () => { await setDevForcedPrompt(true) await refreshFeedbackEligibility() showNotification({ type: 'success', message: 'Next visit to My Chores will show the prompt after ~4s', }) } const handleForcePolicyUpdate = async () => { await resetPolicyUpdate() showNotification({ type: 'success', message: 'Next visit to My Chores will show the policy notice after ~1.5s', }) } const handleResetFeedbackState = async () => { await resetFeedbackState() await refreshFeedbackEligibility() showNotification({ type: 'success', message: 'Feedback state cleared (completions, cooldown, opt-out)', }) } const handleRequestStoreReview = async () => { const requested = await requestStoreReview() await refreshFeedbackEligibility() showNotification({ type: requested ? 'success' : 'warning', message: requested ? 'Review requested. The OS decides whether to actually show it.' : 'Not available — native platform only.', }) } const getNotificationStatusColor = scheduleTime => { if (!scheduleTime) return 'neutral' const now = new Date() const scheduledDate = new Date(scheduleTime) const diffMs = scheduledDate - now if (diffMs < 0) return 'danger' // Past due if (diffMs < 5 * 60 * 1000) return 'warning' // Less than 5 minutes if (diffMs < 60 * 60 * 1000) return 'primary' // Less than 1 hour return 'success' // More than 1 hour } const formatDateTime = timestamp => { if (!timestamp) return 'N/A' return new Date(timestamp).toLocaleString() } return (