1use 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; const INPUT_AREA_REWARDS_ADDRESS: u16 = INPUT_SIZE_REWARDS_ADDRESS + 2; pub struct RewardsAddress {
28 active: bool,
30 state: RewardsAddressState,
31 rewards_address_input_field: Input,
32 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 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 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 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 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 Constraint::Length(2),
220 Constraint::Min(1),
222 Constraint::Length(1),
224 ],
225 )
226 .split(layer_zero);
227
228 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(); let layer_two = Layout::new(
245 Direction::Vertical,
246 [
247 Constraint::Length(3),
249 Constraint::Length(1),
251 Constraint::Length(6),
253 Constraint::Length(1),
255 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 let layer_two = Layout::new(
336 Direction::Vertical,
337 [
338 Constraint::Length(7),
340 Constraint::Length(1),
342 Constraint::Length(5),
344 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 let layer_two = Layout::new(
397 Direction::Vertical,
398 [
399 Constraint::Length(3),
401 Constraint::Length(2),
403 Constraint::Length(3),
405 Constraint::Length(2),
407 Constraint::Length(1),
409 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}