Skip to content

Commit 71abed5

Browse files
yemirhanclaude
andcommitted
feat: Add update available modal on app startup
- Create UpdateContext for global update state management - Add UpdateAvailableModal component with "Download Now", "View in Settings", and "Remind Me Later" options - Integrate modal at app root level via UpdateProvider - Track dismissed versions to avoid showing modal again in same session - Bump version to 1.4.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 76db064 commit 71abed5

File tree

5 files changed

+270
-3
lines changed

5 files changed

+270
-3
lines changed

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@android-debugger/desktop",
3-
"version": "1.3.2",
3+
"version": "1.4.0",
44
"description": "Android Debugger Desktop Application",
55
"author": "Android Debugger Team",
66
"private": true,

apps/desktop/src/renderer/App.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,27 @@ import { WebSocketPanel } from './components/WebSocketPanel';
2525
import { AppInstallerPanel } from './components/AppInstallerPanel';
2626
import { useDevices } from './hooks/useDevices';
2727
import { useBackgroundLogcat } from './hooks/useBackgroundLogcat';
28-
import { SdkProvider, LogsProvider } from './contexts';
28+
import { SdkProvider, LogsProvider, UpdateProvider, useUpdateContext } from './contexts';
29+
import { UpdateAvailableModal } from './components/UpdateAvailableModal';
2930

3031
export type TabId = 'dashboard' | 'memory' | 'logs' | 'cpu-fps' | 'network' | 'sdk' | 'settings' | 'app-info' | 'screen-capture' | 'dev-options' | 'file-inspector' | 'intent-tester' | 'battery' | 'crashes' | 'services' | 'network-stats' | 'activity-stack' | 'jobs' | 'alarms' | 'websocket' | 'install-app';
3132

