From 46e8b23181db84123877ab38cda838ac8b94ccc4 Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Mon, 22 Jan 2024 12:27:05 +0100 Subject: [PATCH] feat: Add app info to settings --- webapp/build.rs | 14 ++ .../lib/common/scaffold_with_nav.dart | 2 +- .../frontend/lib/common/version_service.dart | 17 +- webapp/frontend/lib/main.dart | 4 +- .../lib/settings/app_info_screen.dart | 172 ++++++++++++++++++ .../lib/settings/settings_screen.dart | 66 ++++++- .../lib/settings/settings_service.dart | 23 +++ webapp/src/api.rs | 8 + webapp/src/main.rs | 2 + 9 files changed, 291 insertions(+), 17 deletions(-) create mode 100644 webapp/frontend/lib/settings/app_info_screen.dart create mode 100644 webapp/frontend/lib/settings/settings_service.dart diff --git a/webapp/build.rs b/webapp/build.rs index 931f1b6df..c8b0e6725 100644 --- a/webapp/build.rs +++ b/webapp/build.rs @@ -1,4 +1,5 @@ use std::fs; +use std::process::Command; fn main() { // ensure that the directory exists which needs to be embedded in our binary @@ -6,4 +7,17 @@ fn main() { if fs::create_dir_all(directory_path).is_err() { std::process::exit(1); } + + let output = Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .expect("To be able to get commit hash"); + let git_hash = String::from_utf8(output.stdout).expect("To be a valid string"); + let output = Command::new("git") + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .output() + .expect("To be able to get branch name"); + let branch_name = String::from_utf8(output.stdout).expect("To be a valid string"); + println!("cargo:rustc-env=COMMIT_HASH={}", git_hash); + println!("cargo:rustc-env=BRANCH_NAME={}", branch_name); } diff --git a/webapp/frontend/lib/common/scaffold_with_nav.dart b/webapp/frontend/lib/common/scaffold_with_nav.dart index fbffbe6d7..5f32fc3fd 100644 --- a/webapp/frontend/lib/common/scaffold_with_nav.dart +++ b/webapp/frontend/lib/common/scaffold_with_nav.dart @@ -56,7 +56,7 @@ class _ScaffoldWithNestedNavigation extends State @override void initState() { super.initState(); - context.read().fetchVersion().then((v) => setState(() => version = v)); + context.read().fetchVersion().then((v) => setState(() => version = v.version)); context.read().getBalance().then((b) => setState(() => balance = b)); } diff --git a/webapp/frontend/lib/common/version_service.dart b/webapp/frontend/lib/common/version_service.dart index 460c5156d..c90c783f9 100644 --- a/webapp/frontend/lib/common/version_service.dart +++ b/webapp/frontend/lib/common/version_service.dart @@ -1,17 +1,22 @@ +import 'package:flutter/cupertino.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; class Version { final String version; + final String commitHash; + final String branch; - const Version({required this.version}); + const Version({required this.version, required this.commitHash, required this.branch}); factory Version.fromJson(Map json) { return switch (json) { { 'version': String version, + 'commit_hash': String commitHash, + 'branch': String branch, } => - Version(version: version), + Version(version: version, commitHash: commitHash, branch: branch), _ => throw const FormatException('Failed to load version.'), }; } @@ -20,7 +25,7 @@ class Version { class VersionService { const VersionService(); - Future fetchVersion() async { + Future fetchVersion() async { // TODO(holzeis): this should come from the config const port = "3001"; const host = "localhost"; @@ -29,12 +34,12 @@ class VersionService { final response = await http.get(Uri.http('$host:$port', '/api/version')); if (response.statusCode == 200) { - return Version.fromJson(jsonDecode(response.body) as Map).version; + return Version.fromJson(jsonDecode(response.body) as Map); } else { - return 'unknown'; + throw FlutterError("Failed to fetch version"); } } catch (e) { - return "unknown"; + return throw FlutterError("Failed to fetch version. $e"); } } } diff --git a/webapp/frontend/lib/main.dart b/webapp/frontend/lib/main.dart index 3a5e35e45..e18baec94 100644 --- a/webapp/frontend/lib/main.dart +++ b/webapp/frontend/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get_10101/common/version_service.dart'; import 'package:get_10101/logger/logger.dart'; import 'package:get_10101/routes.dart'; +import 'package:get_10101/settings/settings_service.dart'; import 'package:get_10101/wallet/wallet_service.dart'; import 'package:provider/provider.dart'; @@ -15,7 +16,8 @@ void main() { var providers = [ Provider(create: (context) => const VersionService()), - Provider(create: (context) => const WalletService()) + Provider(create: (context) => const WalletService()), + Provider(create: (context) => const SettingsService()) ]; runApp(MultiProvider(providers: providers, child: const TenTenOneApp())); } diff --git a/webapp/frontend/lib/settings/app_info_screen.dart b/webapp/frontend/lib/settings/app_info_screen.dart new file mode 100644 index 000000000..a7a0161b9 --- /dev/null +++ b/webapp/frontend/lib/settings/app_info_screen.dart @@ -0,0 +1,172 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get_10101/common/color.dart'; +import 'package:get_10101/common/snack_bar.dart'; +import 'package:get_10101/common/version_service.dart'; +import 'package:get_10101/logger/logger.dart'; +import 'package:get_10101/settings/settings_service.dart'; +import 'package:provider/provider.dart'; + +class AppInfoScreen extends StatefulWidget { + const AppInfoScreen({super.key}); + + @override + State createState() => _AppInfoScreenState(); +} + +class _AppInfoScreenState extends State { + EdgeInsets margin = const EdgeInsets.all(10); + + String _version = ""; + String _nodeId = ""; + String _commit = "not available"; + String _branch = "not available"; + + @override + void initState() { + try { + // todo: fetch node id from backend. + var nodeId = "unknown"; // rust.api.getNodeId(); + _nodeId = nodeId; + } catch (e) { + logger.e("Error getting node id: $e"); + _nodeId = "UNKNOWN"; + } + + _nodeId = "03e637498b08254711199fc9ca0b0ae557cf279711ed7c827d48ba01bda146b0c9"; + + Future.wait([ + context.read().fetchVersion(), + context.read().getNodeId() + ]).then((value) { + final version = value[0]; + final nodeId = value[1]; + + setState(() { + _commit = version.commitHash; + _branch = version.branch; + _version = version.version; + _nodeId = "03e637498b08254711199fc9ca0b0ae557cf279711ed7c827d48ba01bda146b0c9"; + }); + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + padding: const EdgeInsets.only(top: 20, left: 10, right: 10), + child: Column( + children: [ + Column( + children: [ + Container( + margin: const EdgeInsets.only(top: 20, left: 10, right: 10, bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "NODE INFO", + style: TextStyle(color: Colors.grey, fontSize: 17), + ), + const SizedBox( + height: 10, + ), + Container( + decoration: BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.circular(15)), + child: moreInfo(context, + title: "Node Id", info: _nodeId, showCopyButton: true)) + ], + ), + ), + Container( + margin: const EdgeInsets.all(10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "BUILD INFO", + style: TextStyle(color: Colors.grey, fontSize: 18), + ), + const SizedBox( + height: 10, + ), + Container( + decoration: BoxDecoration( + color: Colors.white, borderRadius: BorderRadius.circular(15)), + child: Column( + children: [ + moreInfo(context, title: "Version", info: _version), + moreInfo(context, + title: "Commit Hash", info: _commit, showCopyButton: true), + moreInfo(context, + title: "Branch", info: _branch, showCopyButton: kDebugMode) + ], + )) + ], + ), + ), + ], + ), + const SizedBox(height: 10) + ], + )), + ); + } +} + +Widget moreInfo(BuildContext context, + {required String title, required String info, bool showCopyButton = false}) { + return Container( + padding: const EdgeInsets.all(15), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: + const TextStyle(fontSize: 17, fontWeight: FontWeight.w400, color: Colors.black), + ), + const SizedBox(height: 7), + showCopyButton + ? SizedBox( + width: 400, + child: Text( + info, + softWrap: true, + maxLines: 4, + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w300, color: Colors.grey.shade700), + )) + : const SizedBox() + ], + ), + showCopyButton + ? GestureDetector( + onTap: () async { + showSnackBar(ScaffoldMessenger.of(context), "Copied $info"); + await Clipboard.setData(ClipboardData(text: info)); + }, + child: Icon( + Icons.copy, + size: 17, + color: tenTenOnePurple.shade800, + ), + ) + : Text( + info, + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.w300, color: Colors.grey.shade700), + ) + ], + ), + ); +} diff --git a/webapp/frontend/lib/settings/settings_screen.dart b/webapp/frontend/lib/settings/settings_screen.dart index 43fc3ce96..da0a4d74a 100644 --- a/webapp/frontend/lib/settings/settings_screen.dart +++ b/webapp/frontend/lib/settings/settings_screen.dart @@ -1,4 +1,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'; class SettingsScreen extends StatefulWidget { static const route = "/settings"; @@ -9,17 +12,62 @@ class SettingsScreen extends StatefulWidget { State createState() => _SettingsScreenState(); } -class _SettingsScreenState extends State { +class _SettingsScreenState extends State with SingleTickerProviderStateMixin { + late final _tabController = TabController(length: 3, vsync: this); + @override Widget build(BuildContext context) { - return const Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - 'Settings Screen', - ), - ], + return SizedBox( + width: 500, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + TabBar( + unselectedLabelColor: Colors.black, + labelColor: tenTenOnePurple, + tabs: const [ + Tab( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(FontAwesomeIcons.info, size: 20), + SizedBox(width: 10), + Text("App Info") + ], + )), + Tab( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.balance_outlined, size: 20), + SizedBox(width: 10), + Text("Channel") + ], + ), + ), + Tab( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(FontAwesomeIcons.seedling, size: 20), + SizedBox(width: 10), + Text("Backup") + ], + ), + ), + ], + controller: _tabController, + indicatorSize: TabBarIndicatorSize.tab, + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: const [AppInfoScreen(), Text("Channel"), Text("Backup")], + ), + ), + ], + ), ); } } diff --git a/webapp/frontend/lib/settings/settings_service.dart b/webapp/frontend/lib/settings/settings_service.dart new file mode 100644 index 000000000..c704fdb65 --- /dev/null +++ b/webapp/frontend/lib/settings/settings_service.dart @@ -0,0 +1,23 @@ +import 'package:http/http.dart' as http; + +class SettingsService { + const SettingsService(); + + Future getNodeId() async { + // TODO(holzeis): this should come from the config + const port = "3001"; + const host = "localhost"; + + try { + final response = await http.get(Uri.http('$host:$port', '/api/node')); + + if (response.statusCode == 200) { + return response.body; + } else { + return "unknown"; + } + } catch (e) { + return "unknown"; + } + } +} diff --git a/webapp/src/api.rs b/webapp/src/api.rs index a8f2a0e1a..ef812c902 100644 --- a/webapp/src/api.rs +++ b/webapp/src/api.rs @@ -37,11 +37,15 @@ where #[derive(Serialize)] pub struct Version { version: String, + commit_hash: String, + branch: String, } pub async fn version() -> Json { Json(Version { version: env!("CARGO_PKG_VERSION").to_string(), + commit_hash: env!("COMMIT_HASH").to_string(), + branch: env!("BRANCH_NAME").to_string(), }) } @@ -131,3 +135,7 @@ pub async fn send_payment(params: Json) -> Result<(), AppError> { Ok(()) } + +pub async fn get_node_id() -> impl IntoResponse { + ln_dlc::get_node_pubkey().to_string() +} \ No newline at end of file diff --git a/webapp/src/main.rs b/webapp/src/main.rs index 9752b7a09..533b8d31a 100644 --- a/webapp/src/main.rs +++ b/webapp/src/main.rs @@ -4,6 +4,7 @@ mod logger; mod subscribers; use crate::api::get_balance; +use crate::api::get_node_id; use crate::api::get_onchain_payment_history; use crate::api::get_unused_address; use crate::api::send_payment; @@ -93,6 +94,7 @@ fn using_serve_dir(subscribers: Arc) -> Router { .route("/api/newaddress", get(get_unused_address)) .route("/api/sendpayment", post(send_payment)) .route("/api/history", get(get_onchain_payment_history)) + .route("/api/node", get(get_node_id)) .route("/main.dart.js", get(main_dart_handler)) .route("/flutter.js", get(flutter_js)) .route("/index.html", get(index_handler))