node_launchpad/components/popup/
rewards_address.rs

1// Copyright 2024 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, OptionsActions},
13    mode::{InputMode, Scene},
14    style::{EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE, RED, VIVID_SKY_BLUE, clear_area},
15    widgets::hyperlink::Hyperlink,
16};
17use arboard::Clipboard;
18use color_eyre::Result;
19use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
20use ratatui::{prelude::*, widgets::*};
21use regex::Regex;
22use tui_input::{Input, backend::crossterm::EventHandler};
23
24const INPUT_SIZE_REWARDS_ADDRESS: u16 = 42; // Etherum address plus 0x
25const INPUT_AREA_REWARDS_ADDRESS: u16 = INPUT_SIZE_REWARDS_ADDRESS + 2; // +2 for the padding
26
27pub struct RewardsAddress {
28    /// Whether the component is active right now, capturing keystrokes + draw things.
29    active: bool,
30    state: RewardsAddressState,
31    rewards_address_input_field: Input,
32    // cache the old value incase user presses Esc.
33    old_value: String,
34    back_to: Scene,
35    can_save: bool,
36}
37
38enum RewardsAddressState {
39    RewardsAddressAlreadySet,
40    ShowTCs,
41    AcceptTCsAndEnterRewardsAddress,
42}
43
44impl RewardsAddress {
45    pub fn new(rewards_address: String) -> Self {
46        let state = if rewards_address.is_empty() {
47            RewardsAddressState::ShowTCs
48        } else {
49            RewardsAddressState::RewardsAddressAlreadySet
50        };
51        Self {
52            active: false,
53            state,
54            rewards_address_input_field: Input::default().with_value(rewards_address),
55            old_value: Default::default(),
56            back_to: Scene::Status,
57            can_save: false,
58        }
59    }
60
61    pub fn validate(&mut self) {
62        if self.rewards_address_input_field.value().is_empty() {
63            self.can_save = false;
64        } else {
65            let re = Regex::new(r"^0x[a-fA-F0-9]{40}$").expect("Failed to compile regex");
66            self.can_save = re.is_match(self.rewards_address_input_field.value());
67        }
68    }
69
70    fn capture_inputs(&mut self, key: KeyEvent) -> Vec<Action> {
71        match key.code {
72            KeyCode::Enter => {
73                self.validate();
74                if self.can_save {
75                    let rewards_address = self
76                        .rewards_address_input_field
77                        .value()
78                        .to_string()
79                        .to_lowercase();
80                    self.rewards_address_input_field = rewards_address.clone().into();
81
82                    debug!(
83                        "Got Enter, saving the rewards address {rewards_address:?}  and switching to RewardsAddressAlreadySet, and Home Scene",
84                    );
85                    self.state = RewardsAddressState::RewardsAddressAlreadySet;
86                    return vec![
87                        Action::StoreRewardsAddress(rewards_address.clone()),
88                        Action::OptionsActions(OptionsActions::UpdateRewardsAddress(
89                            rewards_address,
90                        )),
91                        Action::SwitchScene(Scene::Status),
92                    ];
93                }
94                vec![]
95            }
96            KeyCode::Esc => {
97                debug!(
98                    "Got Esc, restoring the old value {} and switching to actual screen",
99                    self.old_value
100                );
101                // reset to old value
102                self.rewards_address_input_field = self
103                    .rewards_address_input_field
104                    .clone()
105                    .with_value(self.old_value.clone());
106                vec![Action::SwitchScene(self.back_to)]
107            }
108            KeyCode::Char(' ') => vec![],
109            KeyCode::Backspace => {
110                // if max limit reached, we should allow Backspace to work.
111                self.rewards_address_input_field
112                    .handle_event(&Event::Key(key));
113                self.validate();
114                vec![]
115            }
116            KeyCode::Char('v') => {
117                if key.modifiers.contains(KeyModifiers::CONTROL) {
118                    let mut clipboard = match Clipboard::new() {
119                        Ok(clipboard) => clipboard,
120                        Err(e) => {
121                            error!("Error reading Clipboard : {:?}", e);
122                            return vec![];
123                        }
124                    };
125                    if let Ok(content) = clipboard.get_text() {
126                        self.rewards_address_input_field =
127                            self.rewards_address_input_field.clone().with_value(content);
128                    }
129                }
130                vec![]
131            }
132            _ => {
133                if self.rewards_address_input_field.value().chars().count()
134                    < INPUT_SIZE_REWARDS_ADDRESS as usize
135                {
136                    self.rewards_address_input_field
137                        .handle_event(&Event::Key(key));
138                    self.validate();
139                }
140                vec![]
141            }
142        }
143    }
144}
145
146impl Component for RewardsAddress {
147    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
148        if !self.active {
149            return Ok(vec![]);
150        }
151        // while in entry mode, keybinds are not captured, so gotta exit entry mode from here
152        let send_back = match &self.state {
153            RewardsAddressState::RewardsAddressAlreadySet => self.capture_inputs(key),
154            RewardsAddressState::ShowTCs => match key.code {
155                KeyCode::Char('y') | KeyCode::Char('Y') => {
156                    if !self.rewards_address_input_field.value().is_empty() {
157                        debug!(
158                            "User accepted the TCs, but rewards address already set, moving to RewardsAddressAlreadySet"
159                        );
160                        self.state = RewardsAddressState::RewardsAddressAlreadySet;
161                    } else {
162                        debug!(
163                            "User accepted the TCs, but no rewards address set, moving to AcceptTCsAndEnterRewardsAddress"
164                        );
165                        self.state = RewardsAddressState::AcceptTCsAndEnterRewardsAddress;
166                    }
167                    vec![]
168                }
169                KeyCode::Esc => {
170                    debug!("User rejected the TCs, moving to original screen");
171                    self.state = RewardsAddressState::ShowTCs;
172                    vec![Action::SwitchScene(self.back_to)]
173                }
174                _ => {
175                    vec![]
176                }
177            },
178            RewardsAddressState::AcceptTCsAndEnterRewardsAddress => self.capture_inputs(key),
179        };
180        Ok(send_back)
181    }
182
183    fn update(&mut self, action: Action) -> Result<Option<Action>> {
184        let send_back = match action {
185            Action::SwitchScene(scene) => match scene {
186                Scene::StatusRewardsAddressPopUp | Scene::OptionsRewardsAddressPopUp => {
187                    self.active = true;
188                    self.old_value = self.rewards_address_input_field.value().to_string();
189                    if scene == Scene::StatusRewardsAddressPopUp {
190                        self.back_to = Scene::Status;
191                    } else if scene == Scene::OptionsRewardsAddressPopUp {
192                        self.back_to = Scene::Options;
193                    }
194                    // Set to InputMode::Entry as we want to handle everything within our handle_key_events
195                    // so by default if this scene is active, we capture inputs.
196                    Some(Action::SwitchInputMode(InputMode::Entry))
197                }
198                _ => {
199                    self.active = false;
200                    None
201                }
202            },
203            _ => None,
204        };
205        Ok(send_back)
206    }
207
208    fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
209        if !self.active {
210            return Ok(());
211        }
212
213        let layer_zero = centered_rect_fixed(52, 15, area);
214
215        let layer_one = Layout::new(
216            Direction::Vertical,
217            [
218                // for the pop_up_border
219                Constraint::Length(2),
220                // for the input field
221                Constraint::Min(1),
222                // for the pop_up_border
223                Constraint::Length(1),
224            ],
225        )
226        .split(layer_zero);
227
228        // layer zero
229        let pop_up_border = Paragraph::new("").block(
230            Block::default()
231                .borders(Borders::ALL)
232                .title(" Add Your Wallet ")
233                .bold()
234                .title_style(Style::new().fg(VIVID_SKY_BLUE))
235                .padding(Padding::uniform(2))
236                .border_style(Style::new().fg(VIVID_SKY_BLUE)),
237        );
238        clear_area(f, layer_zero);
239
240        match self.state {
241            RewardsAddressState::RewardsAddressAlreadySet => {
242                self.validate(); // FIXME: maybe this should be somewhere else
243                // split into 4 parts, for the prompt, input, text, dash , and buttons
244                let layer_two = Layout::new(
245                    Direction::Vertical,
246                    [
247                        // for the prompt text
248                        Constraint::Length(3),
249                        // for the input
250                        Constraint::Length(1),
251                        // for the text
252                        Constraint::Length(6),
253                        // gap
254                        Constraint::Length(1),
255                        // for the buttons
256                        Constraint::Length(1),
257                    ],
258                )
259                .split(layer_one[1]);
260
261                let prompt_text = Paragraph::new(Line::from(vec![
262                    Span::styled("Enter new ".to_string(), Style::default()),
263                    Span::styled("Wallet Address".to_string(), Style::default().bold()),
264                ]))
265                .block(Block::default())
266                .alignment(Alignment::Center)
267                .fg(GHOST_WHITE);
268
269                f.render_widget(prompt_text, layer_two[0]);
270
271                let spaces = " ".repeat(
272                    (INPUT_AREA_REWARDS_ADDRESS - 1) as usize
273                        - self.rewards_address_input_field.value().len(),
274                );
275                let input = Paragraph::new(Span::styled(
276                    format!("{}{} ", spaces, self.rewards_address_input_field.value()),
277                    Style::default()
278                        .fg(if self.can_save { VIVID_SKY_BLUE } else { RED })
279                        .bg(INDIGO)
280                        .underlined(),
281                ))
282                .alignment(Alignment::Center);
283                f.render_widget(input, layer_two[1]);
284
285                let text = Paragraph::new(Text::from(if self.can_save {
286                    vec![
287                        Line::raw("Changing your Wallet will reset and restart"),
288                        Line::raw("all your nodes."),
289                    ]
290                } else {
291                    vec![Line::from(Span::styled(
292                        "Invalid wallet address".to_string(),
293                        Style::default().fg(RED),
294                    ))]
295                }))
296                .alignment(Alignment::Center)
297                .block(
298                    Block::default()
299                        .padding(Padding::horizontal(2))
300                        .padding(Padding::top(2)),
301                );
302
303                f.render_widget(text.fg(GHOST_WHITE), layer_two[2]);
304
305                let dash = Block::new()
306                    .borders(Borders::BOTTOM)
307                    .border_style(Style::new().fg(GHOST_WHITE));
308                f.render_widget(dash, layer_two[3]);
309
310                let buttons_layer = Layout::horizontal(vec![
311                    Constraint::Percentage(55),
312                    Constraint::Percentage(45),
313                ])
314                .split(layer_two[4]);
315
316                let button_no = Line::from(vec![Span::styled(
317                    "  Cancel [Esc]",
318                    Style::default().fg(LIGHT_PERIWINKLE),
319                )]);
320
321                f.render_widget(button_no, buttons_layer[0]);
322
323                let button_yes = Line::from(vec![Span::styled(
324                    "Change Wallet [Enter]",
325                    if self.can_save {
326                        Style::default().fg(EUCALYPTUS)
327                    } else {
328                        Style::default().fg(LIGHT_PERIWINKLE)
329                    },
330                )]);
331                f.render_widget(button_yes, buttons_layer[1]);
332            }
333            RewardsAddressState::ShowTCs => {
334                // split the area into 3 parts, for the lines, hypertext,  buttons
335                let layer_two = Layout::new(
336                    Direction::Vertical,
337                    [
338                        // for the text
339                        Constraint::Length(7),
340                        // for the hypertext
341                        Constraint::Length(1),
342                        // gap
343                        Constraint::Length(5),
344                        // for the buttons
345                        Constraint::Length(1),
346                    ],
347                )
348                .split(layer_one[1]);
349
350                let text = Paragraph::new(vec![
351                    Line::from(Span::styled("Add a wallet to receive your node earnings. By doing so, you agree to the Terms and Conditions found here:",Style::default())),
352                    Line::from(Span::styled("\n\n",Style::default())),
353                    ]
354                )
355                .block(Block::default().padding(Padding::horizontal(2)))
356                .wrap(Wrap { trim: false });
357
358                f.render_widget(text.fg(GHOST_WHITE), layer_two[0]);
359
360                let link = Hyperlink::new(
361                    Span::styled(
362                        "  https://autonomi.com/node/terms",
363                        Style::default().fg(VIVID_SKY_BLUE),
364                    ),
365                    "https://autonomi.com/node/terms",
366                );
367
368                f.render_widget_ref(link, layer_two[1]);
369
370                let dash = Block::new()
371                    .borders(Borders::BOTTOM)
372                    .border_style(Style::new().fg(GHOST_WHITE));
373                f.render_widget(dash, layer_two[2]);
374
375                let buttons_layer = Layout::horizontal(vec![
376                    Constraint::Percentage(45),
377                    Constraint::Percentage(55),
378                ])
379                .split(layer_two[3]);
380
381                let button_no = Line::from(vec![Span::styled(
382                    "  No, Cancel [Esc]",
383                    Style::default().fg(LIGHT_PERIWINKLE),
384                )]);
385                f.render_widget(button_no, buttons_layer[0]);
386
387                let button_yes = Paragraph::new(Line::from(vec![Span::styled(
388                    "Yes, I agree! Continue [Y]  ",
389                    Style::default().fg(EUCALYPTUS),
390                )]))
391                .alignment(Alignment::Right);
392                f.render_widget(button_yes, buttons_layer[1]);
393            }
394            RewardsAddressState::AcceptTCsAndEnterRewardsAddress => {
395                // split into 4 parts, for the prompt, input, text, dash , and buttons
396                let layer_two = Layout::new(
397                    Direction::Vertical,
398                    [
399                        // for the prompt text
400                        Constraint::Length(3),
401                        // for the input
402                        Constraint::Length(2),
403                        // for the text
404                        Constraint::Length(3),
405                        // for the hyperlink
406                        Constraint::Length(2),
407                        // gap
408                        Constraint::Length(1),
409                        // for the buttons
410                        Constraint::Length(1),
411                    ],
412                )
413                .split(layer_one[1]);
414
415                let prompt = Paragraph::new(Line::from(vec![
416                    Span::styled("Enter your ", Style::default()),
417                    Span::styled("Wallet Address", Style::default().fg(GHOST_WHITE)),
418                ]))
419                .alignment(Alignment::Center);
420
421                f.render_widget(prompt.fg(GHOST_WHITE), layer_two[0]);
422
423                let spaces = " ".repeat(
424                    (INPUT_AREA_REWARDS_ADDRESS - 1) as usize
425                        - self.rewards_address_input_field.value().len(),
426                );
427                let input = Paragraph::new(Span::styled(
428                    format!("{}{} ", spaces, self.rewards_address_input_field.value()),
429                    Style::default().fg(VIVID_SKY_BLUE).bg(INDIGO).underlined(),
430                ))
431                .alignment(Alignment::Center);
432                f.render_widget(input, layer_two[1]);
433
434                let text = Paragraph::new(vec![Line::from(Span::styled(
435                    "Find out more about compatible wallets, and how to track your earnings:",
436                    Style::default(),
437                ))])
438                .block(Block::default().padding(Padding::horizontal(2)))
439                .wrap(Wrap { trim: false });
440
441                f.render_widget(text.fg(GHOST_WHITE), layer_two[2]);
442
443                let link = Hyperlink::new(
444                    Span::styled(
445                        "  https://autonomi.com/wallet",
446                        Style::default().fg(VIVID_SKY_BLUE),
447                    ),
448                    "https://autonomi.com/wallet",
449                );
450
451                f.render_widget_ref(link, layer_two[3]);
452
453                let dash = Block::new()
454                    .borders(Borders::BOTTOM)
455                    .border_style(Style::new().fg(GHOST_WHITE));
456                f.render_widget(dash, layer_two[4]);
457
458                let buttons_layer = Layout::horizontal(vec![
459                    Constraint::Percentage(50),
460                    Constraint::Percentage(50),
461                ])
462                .split(layer_two[5]);
463
464                let button_no = Line::from(vec![Span::styled(
465                    "  Cancel [Esc]",
466                    Style::default().fg(LIGHT_PERIWINKLE),
467                )]);
468                f.render_widget(button_no, buttons_layer[0]);
469                let button_yes = Paragraph::new(Line::from(vec![Span::styled(
470                    "Save Wallet [Enter]  ",
471                    if self.can_save {
472                        Style::default().fg(EUCALYPTUS)
473                    } else {
474                        Style::default().fg(LIGHT_PERIWINKLE)
475                    },
476                )]))
477                .alignment(Alignment::Right);
478                f.render_widget(button_yes, buttons_layer[1]);
479            }
480        }
481
482        f.render_widget(pop_up_border, layer_zero);
483
484        Ok(())
485    }
486}