diff --git a/mobile/lib/common/settings/seed_screen.dart b/mobile/lib/common/settings/seed_screen.dart index 9b71ca922..683f1ad77 100644 --- a/mobile/lib/common/settings/seed_screen.dart +++ b/mobile/lib/common/settings/seed_screen.dart @@ -92,7 +92,7 @@ class _SeedScreenState extends State { style: TextStyle(fontWeight: FontWeight.bold)), TextSpan( text: - "Save it somewhere safe (not on this phone). If you lose your seed and your phone, you've lost your funds."), + "Save it somewhere safe. If you lose your seed you lose your funds."), ])), ), ), diff --git a/webapp/frontend/lib/settings/seed_screen.dart b/webapp/frontend/lib/settings/seed_screen.dart new file mode 100644 index 000000000..8acc59835 --- /dev/null +++ b/webapp/frontend/lib/settings/seed_screen.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:get_10101/settings/seed_words.dart'; +import 'package:get_10101/settings/settings_service.dart'; +import 'package:provider/provider.dart'; + +class SeedScreen extends StatefulWidget { + const SeedScreen({super.key}); + + @override + State createState() => _SeedScreenState(); +} + +class _SeedScreenState extends State { + bool checked = false; + bool visibility = false; + + List? phrase; + + @override + void initState() { + context.read().getSeedPhrase().then((value) => setState(() => phrase = value)); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.only(top: 20, left: 10, right: 10), + child: Column( + children: [ + Container( + margin: const EdgeInsets.all(10), + child: Center( + child: RichText( + text: const TextSpan( + style: TextStyle(color: Colors.black, fontSize: 18), + children: [ + TextSpan( + text: + "The recovery phrase (sometimes called a seed), is a list of 12 English words. It allows you to recover full access to your funds if needed\n\n"), + TextSpan( + text: "Do not share this seed with anyone. ", + style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan( + text: + "Beware of phising. The developers of 10101 will never ask for your seed.\n\n"), + TextSpan( + text: "Do not lose this seed. ", + style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan( + text: + "Save it somewhere safe (not on this phone). If you lose your seed and your phone, you've lost your funds."), + ])), + ), + ), + const SizedBox(height: 25), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + buildSeedWordsWidget(phrase!, visibility), + const SizedBox(height: 20), + Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: visibility + ? const Icon(Icons.visibility) + : const Icon(Icons.visibility_off), + onPressed: () { + setState(() { + visibility = !visibility; + }); + }, + tooltip: visibility ? 'Hide Seed' : 'Show Seed'), + Text(visibility ? 'Hide Seed' : 'Show Seed') + ], + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/webapp/frontend/lib/settings/seed_words.dart b/webapp/frontend/lib/settings/seed_words.dart new file mode 100644 index 000000000..5c8475259 --- /dev/null +++ b/webapp/frontend/lib/settings/seed_words.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +Row buildSeedWordsWidget(List phrase, bool visible) { + final firstColumn = phrase + .getRange(0, 6) + .toList() + .asMap() + .entries + .map((entry) => SeedWord(entry.value, entry.key + 1, visible)) + .toList(); + final secondColumn = phrase + .getRange(6, 12) + .toList() + .asMap() + .entries + .map((entry) => SeedWord(entry.value, entry.key + 7, visible)) + .toList(); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Column(crossAxisAlignment: CrossAxisAlignment.start, children: firstColumn), + const SizedBox(width: 30), + Column(crossAxisAlignment: CrossAxisAlignment.start, children: secondColumn) + ], + ); +} + +class SeedWord extends StatelessWidget { + final String? word; + final int? index; + final bool visibility; + + const SeedWord(this.word, this.index, this.visibility, {super.key}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(0, 5, 0, 5), + child: Row( + crossAxisAlignment: visibility ? CrossAxisAlignment.baseline : CrossAxisAlignment.end, + textBaseline: TextBaseline.alphabetic, + children: [ + SizedBox( + width: 25.0, + child: Text( + '#$index', + style: TextStyle(fontSize: 14, color: Colors.grey[600]), + ), + ), + const SizedBox(width: 5), + visibility + ? SizedBox( + width: 100, + child: Text( + word!, + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ) + : Container(width: 100, height: 24, color: Colors.grey[300]), + ])); + } +} diff --git a/webapp/frontend/lib/settings/settings_screen.dart b/webapp/frontend/lib/settings/settings_screen.dart index da0a4d74a..0fc557507 100644 --- a/webapp/frontend/lib/settings/settings_screen.dart +++ b/webapp/frontend/lib/settings/settings_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get_10101/common/color.dart'; import 'package:get_10101/settings/app_info_screen.dart'; +import 'package:get_10101/settings/seed_screen.dart'; class SettingsScreen extends StatefulWidget { static const route = "/settings"; @@ -63,7 +64,7 @@ class _SettingsScreenState extends State with SingleTickerProvid Expanded( child: TabBarView( controller: _tabController, - children: const [AppInfoScreen(), Text("Channel"), Text("Backup")], + children: const [AppInfoScreen(), Text("Channel"), SeedScreen()], ), ), ], diff --git a/webapp/frontend/lib/settings/settings_service.dart b/webapp/frontend/lib/settings/settings_service.dart index c704fdb65..3c594f135 100644 --- a/webapp/frontend/lib/settings/settings_service.dart +++ b/webapp/frontend/lib/settings/settings_service.dart @@ -1,23 +1,28 @@ -import 'package:http/http.dart' as http; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:get_10101/common/http_client.dart'; class SettingsService { const SettingsService(); Future getNodeId() async { - // TODO(holzeis): this should come from the config - const port = "3001"; - const host = "localhost"; + final response = await HttpClientManager.instance.get(Uri(path: '/api/node')); + + if (response.statusCode == 200) { + return response.body; + } else { + throw FlutterError("Failed to fetch node id"); + } + } - try { - final response = await http.get(Uri.http('$host:$port', '/api/node')); + Future> getSeedPhrase() async { + final response = await HttpClientManager.instance.get(Uri(path: '/api/seed')); - if (response.statusCode == 200) { - return response.body; - } else { - return "unknown"; - } - } catch (e) { - return "unknown"; + if (response.statusCode == 200) { + return jsonDecode(response.body); + } else { + throw FlutterError("Failed to fetch seed phrase"); } } } diff --git a/webapp/src/api.rs b/webapp/src/api.rs index d3bea5ed5..b290c2e17 100644 --- a/webapp/src/api.rs +++ b/webapp/src/api.rs @@ -151,6 +151,10 @@ pub async fn get_node_id() -> impl IntoResponse { ln_dlc::get_node_pubkey().to_string() } +pub async fn get_seed_phrase() -> Json> { + Json(ln_dlc::get_seed_phrase()) +} + #[derive(Serialize)] pub struct OrderId { id: Uuid, diff --git a/webapp/src/main.rs b/webapp/src/main.rs index 2436d5b5e..216fdedff 100644 --- a/webapp/src/main.rs +++ b/webapp/src/main.rs @@ -7,6 +7,7 @@ use crate::api::get_balance; use crate::api::get_node_id; use crate::api::get_onchain_payment_history; use crate::api::get_positions; +use crate::api::get_seed_phrase; use crate::api::get_unused_address; use crate::api::post_new_order; use crate::api::send_payment; @@ -101,6 +102,7 @@ fn using_serve_dir(subscribers: Arc, network: Network) -> Router .route("/api/orders", post(post_new_order)) .route("/api/positions", get(get_positions)) .route("/api/node", get(get_node_id)) + .route("/api/seed", get(get_seed_phrase)) .route("/main.dart.js", get(main_dart_handler)) .route("/flutter.js", get(flutter_js)) .route("/index.html", get(index_handler))