32-
function App() {
33+
function AppContent() {
3334
const [activeTab, setActiveTab] = useState<TabId>('dashboard');
3435
const [selectedDevice, setSelectedDevice] = useState<Device | null>(null);
3536
const [packageName, setPackageName] = useState<string>('');
3637
const { devices, loading: devicesLoading, refresh: refreshDevices } = useDevices();
38+
const { setNavigateToSettings } = useUpdateContext();
3739

3840
// Start logcat in background when device is selected
3941
// This ensures SDK messages are captured regardless of which panel is active
4042
useBackgroundLogcat(selectedDevice);
4143

44+
// Register settings navigation for update modal
45+
useEffect(() => {
46+
setNavigateToSettings(() => setActiveTab('settings'));
47+
}, [setNavigateToSettings]);
48+
4249
// Auto-select first device
4350
useEffect(() => {
4451
if (!selectedDevice && devices.length > 0) {
@@ -171,9 +178,18 @@ function App() {
171178
</main>
172179
</div>
173180
</div>
181+
<UpdateAvailableModal />
174182
</LogsProvider>
175183
</SdkProvider>
176184
);
177185
}
178186

187+
function App() {
188+
return (
189+
<UpdateProvider>
190+
<AppContent />
191+
</UpdateProvider>
192+
);
193+
}
194+
179195
export default App;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import React from 'react';
2+
import { useUpdateContext } from '../contexts/UpdateContext';
3+
4+
export function UpdateAvailableModal() {
5+
const {
6+
updateStatus,
7+
updateInfo,
8+
updateProgress,
9+
showModal,
10+
dismissModal,
11+
downloadUpdate,
12+
navigateToSettings,
13+
} = useUpdateContext();
14+
15+
if (!showModal || updateStatus !== 'available' || !updateInfo) {
16+
return null;
17+
}
18+
19+
const handleViewInSettings = () => {
20+
dismissModal();
21+
navigateToSettings?.();
22+
};
23+
24+
const handleDownload = async () => {
25+
dismissModal();
26+
await downloadUpdate();
27+
};
28+
29+
return (
30+
<div className="fixed inset-0 z-50 flex items-center justify-center">
31+
{/* Backdrop */}
32+
<div
33+
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
34+
onClick={dismissModal}
35+
/>
36+
37+
{/* Modal */}
38+
<div className="relative bg-surface border border-border-muted rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
39+
{/* Header */}
40+
<div className="px-6 pt-6 pb-4">
41+
<div className="flex items-start gap-4">
42+
<div className="w-12 h-12 rounded-xl bg-accent/10 flex items-center justify-center flex-shrink-0">
43+
<svg
44+
className="w-6 h-6 text-accent"
45+
fill="none"
46+
stroke="currentColor"
47+
viewBox="0 0 24 24"
48+
>
49+
<path
50+
strokeLinecap="round"
51+
strokeLinejoin="round"
52+
strokeWidth={1.5}
53+
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
54+
/>
55+
</svg>
56+
</div>
57+
<div className="flex-1 min-w-0">
58+
<h2 className="text-lg font-semibold text-text-primary">
59+
Update Available
60+
</h2>
61+
<p className="text-sm text-text-secondary mt-1">
62+
Version {updateInfo.version} is ready to download
63+
</p>
64+
</div>
65+
<button
66+
onClick={dismissModal}
67+
className="p-1 rounded-md text-text-muted hover:text-text-primary hover:bg-surface-hover transition-colors"
68+
>
69+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
70+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
71+
</svg>
72+
</button>
73+
</div>
74+
</div>
75+
76+
{/* Release Notes */}
77+
{updateInfo.releaseNotes && (
78+
<div className="px-6 pb-4">
79+
<div className="bg-background rounded-lg p-4 max-h-40 overflow-y-auto">
80+
<h3 className="text-xs font-medium text-text-muted uppercase tracking-wider mb-2">
81+
What's New
82+
</h3>
83+
<p className="text-sm text-text-secondary whitespace-pre-wrap">
84+
{updateInfo.releaseNotes}
85+
</p>
86+
</div>
87+
</div>
88+
)}
89+
90+
{/* Actions */}
91+
<div className="px-6 pb-6 flex gap-3">
92+
<button
93+
onClick={dismissModal}
94+
className="flex-1 px-4 py-2.5 text-sm font-medium text-text-secondary bg-surface-hover hover:bg-border-muted rounded-lg transition-colors"
95+
>
96+
Remind Me Later
97+
</button>
98+
<button
99+
onClick={handleViewInSettings}
100+
className="flex-1 px-4 py-2.5 text-sm font-medium text-text-primary bg-surface-hover hover:bg-border-muted border border-border-muted rounded-lg transition-colors"
101+
>
102+
View in Settings
103+
</button>
104+
<button
105+
onClick={handleDownload}
106+
className="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-accent hover:bg-accent/80 rounded-lg transition-colors"
107+
>
108+
Download Now
109+
</button>
110+
</div>
111+
</div>
112+
</div>
113+
);
114+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
2+
import type { UpdateInfo, UpdateProgress } from '@android-debugger/shared';
3+
4+
export type UpdateStatus = 'idle' | 'checking' | 'available' | 'downloading' | 'downloaded' | 'error';
5+
6+
interface UpdateContextType {
7+
updateStatus: UpdateStatus;
8+
updateInfo: UpdateInfo | null;
9+
updateProgress: UpdateProgress | null;
10+
updateError: string | null;
11+
showModal: boolean;
12+
dismissModal: () => void;
13+
checkForUpdates: () => Promise<void>;
14+
downloadUpdate: () => Promise<void>;
15+
installUpdate: () => Promise<void>;
16+
navigateToSettings: (() => void) | null;
17+
setNavigateToSettings: (fn: () => void) => void;
18+
}
19+
20+
const UpdateContext = createContext<UpdateContextType | null>(null);
21+
22+
interface UpdateProviderProps {
23+
children: ReactNode;
24+
}
25+
26+
export function UpdateProvider({ children }: UpdateProviderProps) {
27+
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>('idle');
28+
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
29+
const [updateProgress, setUpdateProgress] = useState<UpdateProgress | null>(null);
30+
const [updateError, setUpdateError] = useState<string | null>(null);
31+
const [showModal, setShowModal] = useState(false);
32+
const [dismissedVersion, setDismissedVersion] = useState<string | null>(null);
33+
const [navigateToSettings, setNavigateToSettingsFn] = useState<(() => void) | null>(null);
34+
35+
// Set up update event listeners
36+
useEffect(() => {
37+
const unsubChecking = window.electronAPI.onUpdateChecking(() => {
38+
setUpdateStatus('checking');
39+
setUpdateError(null);
40+
});
41+
42+
const unsubAvailable = window.electronAPI.onUpdateAvailable((info) => {
43+
setUpdateStatus('available');
44+
setUpdateInfo(info);
45+
// Show modal only if this version wasn't dismissed in this session
46+
if (info.version !== dismissedVersion) {
47+
setShowModal(true);
48+
}
49+
});
50+
51+
const unsubNotAvailable = window.electronAPI.onUpdateNotAvailable(() => {
52+
setUpdateStatus('idle');
53+
setUpdateInfo(null);
54+
setShowModal(false);
55+
});
56+
57+
const unsubProgress = window.electronAPI.onUpdateProgress((progress) => {
58+
setUpdateStatus('downloading');
59+
setUpdateProgress(progress);
60+
});
61+
62+
const unsubDownloaded = window.electronAPI.onUpdateDownloaded((info) => {
63+
setUpdateStatus('downloaded');
64+
setUpdateInfo(info);
65+
setUpdateProgress(null);
66+
});
67+
68+
const unsubError = window.electronAPI.onUpdateError((error) => {
69+
setUpdateStatus('error');
70+
setUpdateError(error);
71+
setShowModal(false);
72+
});
73+
74+
return () => {
75+
unsubChecking();
76+
unsubAvailable();
77+
unsubNotAvailable();
78+
unsubProgress();
79+
unsubDownloaded();
80+
unsubError();
81+
};
82+
}, [dismissedVersion]);
83+
84+
const dismissModal = useCallback(() => {
85+
setShowModal(false);
86+
if (updateInfo?.version) {
87+
setDismissedVersion(updateInfo.version);
88+
}
89+
}, [updateInfo?.version]);
90+
91+
const checkForUpdates = useCallback(async () => {
92+
setUpdateStatus('checking');
93+
setUpdateError(null);
94+
await window.electronAPI.checkForUpdates();
95+
}, []);
96+
97+
const downloadUpdate = useCallback(async () => {
98+
await window.electronAPI.downloadUpdate();
99+
}, []);
100+
101+
const installUpdate = useCallback(async () => {
102+
await window.electronAPI.installUpdate();
103+
}, []);
104+
105+
const setNavigateToSettings = useCallback((fn: () => void) => {
106+
setNavigateToSettingsFn(() => fn);
107+
}, []);
108+
109+
return (
110+
<UpdateContext.Provider
111+
value={{
112+
updateStatus,
113+
updateInfo,
114+
updateProgress,
115+
updateError,
116+
showModal,
117+
dismissModal,
118+
checkForUpdates,
119+
downloadUpdate,
120+
installUpdate,
121+
navigateToSettings,
122+
setNavigateToSettings,
123+
}}
124+
>
125+
{children}
126+
</UpdateContext.Provider>
127+
);
128+
}
129+
130+
export function useUpdateContext() {
131+
const context = useContext(UpdateContext);
132+
if (!context) {
133+
throw new Error('useUpdateContext must be used within an UpdateProvider');
134+
}
135+
return context;
136+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { SdkProvider, useSdkContext } from './SdkContext';
22
export { LogsProvider, useLogsContext } from './LogsContext';
33
export { CrashProvider, useCrashContext } from './CrashContext';
4+
export { UpdateProvider, useUpdateContext } from './UpdateContext';

0 commit comments

Comments
 (0)