From 6bdd52d50e34272299bc960d1a9001d1bcc713d8 Mon Sep 17 00:00:00 2001 From: Philipp Hoenisch Date: Fri, 9 Feb 2024 08:19:29 +0100 Subject: [PATCH 1/3] chore: remove unnecessary log message This removes these messages (even though they are only on trace). `Received event: Event.serviceHealthUpdate(field0: Instance of 'ServiceUpdate')` Imho they don't add much value. --- mobile/lib/common/service_status_notifier.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/lib/common/service_status_notifier.dart b/mobile/lib/common/service_status_notifier.dart index a6a32448b..4c9103c16 100644 --- a/mobile/lib/common/service_status_notifier.dart +++ b/mobile/lib/common/service_status_notifier.dart @@ -20,7 +20,6 @@ class ServiceStatusNotifier extends ChangeNotifier implements Subscriber { @override void notify(bridge.Event event) { if (event is bridge.Event_ServiceHealthUpdate) { - logger.t("Received event: ${event.toString()}"); var update = event.field0; services[update.service] = update.status; From da0c2913222cbf612e0a0b82b16b00b2ab377281 Mon Sep 17 00:00:00 2001 From: Philipp Hoenisch Date: Fri, 9 Feb 2024 08:38:57 +0100 Subject: [PATCH 2/3] chore: remove unnecessary log message This is just spam in the logs. --- mobile/native/src/orderbook.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/native/src/orderbook.rs b/mobile/native/src/orderbook.rs index 74b859347..e2e658855 100644 --- a/mobile/native/src/orderbook.rs +++ b/mobile/native/src/orderbook.rs @@ -205,7 +205,7 @@ async fn handle_orderbook_message( let msg = serde_json::from_str::(&msg).context("Could not deserialize orderbook message")?; - tracing::debug!(%msg, "New orderbook message"); + tracing::trace!(%msg, "New orderbook message"); match msg { Message::Authenticated(lsp_config) => { From 1db3ebb89e293fb569e0377eaed35e01a30aff9a Mon Sep 17 00:00:00 2001 From: Philipp Hoenisch Date: Fri, 9 Feb 2024 08:52:14 +0100 Subject: [PATCH 3/3] feat: allow user to change their contact details Also, users can register with Nostr, email or telegram. Note there is no input validation so the user can put anything :) --- CHANGELOG.md | 1 + .../down.sql | 4 + .../up.sql | 4 + coordinator/src/admin.rs | 4 +- coordinator/src/db/user.rs | 27 +-- .../src/orderbook/tests/registration_test.rs | 5 +- coordinator/src/routes.rs | 43 ++++- coordinator/src/schema.rs | 3 +- crates/commons/src/lib.rs | 9 +- mobile/lib/common/routes.dart | 9 + .../lib/common/settings/settings_screen.dart | 12 +- mobile/lib/common/settings/user_screen.dart | 169 ++++++++++++++++++ .../features/welcome/seed_import_screen.dart | 2 +- .../lib/features/welcome/welcome_screen.dart | 32 ++-- mobile/lib/util/preferences.dart | 16 +- mobile/native/src/api.rs | 25 ++- mobile/native/src/trade/users/mod.rs | 42 ++++- 17 files changed, 344 insertions(+), 63 deletions(-) create mode 100644 coordinator/migrations/2024-02-08-105459_change_user_email_to_contact_detail/down.sql create mode 100644 coordinator/migrations/2024-02-08-105459_change_user_email_to_contact_detail/up.sql create mode 100644 mobile/lib/common/settings/user_screen.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index a9bb3369b..a13c09445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix(mobile): Make disclaimer screen usable on smaller devices. - Feat(mobile): Allow to manually increase for how many derived addresses to sync for. This might be needed if you recovered a wallet and do not see any on-chain funds. +- Feat(mobile): Allow users to register with Email, Nostr and Telegram handle and let them change these details later on. ## [1.8.6] - 2024-02-08 diff --git a/coordinator/migrations/2024-02-08-105459_change_user_email_to_contact_detail/down.sql b/coordinator/migrations/2024-02-08-105459_change_user_email_to_contact_detail/down.sql new file mode 100644 index 000000000..4c072881f --- /dev/null +++ b/coordinator/migrations/2024-02-08-105459_change_user_email_to_contact_detail/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE + users RENAME COLUMN contact TO email; +ALTER TABLE + users ADD COLUMN nostr TEXT; diff --git a/coordinator/migrations/2024-02-08-105459_change_user_email_to_contact_detail/up.sql b/coordinator/migrations/2024-02-08-105459_change_user_email_to_contact_detail/up.sql new file mode 100644 index 000000000..a710535a7 --- /dev/null +++ b/coordinator/migrations/2024-02-08-105459_change_user_email_to_contact_detail/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE + users RENAME COLUMN email TO contact; +ALTER TABLE + users DROP COLUMN nostr; diff --git a/coordinator/src/admin.rs b/coordinator/src/admin.rs index d7e24daa5..8dd4de7fa 100644 --- a/coordinator/src/admin.rs +++ b/coordinator/src/admin.rs @@ -269,7 +269,7 @@ pub async fn list_channels( .map(|channel| { let user_email = match db::user::by_id(&mut conn, channel.counterparty.node_id.to_string()) { - Ok(Some(user)) => user.email, + Ok(Some(user)) => user.contact, _ => "unknown".to_string(), }; let balances = if let Some(funding_txo) = channel.funding_txo { @@ -338,7 +338,7 @@ pub async fn list_dlc_channels( .map(|dlc_channel| { let (email, registration_timestamp) = match db::user::by_id(&mut conn, dlc_channel.get_counter_party_id().to_string()) { - Ok(Some(user)) => (user.email, Some(user.timestamp)), + Ok(Some(user)) => (user.contact, Some(user.timestamp)), _ => ("unknown".to_string(), None), }; diff --git a/coordinator/src/db/user.rs b/coordinator/src/db/user.rs index 238cc247a..1e99d9e85 100644 --- a/coordinator/src/db/user.rs +++ b/coordinator/src/db/user.rs @@ -15,8 +15,7 @@ pub struct User { #[diesel(deserialize_as = i32)] pub id: Option, pub pubkey: String, - pub email: String, - pub nostr: String, + pub contact: String, pub timestamp: OffsetDateTime, pub fcm_token: String, pub last_login: OffsetDateTime, @@ -27,8 +26,7 @@ impl From for User { User { id: None, pubkey: value.pubkey.to_string(), - email: value.email.unwrap_or("".to_owned()), - nostr: value.nostr.unwrap_or("".to_owned()), + contact: value.contact.unwrap_or("".to_owned()), timestamp: OffsetDateTime::now_utc(), fcm_token: "".to_owned(), last_login: OffsetDateTime::now_utc(), @@ -48,10 +46,10 @@ pub fn by_id(conn: &mut PgConnection, id: String) -> QueryResult> { Ok(x) } -pub fn upsert_email( +pub fn upsert_user( conn: &mut PgConnection, trader_id: PublicKey, - email: String, + contact: String, ) -> QueryResult { let timestamp = OffsetDateTime::now_utc(); @@ -59,15 +57,14 @@ pub fn upsert_email( .values(User { id: None, pubkey: trader_id.to_string(), - email: email.clone(), - nostr: "".to_owned(), + contact: contact.clone(), timestamp, fcm_token: "".to_owned(), last_login: timestamp, }) .on_conflict(schema::users::pubkey) .do_update() - .set((users::email.eq(&email), users::last_login.eq(timestamp))) + .set((users::contact.eq(&contact), users::last_login.eq(timestamp))) .get_result(conn)?; Ok(user) } @@ -79,8 +76,7 @@ pub fn login_user(conn: &mut PgConnection, trader_id: PublicKey, token: String) .values(User { id: None, pubkey: trader_id.to_string(), - email: "".to_owned(), - nostr: "".to_owned(), + contact: "".to_owned(), timestamp: OffsetDateTime::now_utc(), fcm_token: token.clone(), last_login, @@ -100,3 +96,12 @@ pub fn login_user(conn: &mut PgConnection, trader_id: PublicKey, token: String) } Ok(()) } + +pub fn get_user(conn: &mut PgConnection, trader_id: PublicKey) -> Result> { + let maybe_user = users::table + .filter(users::pubkey.eq(trader_id.to_string())) + .first(conn) + .optional()?; + + Ok(maybe_user) +} diff --git a/coordinator/src/orderbook/tests/registration_test.rs b/coordinator/src/orderbook/tests/registration_test.rs index 9c5dfc072..192fc6f8d 100644 --- a/coordinator/src/orderbook/tests/registration_test.rs +++ b/coordinator/src/orderbook/tests/registration_test.rs @@ -22,7 +22,7 @@ async fn registered_user_is_stored_in_db() { let dummy_email = "dummy@user.com".to_string(); let fcm_token = "just_a_token".to_string(); - let user = user::upsert_email(&mut conn, dummy_pubkey, dummy_email.clone()).unwrap(); + let user = user::upsert_user(&mut conn, dummy_pubkey, dummy_email.clone()).unwrap(); assert!(user.id.is_some(), "Id should be filled in by diesel"); user::login_user(&mut conn, dummy_pubkey, fcm_token.clone()).unwrap(); @@ -30,9 +30,8 @@ async fn registered_user_is_stored_in_db() { assert_eq!(users.len(), 1); // We started without the id, so we can't compare the whole user. assert_eq!(users.first().unwrap().pubkey, dummy_pubkey.to_string()); - assert_eq!(users.first().unwrap().email, dummy_email); + assert_eq!(users.first().unwrap().contact, dummy_email); assert_eq!(users.first().unwrap().fcm_token, fcm_token); - assert!(users.first().unwrap().nostr.is_empty()); } fn dummy_public_key() -> PublicKey { diff --git a/coordinator/src/routes.rs b/coordinator/src/routes.rs index 7fddb0624..cd20f5df1 100644 --- a/coordinator/src/routes.rs +++ b/coordinator/src/routes.rs @@ -21,6 +21,7 @@ use crate::collaborative_revert::confirm_legacy_collaborative_revert; use crate::db; use crate::db::liquidity::LiquidityRequestLog; use crate::db::user; +use crate::db::user::User; use crate::is_liquidity_sufficient; use crate::leaderboard::generate_leader_board; use crate::leaderboard::LeaderBoard; @@ -160,7 +161,11 @@ pub fn router( .route("/api/orderbook/websocket", get(websocket_handler)) .route("/api/trade", post(post_trade)) .route("/api/rollover/:dlc_channel_id", post(rollover)) + // Deprecated: we just keep it for backwards compatbility as otherwise old apps won't + // pass registration .route("/api/register", post(post_register)) + .route("/api/users", post(post_register)) + .route("/api/users/:trader_pubkey", get(get_user)) .route("/api/admin/wallet/balance", get(get_balance)) .route("/api/admin/wallet/utxos", get(get_utxos)) .route("/api/admin/channels", get(list_channels).post(open_channel)) @@ -438,8 +443,8 @@ pub async fn post_register( .get() .map_err(|e| AppError::InternalServerError(format!("Could not get connection: {e:#}")))?; - if let Some(email) = register_params.email { - user::upsert_email(&mut conn, register_params.pubkey, email) + if let Some(contact) = register_params.contact { + user::upsert_user(&mut conn, register_params.pubkey, contact) .map_err(|e| AppError::InternalServerError(format!("Could not upsert user: {e:#}")))?; } else { tracing::warn!(trader_id=%register_params.pubkey, "Did not receive an email during registration"); @@ -448,6 +453,40 @@ pub async fn post_register( Ok(()) } +impl TryFrom for commons::User { + type Error = AppError; + fn try_from(value: User) -> Result { + Ok(commons::User { + pubkey: PublicKey::from_str(&value.pubkey).map_err(|_| { + AppError::InternalServerError("Could not parse user pubkey".to_string()) + })?, + contact: Some(value.contact).filter(|s| !s.is_empty()), + }) + } +} + +#[instrument(skip_all, err(Debug))] +pub async fn get_user( + State(state): State>, + Path(trader_pubkey): Path, +) -> Result, AppError> { + let mut conn = state + .pool + .get() + .map_err(|e| AppError::InternalServerError(format!("Could not get connection: {e:#}")))?; + + let trader_pubkey = PublicKey::from_str(trader_pubkey.as_str()) + .map_err(|_| AppError::BadRequest("Invalid trader id provided".to_string()))?; + + let option = user::get_user(&mut conn, trader_pubkey) + .map_err(|e| AppError::InternalServerError(format!("Could not load users: {e:#}")))?; + + match option { + None => Err(AppError::NoMatchFound("No user found".to_string())), + Some(user) => Ok(Json(user.try_into()?)), + } +} + async fn get_settings(State(state): State>) -> impl IntoResponse { let settings = state.settings.read().await; serde_json::to_string(&*settings).expect("to be able to serialise settings") diff --git a/coordinator/src/schema.rs b/coordinator/src/schema.rs index aee14a520..804668661 100644 --- a/coordinator/src/schema.rs +++ b/coordinator/src/schema.rs @@ -328,8 +328,7 @@ diesel::table! { users (id) { id -> Int4, pubkey -> Text, - email -> Text, - nostr -> Text, + contact -> Text, timestamp -> Timestamptz, fcm_token -> Text, last_login -> Timestamptz, diff --git a/crates/commons/src/lib.rs b/crates/commons/src/lib.rs index 97eaedfa0..cc7315924 100644 --- a/crates/commons/src/lib.rs +++ b/crates/commons/src/lib.rs @@ -37,6 +37,11 @@ pub const AUTH_SIGN_MESSAGE: &[u8; 19] = b"Hello it's me Mario"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegisterParams { pub pubkey: PublicKey, - pub email: Option, - pub nostr: Option, + pub contact: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct User { + pub pubkey: PublicKey, + pub contact: Option, } diff --git a/mobile/lib/common/routes.dart b/mobile/lib/common/routes.dart index 0343e93c7..d6c3debb7 100644 --- a/mobile/lib/common/routes.dart +++ b/mobile/lib/common/routes.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get_10101/common/global_keys.dart'; import 'package:get_10101/common/settings/channel_screen.dart'; import 'package:get_10101/common/settings/emergency_kit_screen.dart'; +import 'package:get_10101/common/settings/user_screen.dart'; import 'package:get_10101/common/settings/wallet_settings.dart'; import 'package:get_10101/common/status_screen.dart'; import 'package:get_10101/features/wallet/domain/destination.dart'; @@ -110,6 +111,14 @@ GoRouter createRoutes() { return const WalletSettings(); }, ), + GoRoute( + path: UserSettings.subRouteName, + // Use root navigator so the screen overlays the application shell + parentNavigatorKey: rootNavigatorKey, + builder: (BuildContext context, GoRouterState state) { + return const UserSettings(); + }, + ), GoRoute( path: ChannelScreen.subRouteName, // Use root navigator so the screen overlays the application shell diff --git a/mobile/lib/common/settings/settings_screen.dart b/mobile/lib/common/settings/settings_screen.dart index 6b922f1ed..eb25f88aa 100644 --- a/mobile/lib/common/settings/settings_screen.dart +++ b/mobile/lib/common/settings/settings_screen.dart @@ -12,6 +12,7 @@ import 'package:get_10101/common/settings/emergency_kit_screen.dart'; import 'package:get_10101/common/settings/force_close_screen.dart'; import 'package:get_10101/common/settings/open_telegram.dart'; import 'package:get_10101/common/settings/share_logs_screen.dart'; +import 'package:get_10101/common/settings/user_screen.dart'; import 'package:get_10101/common/settings/wallet_settings.dart'; import 'package:get_10101/common/snack_bar.dart'; import 'package:get_10101/common/status_screen.dart'; @@ -176,7 +177,16 @@ class _SettingsScreenState extends State { icon: Icons.wallet_outlined, title: "Wallet Settings", callBackFunc: () => - GoRouter.of(context).push(WalletSettings.route)) + GoRouter.of(context).push(WalletSettings.route)), + const Divider( + height: 0.5, + thickness: 0.8, + indent: 55, + ), + SettingsClickable( + icon: FontAwesomeIcons.userAstronaut, + title: "User Settings", + callBackFunc: () => GoRouter.of(context).push(UserSettings.route)) ], ), ) diff --git a/mobile/lib/common/settings/user_screen.dart b/mobile/lib/common/settings/user_screen.dart new file mode 100644 index 000000000..7c93b4b06 --- /dev/null +++ b/mobile/lib/common/settings/user_screen.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:get_10101/common/settings/settings_screen.dart'; +import 'package:get_10101/common/snack_bar.dart'; +import 'package:get_10101/logger/logger.dart'; +import 'package:go_router/go_router.dart'; +import 'package:get_10101/ffi.dart' as rust; + +class UserSettings extends StatefulWidget { + static const route = "${SettingsScreen.route}/$subRouteName"; + static const subRouteName = "user"; + + const UserSettings({super.key}); + + @override + State createState() => _UserSettingsState(); +} + +class _UserSettingsState extends State { + var contactFieldController = TextEditingController(); + bool contactFieldEnabled = false; + + @override + void initState() { + super.initState(); + rust.api + .getUserDetails() + .then((user) => contactFieldController.text = user.contact != null ? user.contact! : ""); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 20, left: 10, right: 10), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Stack( + children: [ + GestureDetector( + child: Container( + alignment: AlignmentDirectional.topStart, + decoration: BoxDecoration( + color: Colors.transparent, borderRadius: BorderRadius.circular(10)), + width: 70, + child: const Icon( + Icons.arrow_back_ios_new_rounded, + size: 22, + )), + onTap: () { + GoRouter.of(context).pop(); + }, + ), + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "User Settings", + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 20), + ), + ], + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 20, + ), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Contact details \n", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + RichText( + text: const TextSpan( + text: + '10101 will use these details to reach out to you in case of problems in the app. \n\n', + style: TextStyle(fontSize: 16, color: Colors.black), + children: [ + TextSpan( + text: 'This can be a ', + ), + TextSpan( + text: 'Nostr Pubkey', style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan( + text: ', a ', + ), + TextSpan( + text: 'Telegram handle ', + style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan( + text: 'or an ', + ), + TextSpan( + text: 'email address.', style: TextStyle(fontWeight: FontWeight.bold)), + TextSpan( + text: + "\n\nIf you want to delete your contact details. Simply remove the details below.") + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + child: Stack( + alignment: Alignment.centerRight, + children: [ + TextFormField( + enabled: contactFieldEnabled, + controller: contactFieldController, + decoration: const InputDecoration( + border: UnderlineInputBorder(), + labelText: 'Contact details', + ), + ), + Visibility( + replacement: IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + setState(() { + contactFieldEnabled = true; + }); + }, + ), + visible: contactFieldEnabled, + child: IconButton( + icon: const Icon( + Icons.check, + color: Colors.green, + ), + onPressed: () async { + final messenger = ScaffoldMessenger.of(context); + try { + var newContact = contactFieldController.value.text; + logger.i("Successfully updated to $newContact"); + await rust.api.registerBeta(contact: newContact); + showSnackBar(messenger, "Successfully updated to $newContact"); + } catch (exception) { + showSnackBar( + messenger, "Error when updating contact details $exception"); + } finally { + setState(() { + contactFieldEnabled = false; + }); + } + }, + ), + ) + ], + ), + ) + ], + ), + ), + ], + ), + )), + ); + } +} diff --git a/mobile/lib/features/welcome/seed_import_screen.dart b/mobile/lib/features/welcome/seed_import_screen.dart index 1b8c48aff..d15d0b653 100644 --- a/mobile/lib/features/welcome/seed_import_screen.dart +++ b/mobile/lib/features/welcome/seed_import_screen.dart @@ -148,7 +148,7 @@ class SeedPhraseImporterState extends State { ScaffoldMessenger.of(context), "Failed to import from seed. $error")); // TODO(holzeis): Backup preferences and restore email from there. - Preferences.instance.setEmailAddress("restored"); + Preferences.instance.setContactDetails("restored"); GoRouter.of(context).go(LoadingScreen.route, extra: restore); }).catchError((e) { logger.e("Error restoring from seed phrase: $e"); diff --git a/mobile/lib/features/welcome/welcome_screen.dart b/mobile/lib/features/welcome/welcome_screen.dart index b032fc2b3..5f2e100dc 100644 --- a/mobile/lib/features/welcome/welcome_screen.dart +++ b/mobile/lib/features/welcome/welcome_screen.dart @@ -22,17 +22,10 @@ class WelcomeScreen extends StatefulWidget { class _WelcomeScreenState extends State { final GlobalKey _formKey = GlobalKey(); - String _email = ""; + String _contact = ""; bool _betaDisclaimer = false; bool _loseDisclaimer = false; - /// TODO Convert to a flutter package that checks the email domain validity - /// (MX record, etc.) - bool isEmailValid(String email) { - return RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+") - .hasMatch(email); - } - @override Widget build(BuildContext context) { return AnnotatedRegion( @@ -126,7 +119,7 @@ class _WelcomeScreenState extends State { key: _formKey, child: TextFormField( keyboardType: TextInputType.emailAddress, - initialValue: _email, + initialValue: _contact, decoration: InputDecoration( border: OutlineInputBorder(borderRadius: BorderRadius.circular(10.0)), @@ -136,7 +129,7 @@ class _WelcomeScreenState extends State { color: tenTenOnePurple.shade300.withOpacity(0.2))), filled: true, fillColor: tenTenOnePurple.shade300.withOpacity(0.2), - labelText: 'Email (optional)', + labelText: 'Nostr Pubkey, Telegram or Email (optional)', labelStyle: const TextStyle(color: Colors.black87, fontSize: 14), hintText: 'Let us know how to reach you'), validator: (value) { @@ -144,13 +137,14 @@ class _WelcomeScreenState extends State { return null; } - if (!isEmailValid(value)) { - return 'Please enter a valid email address'; + if (value.length > 64) { + return 'Contact details are too long.'; } + return null; }, onSaved: (value) { - _email = value ?? ""; + _contact = value ?? ""; }, ), ), @@ -209,19 +203,19 @@ class _WelcomeScreenState extends State { Future setupWallet() async { var seedPath = await getSeedFilePath(); - await Preferences.instance.setEmailAddress(_email); - logger.i("Successfully stored the email address $_email ."); + await Preferences.instance.setContactDetails(_contact); + logger.i("Successfully stored the contact: $_contact ."); await api.initNewMnemonic(targetSeedFilePath: seedPath); - await api.registerBeta(email: _email); + await api.registerBeta(contact: _contact); } @override void initState() { super.initState(); - Preferences.instance.getEmailAddress().then((value) => setState(() { - _email = value; - logger.i("retrieved stored email from the preferences: $_email."); + Preferences.instance.getContactDetails().then((value) => setState(() { + _contact = value; + logger.i("retrieved stored contact from the preferences: $_contact."); })); } } diff --git a/mobile/lib/util/preferences.dart b/mobile/lib/util/preferences.dart index bcdb3de8a..b591777fd 100644 --- a/mobile/lib/util/preferences.dart +++ b/mobile/lib/util/preferences.dart @@ -8,7 +8,7 @@ class Preferences { static final Preferences instance = Preferences._privateConstructor(); - static const emailAddress = "emailAddress"; + static const contactDetails = "emailAddress"; static const openPosition = "openPosition"; static const fullBackup = "fullBackup"; static const logLevelTrace = "logLevelTrace"; @@ -53,18 +53,18 @@ class Preferences { preferences.remove(openPosition); } - Future setEmailAddress(String value) async { + Future setContactDetails(String value) async { SharedPreferences preferences = await SharedPreferences.getInstance(); - return preferences.setString(emailAddress, value); + return preferences.setString(contactDetails, value); } - Future getEmailAddress() async { + Future getContactDetails() async { SharedPreferences preferences = await SharedPreferences.getInstance(); - return preferences.getString(emailAddress) ?? ""; + return preferences.getString(contactDetails) ?? ""; } - Future hasEmailAddress() async { - var email = await getEmailAddress(); - return email.isNotEmpty; + Future hasContactDetails() async { + var contact = await getContactDetails(); + return contact.isNotEmpty; } } diff --git a/mobile/native/src/api.rs b/mobile/native/src/api.rs index a96223e9e..6b0ecff2e 100644 --- a/mobile/native/src/api.rs +++ b/mobile/native/src/api.rs @@ -762,10 +762,29 @@ pub fn init_new_mnemonic(target_seed_file_path: String) -> Result<()> { ln_dlc::init_new_mnemonic(file_path.as_path()) } -/// Enroll a user in the beta program +/// Enroll or update a user in the beta program #[tokio::main(flavor = "current_thread")] -pub async fn register_beta(email: String) -> Result<()> { - users::register_beta(email).await +pub async fn register_beta(contact: String) -> Result<()> { + users::register_beta(contact).await +} + +pub struct User { + pub pubkey: String, + pub contact: Option, +} + +impl From for User { + fn from(value: commons::User) -> Self { + User { + pubkey: value.pubkey.to_string(), + contact: value.contact, + } + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_user_details() -> Result { + users::get_user_details().await.map(|user| user.into()) } pub enum Destination { diff --git a/mobile/native/src/trade/users/mod.rs b/mobile/native/src/trade/users/mod.rs index 4e204afb3..e14c94ad8 100644 --- a/mobile/native/src/trade/users/mod.rs +++ b/mobile/native/src/trade/users/mod.rs @@ -5,27 +5,31 @@ use anyhow::anyhow; use anyhow::Context; use anyhow::Result; use commons::RegisterParams; +use commons::User; /// Enroll the user in the beta program -pub async fn register_beta(email: String) -> Result<()> { +pub async fn register_beta(contact: String) -> Result<()> { let register = RegisterParams { pubkey: ln_dlc::get_node_pubkey(), - email: Some(email), - nostr: None, + contact: Some(contact), }; + tracing::debug!( + pubkey = register.pubkey.to_string(), + contact = register.contact, + "Registering user" + ); + let client = reqwest_client(); let response = client - .post(format!( - "http://{}/api/register", - config::get_http_endpoint() - )) + .post(format!("http://{}/api/users", config::get_http_endpoint())) .json(®ister) .send() .await .context("Failed to register beta program with coordinator")?; - if !response.status().is_success() { + let status_code = response.status(); + if !status_code.is_success() { let response_text = match response.text().await { Ok(text) => text, Err(err) => { @@ -33,9 +37,29 @@ pub async fn register_beta(email: String) -> Result<()> { } }; return Err(anyhow!( - "Could not register email with coordinator: {response_text}" + "Could not register with coordinator: HTTP${status_code}: {response_text}" )); } tracing::info!("Registered into beta program successfully"); Ok(()) } + +/// Retrieve latest user details +pub async fn get_user_details() -> Result { + let key = ln_dlc::get_node_pubkey(); + + let client = reqwest_client(); + let response = client + .get(format!( + "http://{}/api/users/{}", + config::get_http_endpoint(), + key + )) + .send() + .await + .context("Failed to retrieve user details")?; + + let user = response.json::().await?; + + Ok(user) +}