1use 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; 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 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 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 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 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 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 Constraint::Length(2),
217 Constraint::Min(1),
219 Constraint::Length(1),
221 ],
222 )
223 .split(layer_zero);
224
225 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(); let layer_two = Layout::new(
242 Direction::Vertical,
243 [
244 Constraint::Length(3),
246 Constraint::Length(1),
248 Constraint::Length(6),
250 Constraint::Length(1),
252 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 let layer_two = Layout::new(
333 Direction::Vertical,
334 [
335 Constraint::Length(7),
337 Constraint::Length(1),
339 Constraint::Length(5),
341 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 let layer_two = Layout::new(
396 Direction::Vertical,
397 [
398 Constraint::Length(3),
400 Constraint::Length(2),
402 Constraint::Length(3),
404 Constraint::Length(2),
406 Constraint::Length(1),
408 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}