Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions lib/features/account/screens/account_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:mostro/features/account/providers/backup_reminder_provider.dart'
import 'package:mostro/features/account/providers/privacy_mode_provider.dart';
import 'package:mostro/l10n/app_localizations.dart';
import 'package:mostro/shared/providers/session_provider.dart';
import 'package:mostro/src/rust/api/orders.dart' as orders_api;

/// Account screen — Route `/key_management`.
///
Expand Down Expand Up @@ -451,9 +452,27 @@ class _AccountScreenState extends ConsumerState<AccountScreen> {
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
onPressed: () async {
Navigator.pop(dialogContext);
// TODO: wire restore-session action in Phase 7.
try {
await orders_api.restartOrdersSubscription();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Order book refreshed')),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch (e) {
debugPrint('[account] refresh error: $e');
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
kDebugMode
? 'Refresh failed: $e'
: 'Refresh failed',
),
),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
},
child: const Text('Refresh'),
),
Expand Down
7 changes: 0 additions & 7 deletions lib/features/home/providers/home_order_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,6 @@ int _platformInt64ToInt(dynamic v) => v is BigInt ? v.toInt() : v as int;
// ── Order model ───────────────────────────────────────────────────────────────

/// Lightweight Dart-side order model for the UI layer.
///
/// TODO(Phase 18+): Add `OrderItem.fromInfo(OrderInfo info)` factory once
/// `flutter_rust_bridge_codegen generate` has been run and the generated
/// `OrderInfo` type is available at `package:rust/src/rust/api/orders.dart`.
/// Field mapping: id, kind.name.toLowerCase(), fiatAmount, fiatAmountMin,
/// fiatAmountMax, fiatCode, paymentMethod, premium, creatorPubkey,
/// DateTime.fromMillisecondsSinceEpoch(createdAt * 1000), expiresAt.
class OrderItem {
OrderItem({
required this.id,
Expand Down
4 changes: 2 additions & 2 deletions lib/features/order/screens/take_order_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ class _TakeOrderScreenState extends ConsumerState<TakeOrderScreen> {
const SizedBox(height: AppSpacing.sm),

// Card 5: Creator reputation — hidden in full privacy mode.
// TODO(bridge): the rate_counterpart route should also be skipped
// when privacy mode is on (controlled via the trade flow, not here).
// Rating step is skipped in trade_detail_screen.dart when privacy
// mode is active.
if (!privacyMode) ...[
_InfoCard(
color: cardBg,
Expand Down
15 changes: 12 additions & 3 deletions lib/features/trades/screens/trade_detail_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:mostro/core/app_routes.dart';
import 'package:mostro/core/app_theme.dart';
import 'package:mostro/src/rust/api/disputes.dart' as disputes_api;
import 'package:mostro/src/rust/api/orders.dart' as orders_api;
import 'package:mostro/features/account/providers/privacy_mode_provider.dart';
import 'package:mostro/features/disputes/providers/disputes_providers.dart';
import 'package:mostro/features/home/providers/home_order_providers.dart';
import 'package:mostro/features/order/providers/trade_state_provider.dart';
Expand Down Expand Up @@ -283,7 +284,10 @@ class _TradeDetailScreenState extends ConsumerState<TradeDetailScreen> {
if (confirmed != true || !context.mounted) return;
try {
await orders_api.releaseOrder(orderId: widget.orderId);
if (context.mounted) {
if (!context.mounted) return;
if (ref.read(privacyModeProvider)) {
context.go(AppRoute.home);
} else {
context.push(AppRoute.rateUserPath(widget.orderId));
}
} catch (e, st) {
Expand Down Expand Up @@ -754,8 +758,13 @@ class _TradeDetailScreenState extends ConsumerState<TradeDetailScreen> {
// ── Pending rating — RATE + CLOSE ─────────────────────────────
if (status == TradeStatus.pendingRating) ...[
FilledButton.icon(
onPressed: () =>
context.push(AppRoute.rateUserPath(widget.orderId)),
onPressed: () {
if (ref.read(privacyModeProvider)) {
context.go(AppRoute.home);
} else {
context.push(AppRoute.rateUserPath(widget.orderId));
}
},
icon: const Icon(Icons.star_outline, size: 16),
label: const Text('RATE'),
style: FilledButton.styleFrom(
Expand Down
11 changes: 11 additions & 0 deletions rust/src/api/orders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,17 @@ pub async fn subscribe_orders() {
});
}

/// Force-restart the orders subscription.
///
/// Tears down the existing subscription (if any) by resetting the guard,
/// then spawns a fresh subscription loop. Used by the UI "Refresh" action
/// so users get a real re-subscribe instead of a silent no-op.
pub async fn restart_orders_subscription() {
// Clear the guard so subscribe_orders can acquire it again.
SUBSCRIPTION_ACTIVE.store(false, Ordering::Release);
subscribe_orders().await;
}

async fn _run_order_subscription() {
let Ok(pool) = crate::api::nostr::get_pool() else {
log::error!("[orders] subscription failed: relay pool not initialized");
Expand Down
28 changes: 28 additions & 0 deletions rust/src/api/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use tokio::sync::{broadcast, RwLock};
use tokio::sync::broadcast::error::RecvError;

use crate::api::types::{AppSettings, ThemeMode};
use crate::db::Storage;

// ── SettingsStore ─────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -200,6 +201,33 @@ pub fn get_mostro_pubkey() -> String {
crate::config::active_mostro_pubkey()
}

/// Return the active Mostro node info, falling back to the compiled
/// default if none has been persisted yet.
pub async fn get_mostro_node() -> Result<crate::api::types::MostroNodeInfo> {
if let Some(db) = crate::db::app_db::db() {
if let Ok(Some(node)) = db.get_active_mostro_node().await {
return Ok(node);
}
}
Ok(crate::db::seeds::get_default_mostro_node())
}

/// Persist a new active Mostro node selection.
///
/// Also updates the in-memory pubkey override so the relay pool and
/// outgoing events use the new node immediately.
///
/// TODO(#93): After updating the pubkey, refresh relay subscriptions so
/// `subscribe_order_and_dm_feeds` re-subscribes with the new node's key.
pub async fn set_mostro_node(node: crate::api::types::MostroNodeInfo) -> Result<()> {
let pubkey = node.pubkey.clone();
if let Some(db) = crate::db::app_db::db() {
db.save_mostro_node(&node).await?;
}
crate::config::set_active_mostro_pubkey(Some(pubkey));
Ok(())
}

/// Toggle the in-memory logging flag (not persisted to disk).
///
/// When a Tokio runtime is available the update is dispatched asynchronously
Expand Down
12 changes: 12 additions & 0 deletions rust/src/db/indexeddb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ impl Storage for IndexedDbStorage {
Ok(None) // no persisted key: caller will treat absence correctly
}

async fn save_mostro_node(&self, _node: &crate::api::types::MostroNodeInfo) -> Result<()> {
log::warn!("save_mostro_node: IndexedDB backend not implemented — node selection will not survive reload");
// TODO(#93): implement IndexedDB persistence for Mostro node selection
Ok(())
}

async fn get_active_mostro_node(&self) -> Result<Option<crate::api::types::MostroNodeInfo>> {
log::warn!("get_active_mostro_node: IndexedDB backend not implemented — falling back to default");
// TODO(#93): implement IndexedDB persistence for Mostro node selection
Ok(None)
}

async fn get_trade_by_order_id(
&self,
_order_id: &str,
Expand Down
8 changes: 8 additions & 0 deletions rust/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ pub trait Storage: Send + Sync {
/// Retrieve the BIP-32 key index for `order_id`, or `None` if not found.
async fn get_trade_key(&self, order_id: &str) -> Result<Option<u32>>;

// ── Settings / Mostro node ────────────────────────────────────────────────

/// Persist a Mostro node info record, replacing any existing one.
async fn save_mostro_node(&self, node: &crate::api::types::MostroNodeInfo) -> Result<()>;

/// Return the currently active Mostro node, or `None` if none has been saved.
async fn get_active_mostro_node(&self) -> Result<Option<crate::api::types::MostroNodeInfo>>;

/// Look up a persisted trade by the order ID it is associated with.
async fn get_trade_by_order_id(
&self,
Expand Down
6 changes: 6 additions & 0 deletions rust/src/db/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@ CREATE TABLE IF NOT EXISTS queued_messages (
next_retry_at INTEGER
);

-- Generic key-value settings store (Mostro node, preferences, etc.).
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);

-- Maps order_id → BIP-32 trade key index used when taking/creating that order.
-- Persists across restarts so fiat-sent, release, and cancel can re-derive the
-- correct signing key even after the app is killed between protocol steps.
Expand Down
17 changes: 6 additions & 11 deletions rust/src/db/seeds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,13 @@ async fn seed_default_relays<S: Storage>(storage: &S) -> Result<()> {
}

/// Seed the default Mostro node if no active node is configured.
///
/// Stores the node info in the settings table. Since the `Storage` trait
/// doesn't have Mostro-node-specific methods yet, we store it as a relay
/// with a special source marker and use a separate settings key.
///
/// TODO: Add `save_mostro_node` / `get_active_mostro_node` to Storage
/// trait when the settings/about screens are implemented (Phase 16-17).
async fn seed_default_mostro_node<S: Storage>(storage: &S) -> Result<()> {
// For now, the default Mostro node info is served from the compiled
// constant via `get_default_mostro_node()`. Persistence will be added
// when the Storage trait gains Mostro-node methods.
let _ = storage;
if storage.get_active_mostro_node().await?.is_some() {
return Ok(());
}
let node = get_default_mostro_node();
storage.save_mostro_node(&node).await?;
log::info!("[seeds] default Mostro node seeded: {}", node.pubkey);
Ok(())
}

Expand Down
25 changes: 25 additions & 0 deletions rust/src/db/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,31 @@ impl Storage for SqliteStorage {
Ok(row.map(|(idx,)| idx as u32))
}

async fn save_mostro_node(&self, node: &crate::api::types::MostroNodeInfo) -> Result<()> {
let json = serde_json::to_string(node)?;
sqlx::query(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('active_mostro_node', ?)",
)
.bind(&json)
.execute(&self.pool)
.await?;
Ok(())
}

async fn get_active_mostro_node(
&self,
) -> Result<Option<crate::api::types::MostroNodeInfo>> {
let row: Option<(String,)> = sqlx::query_as(
"SELECT value FROM settings WHERE key = 'active_mostro_node'",
)
.fetch_optional(&self.pool)
.await?;
match row {
None => Ok(None),
Some((json,)) => Ok(Some(serde_json::from_str(&json)?)),
}
}

async fn get_trade_by_order_id(&self, order_id: &str) -> Result<Option<TradeInfo>> {
// The `data` column holds the full JSON-serialised TradeInfo; use
// SQLite's json_extract to filter by the nested order id without
Expand Down
Loading