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::utils::centered_rect_fixed;
10use super::super::Component;
11use crate::{
12    action::{Action, OptionsActions},
13    mode::{InputMode, Scene},
14    style::{clear_area, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE, RED, VIVID_SKY_BLUE},
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::{backend::crossterm::EventHandler, Input};
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        let send_back = 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        send_back
144    }
145}
146
147impl Component for RewardsAddress {
148    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
149        if !self.active {
150            return Ok(vec![]);
151        }
152        // while in entry mode, keybinds are not captured, so gotta exit entry mode from here
153        let send_back = match &self.state {
154            RewardsAddressState::RewardsAddressAlreadySet => self.capture_inputs(key),
155            RewardsAddressState::ShowTCs => match key.code {
156                KeyCode::Char('y') | KeyCode::Char('Y') => {
157                    if !self.rewards_address_input_field.value().is_empty() {
158                        debug!("User accepted the TCs, but rewards address already set, moving to RewardsAddressAlreadySet");
159                        self.state = RewardsAddressState::RewardsAddressAlreadySet;
160                    } else {
161                        debug!("User accepted the TCs, but no rewards address set, moving to AcceptTCsAndEnterRewardsAddress");
162                        self.state = RewardsAddressState::AcceptTCsAndEnterRewardsAddress;
163                    }
164                    vec![]
165                }
166                KeyCode::Esc => {
167                    debug!("User rejected the TCs, moving to original screen");
168                    self.state = RewardsAddressState::ShowTCs;
169                    vec![Action::SwitchScene(self.back_to)]
170                }
171                _ => {
172                    vec![]
173                }
174            },
175            RewardsAddressState::AcceptTCsAndEnterRewardsAddress => self.capture_inputs(key),
176        };
177        Ok(send_back)
178    }
179
180    fn update(&mut self, action: Action) -> Result<Option<Action>> {
181        let send_back = match action {
182            Action::SwitchScene(scene) => match scene {
183                Scene::StatusRewardsAddressPopUp | Scene::OptionsRewardsAddressPopUp => {
184                    self.active = true;
185                    self.old_value = self.rewards_address_input_field.value().to_string();
186                    if scene == Scene::StatusRewardsAddressPopUp {
187                        self.back_to = Scene::Status;
188                    } else if scene == Scene::OptionsRewardsAddressPopUp {
189                        self.back_to = Scene::Options;
190                    }
191                    // Set to InputMode::Entry as we want to handle everything within our handle_key_events
192                    // so by default if this scene is active, we capture inputs.
193                    Some(Action::SwitchInputMode(InputMode::Entry))
194                }
195                _ => {
196                    self.active = false;
197                    None
198                }
199            },
200            _ => None,
201        };
202        Ok(send_back)
203    }
204
205    fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
206        if !self.active {
207            return Ok(());
208        }
209
210        let layer_zero = centered_rect_fixed(52, 15, area);
211
212        let layer_one = Layout::new(
213            Direction::Vertical,
214            [
215                // for the pop_up_border
216                Constraint::Length(2),
217                // for the input field
218                Constraint::Min(1),
219                // for the pop_up_border
220                Constraint::Length(1),
221            ],
222        )
223        .split(layer_zero);
224
225        // layer zero
226        let pop_up_border = Paragraph::new("").block(
227            Block::default()
228                .borders(Borders::ALL)
229                .title(" Add Your Wallet ")
230                .bold()
231                .title_style(Style::new().fg(VIVID_SKY_BLUE))
232                .padding(Padding::uniform(2))
233                .border_style(Style::new().fg(VIVID_SKY_BLUE)),
234        );
235        clear_area(f, layer_zero);
236
237        match self.state {
238            RewardsAddressState::RewardsAddressAlreadySet => {
239                self.validate(); // FIXME: maybe this should be somewhere else
240                                 // split into 4 parts, for the prompt, input, text, dash , and buttons
241                let layer_two = Layout::new(
242                    Direction::Vertical,
243                    [
244                        // for the prompt text
245                        Constraint::Length(3),
246                        // for the input
247                        Constraint::Length(1),
248                        // for the text
249                        Constraint::Length(6),
250                        // gap
251                        Constraint::Length(1),
252                        // for the buttons
253                        Constraint::Length(1),
254                    ],
255                )
256                .split(layer_one[1]);
257
258                let prompt_text = Paragraph::new(Line::from(vec![
259                    Span::styled("Enter new ".to_string(), Style::default()),
260                    Span::styled("Wallet Address".to_string(), Style::default().bold()),
261                ]))
262                .block(Block::default())
263                .alignment(Alignment::Center)
264                .fg(GHOST_WHITE);
265
266                f.render_widget(prompt_text, layer_two[0]);
267
268                let spaces = " ".repeat(
269                    (INPUT_AREA_REWARDS_ADDRESS - 1) as usize
270                        - self.rewards_address_input_field.value().len(),
271                );
272                let input = Paragraph::new(Span::styled(
273                    format!("{}{} ", spaces, self.rewards_address_input_field.value()),
274                    Style::default()
275                        .fg(if self.can_save { VIVID_SKY_BLUE } else { RED })
276                        .bg(INDIGO)
277                        .underlined(),
278                ))
279                .alignment(Alignment::Center);
280                f.render_widget(input, layer_two[1]);
281
282                let text = Paragraph::new(Text::from(if self.can_save {
283                    vec![
284                        Line::raw("Changing your Wallet will reset and restart"),
285                        Line::raw("all your nodes."),
286                    ]
287                } else {
288                    vec![Line::from(Span::styled(
289                        "Invalid wallet address".to_string(),
290                        Style::default().fg(RED),
291                    ))]
292                }))
293                .alignment(Alignment::Center)
294                .block(
295                    Block::default()
296                        .padding(Padding::horizontal(2))
297                        .padding(Padding::top(2)),
298                );
299
300                f.render_widget(text.fg(GHOST_WHITE), layer_two[2]);
301
302                let dash = Block::new()
303                    .borders(Borders::BOTTOM)
304                    .border_style(Style::new().fg(GHOST_WHITE));
305                f.render_widget(dash, layer_two[3]);
306
307                let buttons_layer = Layout::horizontal(vec![
308                    Constraint::Percentage(55),
309                    Constraint::Percentage(45),
310                ])
311                .split(layer_two[4]);
312
313                let button_no = Line::from(vec![Span::styled(
314                    "  Cancel [Esc]",
315                    Style::default().fg(LIGHT_PERIWINKLE),
316                )]);
317
318                f.render_widget(button_no, buttons_layer[0]);
319
320                let button_yes = Line::from(vec![Span::styled(
321                    "Change Wallet [Enter]",
322                    if self.can_save {
323                        Style::default().fg(EUCALYPTUS)
324                    } else {
325                        Style::default().fg(LIGHT_PERIWINKLE)
326                    },
327                )]);
328                f.render_widget(button_yes, buttons_layer[1]);
329            }
330            RewardsAddressState::ShowTCs => {
331                // split the area into 3 parts, for the lines, hypertext,  buttons
332                let layer_two = Layout::new(
333                    Direction::Vertical,
334                    [
335                        // for the text
336                        Constraint::Length(7),
337                        // for the hypertext
338                        Constraint::Length(1),
339                        // gap
340                        Constraint::Length(5),
341                        // for the buttons
342                        Constraint::Length(1),
343                    ],
344                )
345                .split(layer_one[1]);
346
347                let text = Paragraph::new(vec![
348                    Line::from(Span::styled("Add your wallet to store your node earnings, and we'll pay you rewards to the same wallet after the Network's Token Generation Event.",Style::default())),
349                    Line::from(Span::styled("\n\n",Style::default())),
350                    Line::from(Span::styled("By continuing you agree to the Terms and Conditions found here:",Style::default())),
351                    Line::from(Span::styled("\n\n",Style::default())),
352                    ]
353                )
354                .block(Block::default().padding(Padding::horizontal(2)))
355                .wrap(Wrap { trim: false });
356
357                f.render_widget(text.fg(GHOST_WHITE), layer_two[0]);
358
359                let link = Hyperlink::new(
360                    Span::styled(
361                        "  https://autonomi.com/beta/terms",
362                        Style::default().fg(VIVID_SKY_BLUE),
363                    ),
364                    "https://autonomi.com/beta/terms",
365                );
366
367                f.render_widget_ref(link, layer_two[1]);
368
369                let dash = Block::new()
370                    .borders(Borders::BOTTOM)
371                    .border_style(Style::new().fg(GHOST_WHITE));
372                f.render_widget(dash, layer_two[2]);
373
374                let buttons_layer = Layout::horizontal(vec![
375                    Constraint::Percentage(45),
376                    Constraint::Percentage(55),
377                ])
378                .split(layer_two[3]);
379
380                let button_no = Line::from(vec![Span::styled(
381                    "  No, Cancel [Esc]",
382                    Style::default().fg(LIGHT_PERIWINKLE),
383                )]);
384                f.render_widget(button_no, buttons_layer[0]);
385
386                let button_yes = Paragraph::new(Line::from(vec![Span::styled(
387                    "Yes, I agree! Continue [Y]  ",
388                    Style::default().fg(EUCALYPTUS),
389                )]))
390                .alignment(Alignment::Right);
391                f.render_widget(button_yes, buttons_layer[1]);
392            }
393            RewardsAddressState::AcceptTCsAndEnterRewardsAddress => {
394                // split into 4 parts, for the prompt, input, text, dash , and buttons
395                let layer_two = Layout::new(
396                    Direction::Vertical,
397                    [
398                        // for the prompt text
399                        Constraint::Length(3),
400                        // for the input
401                        Constraint::Length(2),
402                        // for the text
403                        Constraint::Length(3),
404                        // for the hyperlink
405                        Constraint::Length(2),
406                        // gap
407                        Constraint::Length(1),
408                        // for the buttons
409                        Constraint::Length(1),
410                    ],
411                )
412                .split(layer_one[1]);
413
414                let prompt = Paragraph::new(Line::from(vec![
415                    Span::styled("Enter your ", Style::default()),
416                    Span::styled("Wallet Address", Style::default().fg(GHOST_WHITE)),
417                ]))
418                .alignment(Alignment::Center);
419
420                f.render_widget(prompt.fg(GHOST_WHITE), layer_two[0]);
421
422                let spaces = " ".repeat(
423                    (INPUT_AREA_REWARDS_ADDRESS - 1) as usize
424                        - self.rewards_address_input_field.value().len(),
425                );
426                let input = Paragraph::new(Span::styled(
427                    format!("{}{} ", spaces, self.rewards_address_input_field.value()),
428                    Style::default().fg(VIVID_SKY_BLUE).bg(INDIGO).underlined(),
429                ))
430                .alignment(Alignment::Center);
431                f.render_widget(input, layer_two[1]);
432
433                let text = Paragraph::new(vec![Line::from(Span::styled(
434                    "Find out more about compatible wallets, and how to track your earnings:",
435                    Style::default(),
436                ))])
437                .block(Block::default().padding(Padding::horizontal(2)))
438                .wrap(Wrap { trim: false });
439
440                f.render_widget(text.fg(GHOST_WHITE), layer_two[2]);
441
442                let link = Hyperlink::new(
443                    Span::styled(
444                        "  https://autonomi.com/wallet",
445                        Style::default().fg(VIVID_SKY_BLUE),
446                    ),
447                    "https://autonomi.com/wallet",
448                );
449
450                f.render_widget_ref(link, layer_two[3]);
451
452                let dash = Block::new()
453                    .borders(Borders::BOTTOM)
454                    .border_style(Style::new().fg(GHOST_WHITE));
455                f.render_widget(dash, layer_two[4]);
456
457                let buttons_layer = Layout::horizontal(vec![
458                    Constraint::Percentage(50),
459                    Constraint::Percentage(50),
460                ])
461                .split(layer_two[5]);
462
463                let button_no = Line::from(vec![Span::styled(
464                    "  Cancel [Esc]",
465                    Style::default().fg(LIGHT_PERIWINKLE),
466                )]);
467                f.render_widget(button_no, buttons_layer[0]);
468                let button_yes = Paragraph::new(Line::from(vec![Span::styled(
469                    "Save Wallet [Enter]  ",
470                    if self.can_save {
471                        Style::default().fg(EUCALYPTUS)
472                    } else {
473                        Style::default().fg(LIGHT_PERIWINKLE)
474                    },
475                )]))
476                .alignment(Alignment::Right);
477                f.render_widget(button_yes, buttons_layer[1]);
478            }
479        }
480
481        f.render_widget(pop_up_border, layer_zero);
482
483        Ok(())
484    }
485}