node_launchpad/components/popup/
upgrade_launchpad.rs

1// Copyright 2025 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use super::super::Component;
10use super::super::utils::centered_rect_fixed;
11use crate::{
12    action::{Action, UpgradeLaunchpadActions},
13    mode::{InputMode, Scene},
14    style::{EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VIVID_SKY_BLUE, clear_area},
15    widgets::hyperlink::Hyperlink,
16};
17use ant_releases::{AntReleaseRepoActions, ReleaseType};
18use color_eyre::Result;
19use crossterm::event::{KeyCode, KeyEvent};
20use ratatui::{prelude::*, widgets::*};
21use semver::Version;
22use std::time::Duration;
23
24#[derive(Debug, Default)]
25pub struct UpgradeLaunchpadPopup {
26    active: bool,
27    current_version: Option<String>,
28    latest_version: Option<String>,
29}
30
31impl Component for UpgradeLaunchpadPopup {
32    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
33        if !self.active {
34            return Ok(vec![]);
35        }
36
37        match key.code {
38            KeyCode::Enter | KeyCode::Esc => {
39                info!("User dismissed the LP upgrade notification.");
40                self.active = false;
41                Ok(vec![
42                    Action::SwitchInputMode(InputMode::Navigation),
43                    Action::SwitchScene(Scene::Status),
44                ])
45            }
46            _ => Ok(vec![]),
47        }
48    }
49
50    fn update(&mut self, action: Action) -> Result<Option<Action>> {
51        let send_back = match action {
52            Action::SwitchScene(scene) => match scene {
53                Scene::UpgradeLaunchpadPopUp => {
54                    self.active = true;
55                    Some(Action::SwitchInputMode(InputMode::Entry))
56                }
57                _ => {
58                    self.active = false;
59                    None
60                }
61            },
62            Action::UpgradeLaunchpadActions(update_launchpad_actions) => {
63                match update_launchpad_actions {
64                    UpgradeLaunchpadActions::UpdateAvailable {
65                        current_version,
66                        latest_version,
67                    } => {
68                        info!(
69                            "Received UpdateAvailable action with current version: {current_version} and latest version: {latest_version}. Switching to UpgradeLaunchpadPopUp scene."
70                        );
71                        self.current_version = Some(current_version);
72                        self.latest_version = Some(latest_version);
73                        Some(Action::SwitchScene(Scene::UpgradeLaunchpadPopUp))
74                    }
75                }
76            }
77            _ => None,
78        };
79        Ok(send_back)
80    }
81
82    fn register_action_handler(
83        &mut self,
84        tx: tokio::sync::mpsc::UnboundedSender<Action>,
85    ) -> Result<()> {
86        info!("We've received the action sender. Spawning task to check for updates.");
87        tokio::spawn(async move {
88            loop {
89                match check_for_update().await {
90                    Ok(Some((latest_version, current_version))) => {
91                        if let Err(err) = tx.send(Action::UpgradeLaunchpadActions(
92                            UpgradeLaunchpadActions::UpdateAvailable {
93                                current_version: current_version.to_string(),
94                                latest_version: latest_version.to_string(),
95                            },
96                        )) {
97                            error!(
98                                "Error sending UpgradeLaunchpadActions::UpdateAvailable action: {err}"
99                            );
100                        }
101                    }
102                    _ => {
103                        info!("No new launchpad version available.");
104                    }
105                };
106                info!("Checking for LP update in 12 hours..");
107                tokio::time::sleep(Duration::from_secs(12 * 60 * 60)).await;
108            }
109        });
110
111        Ok(())
112    }
113
114    fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
115        if !self.active {
116            return Ok(());
117        }
118
119        let Some(current_version) = self.current_version.as_ref() else {
120            error!(
121                "Current version is not set, even though the upgrade popup is active. This is unexpected."
122            );
123            return Ok(());
124        };
125        let Some(latest_version) = self.latest_version.as_ref() else {
126            error!(
127                "Latest version is not set, even though the upgrade popup is active. This is unexpected."
128            );
129            return Ok(());
130        };
131
132        let layer_zero = centered_rect_fixed(60, 15, area);
133        let layer_one = Layout::new(
134            Direction::Vertical,
135            [
136                Constraint::Length(2),
137                Constraint::Min(1),
138                Constraint::Length(1),
139            ],
140        )
141        .split(layer_zero);
142
143        let pop_up_border = Paragraph::new("").block(
144            Block::default()
145                .borders(Borders::ALL)
146                .title(" Update Available ")
147                .bold()
148                .title_style(Style::new().fg(VIVID_SKY_BLUE))
149                .padding(Padding::uniform(2))
150                .border_style(Style::new().fg(VIVID_SKY_BLUE)),
151        );
152        clear_area(f, layer_zero);
153
154        let layer_two = Layout::new(
155            Direction::Vertical,
156            [
157                Constraint::Length(6),
158                Constraint::Length(2),
159                Constraint::Length(3),
160                Constraint::Length(1),
161            ],
162        )
163        .split(layer_one[1]);
164
165        let text = Paragraph::new(vec![
166            Line::from(Span::styled("\n", Style::default())),
167            Line::from(vec![Span::styled(
168                "A new version of Node Launchpad is available:".to_string(),
169                Style::default().fg(LIGHT_PERIWINKLE),
170            )]),
171            Line::from(vec![Span::styled(
172                format!("v{current_version} → v{latest_version}"),
173                Style::default().fg(LIGHT_PERIWINKLE),
174            )]),
175            Line::from(Span::styled("\n", Style::default())),
176            Line::from(vec![Span::styled(
177                "To update, please download the latest version from:",
178                Style::default().fg(GHOST_WHITE),
179            )]),
180        ])
181        .block(Block::default().padding(Padding::horizontal(2)))
182        .alignment(Alignment::Center)
183        .wrap(Wrap { trim: true });
184
185        f.render_widget(text, layer_two[0]);
186
187        // Center the link in its own layout
188        let link_layout = Layout::default()
189            .direction(Direction::Horizontal)
190            .constraints([
191                Constraint::Percentage(28), // Left margin
192                Constraint::Percentage(44), // Link
193                Constraint::Percentage(28), // Right margin
194            ])
195            .split(layer_two[1]);
196
197        // Render hyperlink with proper spacing and alignment
198        let link = Hyperlink::new(
199            Span::styled(
200                "https://autonomi.com/node",
201                Style::default().fg(VIVID_SKY_BLUE),
202            ),
203            "https://autonomi.com/node",
204        );
205        // Use render_widget_ref for hyperlinks to render correctly
206        f.render_widget_ref(link, link_layout[1]);
207
208        let dash = Block::new()
209            .borders(Borders::BOTTOM)
210            .border_style(Style::new().fg(GHOST_WHITE));
211        f.render_widget(dash, layer_two[2]);
212
213        let buttons_layer =
214            Layout::horizontal(vec![Constraint::Percentage(100)]).split(layer_two[3]);
215
216        let button_ok = Paragraph::new(Line::from(vec![Span::styled(
217            "Press [Enter] to continue",
218            Style::default().fg(EUCALYPTUS),
219        )]))
220        .alignment(Alignment::Center);
221        f.render_widget(button_ok, buttons_layer[0]);
222
223        f.render_widget(pop_up_border, layer_zero);
224
225        Ok(())
226    }
227}
228
229/// Checks if an update is available.
230/// Return New, Current version if available.
231pub async fn check_for_update() -> Result<Option<(Version, Version)>> {
232    let release_repo = <dyn AntReleaseRepoActions>::default_config();
233    let current_version = Version::parse(env!("CARGO_PKG_VERSION"))?;
234
235    match release_repo
236        .get_latest_version(&ReleaseType::NodeLaunchpad)
237        .await
238    {
239        Ok(latest_version) => {
240            info!("Current version: {current_version} and latest version: {latest_version}");
241            if latest_version > current_version {
242                Ok(Some((latest_version, current_version)))
243            } else {
244                Ok(None)
245            }
246        }
247        Err(e) => {
248            debug!("Failed to check for updates: {}", e);
249            Ok(None)
250        }
251    }
252}