11import { useEffect , useRef , useState } from "react"
22import { flushSync } from "react-dom"
33import { useUpdateChecker } from "../lib/hooks/use-update-checker"
4+ import { useJustUpdated } from "../lib/hooks/use-just-updated"
45import { Button } from "./ui/button"
56import { IconSpinner } from "../icons"
67
7- // For testing: set to "available" or "downloading " to see the UI
8+ // For testing: set to "available", "downloading", or "just-updated " to see the UI
89// Change to "none" for production
9- const MOCK_STATE : "none" | "available" | "downloading" = "none"
10+ const MOCK_STATE : "none" | "available" | "downloading" | "just-updated" = "none"
1011
1112export function UpdateBanner ( ) {
1213 const {
@@ -15,6 +16,13 @@ export function UpdateBanner() {
1516 installUpdate,
1617 dismissUpdate,
1718 } = useUpdateChecker ( )
19+
20+ const {
21+ justUpdated : realJustUpdated ,
22+ justUpdatedVersion,
23+ dismissJustUpdated,
24+ openChangelog,
25+ } = useJustUpdated ( )
1826 const hasTriggeredInstall = useRef ( false )
1927
2028 // Optimistic loading state - show spinner immediately on click
@@ -25,7 +33,7 @@ export function UpdateBanner() {
2533
2634 // Mock state for testing UI
2735 const [ mockStatus , setMockStatus ] = useState <
28- "available" | "downloading" | "dismissed"
36+ "available" | "downloading" | "dismissed" | "just-updated"
2937 > ( MOCK_STATE === "none" ? "available" : MOCK_STATE )
3038 const [ mockProgress , setMockProgress ] = useState ( 0 )
3139
@@ -45,12 +53,33 @@ export function UpdateBanner() {
4553 }
4654 } , [ isMocking , mockStatus ] )
4755
48- const state = isMocking
49- ? {
50- status : mockStatus === "dismissed" ? "idle" : mockStatus ,
51- progress : mockProgress ,
52- }
53- : realState
56+ // Just updated state (show "What's New" banner)
57+ // When mocking "just-updated", we need to show that state regardless of real state
58+ const justUpdated =
59+ isMocking && MOCK_STATE === "just-updated" ? true : realJustUpdated
60+
61+ // Get current app version for display
62+ const [ currentVersion , setCurrentVersion ] = useState < string | null > ( null )
63+ useEffect ( ( ) => {
64+ window . desktopApi ?. getVersion ( ) . then ( setCurrentVersion )
65+ } , [ ] )
66+
67+ // Use current version for display (or the just updated version if available)
68+ const displayVersion = justUpdatedVersion || currentVersion
69+
70+ // For mocking just-updated, force idle state so only the "What's New" banner shows
71+ const state =
72+ isMocking && MOCK_STATE === "just-updated"
73+ ? { status : "idle" as const , progress : 0 }
74+ : isMocking
75+ ? {
76+ status :
77+ mockStatus === "dismissed" || mockStatus === "just-updated"
78+ ? ( "idle" as const )
79+ : mockStatus ,
80+ progress : mockProgress ,
81+ }
82+ : realState
5483
5584 // Clear pending state when status changes from "available"
5685 // This handles: download started, error occurred, or state reset
@@ -102,6 +131,62 @@ export function UpdateBanner() {
102131 }
103132 }
104133
134+ const handleOpenChangelog = ( ) => {
135+ // Open changelog URL
136+ window . desktopApi ?. openExternal ( "https://1code.dev/changelog" )
137+ // Dismiss the banner
138+ if ( isMocking ) {
139+ setMockStatus ( "dismissed" )
140+ } else {
141+ dismissJustUpdated ( )
142+ }
143+ }
144+
145+ const handleDismissWhatsNew = ( ) => {
146+ if ( isMocking ) {
147+ setMockStatus ( "dismissed" )
148+ } else {
149+ dismissJustUpdated ( )
150+ }
151+ }
152+
153+ // Show "What's New" banner if app was just updated
154+ if ( justUpdated ) {
155+ return (
156+ < div className = "fixed bottom-4 left-4 z-50 flex items-center gap-3 rounded-lg border border-border bg-popover p-2.5 text-sm text-popover-foreground shadow-lg animate-in fade-in-0 slide-in-from-bottom-2" >
157+ < span className = "text-foreground" >
158+ Updated to v{ displayVersion }
159+ </ span >
160+ < div className = "flex items-center gap-2 ml-2" >
161+ < Button size = "sm" onClick = { handleOpenChangelog } >
162+ See what's new
163+ </ Button >
164+ < button
165+ onClick = { handleDismissWhatsNew }
166+ className = "text-muted-foreground hover:text-foreground transition-colors p-1 rounded hover:bg-muted"
167+ aria-label = "Dismiss"
168+ >
169+ < svg
170+ width = "14"
171+ height = "14"
172+ viewBox = "0 0 14 14"
173+ fill = "none"
174+ xmlns = "http://www.w3.org/2000/svg"
175+ >
176+ < path
177+ d = "M11 3L3 11M3 3L11 11"
178+ stroke = "currentColor"
179+ strokeWidth = "1.5"
180+ strokeLinecap = "round"
181+ strokeLinejoin = "round"
182+ />
183+ </ svg >
184+ </ button >
185+ </ div >
186+ </ div >
187+ )
188+ }
189+
105190 // Don't show anything for idle, checking, or error states
106191 if (
107192 state . status === "idle" ||
0 commit comments