[NWC] Phase 2: Wallet connection management UI#463
Conversation
- Add NwcStorage for secure connection persistence (FlutterSecureStorage) - Add NwcNotifier (StateNotifier) for wallet state management - Add WalletSettingsScreen with connected/disconnected states - Add ConnectWalletScreen with URI paste and validation - Add WalletStatusCard and WalletBalanceWidget - Add wallet routes (/wallet_settings, /connect_wallet) - Add wallet card to Settings screen - Add localization strings (EN/ES/IT) - Add unit tests for storage and provider - Add NWC_PHASE2_IMPLEMENTATION.md Closes #457
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds Nostr Wallet Connect (NWC) Phase 2: secure storage for connection URIs, a Riverpod NWC provider with auto-reconnect and lifecycle handling, connect/settings screens and widgets, routing entries, localization, client/crypto updates, documentation, and unit tests. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant ConnectScreen as ConnectWalletScreen
participant Provider as nwcProvider
participant Notifier as NwcNotifier
participant Storage as NwcStorage
participant Secure as FlutterSecureStorage
participant Settings as WalletSettingsScreen
User->>ConnectScreen: Paste URI & tap Connect
ConnectScreen->>ConnectScreen: Validate URI (NwcConnection.fromUri)
alt Invalid
ConnectScreen->>User: Show validation error
else Valid
ConnectScreen->>Provider: notifier.connect(uri)
Provider->>Notifier: connect(uri, persist:true)
Notifier->>Notifier: Build NwcClient & perform get_info/get_balance
alt Success
Notifier->>Storage: saveConnection(uri)
Storage->>Secure: write("nwc_connection_uri", uri)
Notifier->>Provider: update state -> connected
Provider->>ConnectScreen: notify listeners
ConnectScreen->>Settings: navigate /wallet_settings
else Error
Notifier->>Provider: update state with error
Provider->>ConnectScreen: notify listeners
ConnectScreen->>User: display error
end
end
sequenceDiagram
participant User
participant Settings as WalletSettingsScreen
participant Provider as nwcProvider
participant Notifier as NwcNotifier
participant Storage as NwcStorage
participant Secure as FlutterSecureStorage
User->>Settings: Open wallet settings
Settings->>Provider: read state
Provider->>Settings: return current state & balance
User->>Settings: Tap Disconnect
Settings->>User: show confirmation dialog
User->>Settings: Confirm
Settings->>Provider: notifier.disconnect()
Provider->>Notifier: disconnect()
Notifier->>Notifier: teardown NwcClient & cancel subscriptions
Notifier->>Storage: deleteConnection()
Storage->>Secure: delete("nwc_connection_uri")
Notifier->>Provider: update state -> disconnected
Provider->>Settings: notify listeners
Settings->>User: show disconnected view
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@lib/features/wallet/providers/nwc_provider.dart`:
- Around line 28-30: The balanceSats getter currently rounds when converting
balanceMsats to satoshis which can overstate the balance; update the balanceSats
getter to truncate using integer division (use Dart's integer division operator)
instead of (balanceMsats! / 1000).round(), handling the nullable balanceMsats
correctly so it returns null when balanceMsats is null and returns balanceMsats
~/ 1000 when present.
- Around line 117-153: The persistence calls can throw and currently can push
the provider into an error state or prevent resetting state; update connect()
and disconnect() so storage failures are caught and only logged without altering
connection state: in connect(), move or wrap the call to
_storage.saveConnection(uri) in a try/catch after you set state =
NwcState(status: NwcStatus.connected, ...), catching any exception and calling
logger.e(...) (include the error) but do not change state or rethrow; in
disconnect(), ensure _client = null and state = const NwcState(status:
NwcStatus.disconnected) always happen, and wrap await
_storage.deleteConnection() in a try/catch that logs failures via logger.e(...)
so delete errors don’t prevent clearing _client or updating state.
In `@lib/features/wallet/widgets/wallet_balance_widget.dart`:
- Around line 63-65: Replace the hardcoded balance display strings in
wallet_balance_widget.dart — the ternary that currently renders '⚡
${_formatBalance(balanceSats!)} sats' and '⚡ -- sats' — with localized strings
from S.of(context)!; use a localized suffix key (e.g.
S.of(context)!.walletBalanceSuffix) appended to _formatBalance(balanceSats!) and
a localized placeholder key (e.g. S.of(context)!.walletBalancePlaceholder) for
the null case, and add the corresponding keys to the ARB/localization files if
they don’t already exist so the UI renders correctly in all locales.
In `@lib/features/wallet/widgets/wallet_status_card.dart`:
- Around line 100-105: The balance suffix "sats" is hardcoded in
WalletStatusCard; update the user-facing string to use localization instead.
Replace the literal in the Text widget that constructs '⚡
${_formatSats(nwcState.balanceSats!)} sats' with a localized string from
S.of(context) (e.g., S.of(context)!.satsSuffix or similar), ensuring the
formatted amount still uses _formatSats and the localized suffix is appended in
WalletStatusCard where nwcState.balanceSats is rendered.
🧹 Nitpick comments (3)
test/features/wallet/providers/nwc_provider_test.dart (1)
4-82: Consider addingNwcNotifiertests for connect/disconnect/refresh flows.The current tests thoroughly validate
NwcStatebehavior (defaults, conversion, copyWith, equality), which is good. However, theNwcNotifierlifecycle (connect, disconnect, auto-reconnect, refreshBalance) is untested. These are the most important behavioral paths and where bugs are most likely to surface.Would you like me to open an issue to track adding
NwcNotifierunit tests with mockedNwcStorageandNwcClient?lib/features/wallet/screens/connect_wallet_screen.dart (2)
139-141: Hardcoded hint text should be a localized string.The hint
'nostr+walletconnect://...'is user-facing text. Even if it's a technical protocol prefix unlikely to be translated, the coding guidelines require all user-facing strings to go through ARB files viaS.of(context)!.As per coding guidelines: "Replace all hardcoded user-facing text with localized strings using
S.of(context)!.keyName."
215-236: Empty URI error reuses the field label, which may confuse users.When the URI field is empty,
_validationErroris set toS.of(context)!.pasteNwcUri(line 218), which is likely the same text as the field header (line 101). Consider adding a distinct localization key likenwcUriRequiredfor a clearer error message (e.g., "Please enter an NWC URI").
| balanceSats != null | ||
| ? '⚡ ${_formatBalance(balanceSats!)} sats' | ||
| : '⚡ -- sats', |
There was a problem hiding this comment.
Localize the balance text and placeholder.
Use localized keys for the suffix and the placeholder value so it renders correctly in all locales.
💬 Suggested change
- balanceSats != null
- ? '⚡ ${_formatBalance(balanceSats!)} sats'
- : '⚡ -- sats',
+ balanceSats != null
+ ? '⚡ ${_formatBalance(balanceSats!)}${S.of(context)!.sats}'
+ : '⚡ ${S.of(context)!.notAvailableShort}${S.of(context)!.sats}',As per coding guidelines, replace all hardcoded user-facing text with localized strings using S.of(context)!.keyName.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| balanceSats != null | |
| ? '⚡ ${_formatBalance(balanceSats!)} sats' | |
| : '⚡ -- sats', | |
| balanceSats != null | |
| ? '⚡ ${_formatBalance(balanceSats!)}${S.of(context)!.sats}' | |
| : '⚡ ${S.of(context)!.notAvailableShort}${S.of(context)!.sats}', |
🤖 Prompt for AI Agents
In `@lib/features/wallet/widgets/wallet_balance_widget.dart` around lines 63 - 65,
Replace the hardcoded balance display strings in wallet_balance_widget.dart —
the ternary that currently renders '⚡ ${_formatBalance(balanceSats!)} sats' and
'⚡ -- sats' — with localized strings from S.of(context)!; use a localized suffix
key (e.g. S.of(context)!.walletBalanceSuffix) appended to
_formatBalance(balanceSats!) and a localized placeholder key (e.g.
S.of(context)!.walletBalancePlaceholder) for the null case, and add the
corresponding keys to the ARB/localization files if they don’t already exist so
the UI renders correctly in all locales.
There was a problem hiding this comment.
Valid suggestion. The sats suffix is already defined as a localization key in the .arb files. However, this is consistent with the rest of the app where sats is used as-is (it's a universal Bitcoin term, not typically translated). Will address in a dedicated i18n cleanup PR if needed.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if 'sats' key exists in ARB files
echo "=== Checking for 'sats' key in ARB files ==="
rg -n '"sats"' lib/l10n/*.arb
echo -e "\n=== Checking for hardcoded 'sats' usage in Dart files ==="
rg -n "sats'" --type=dart -C2 | head -50
Length of output: 1946
@mostronator, thanks for the context! I can confirm that the sats key is indeed defined in all three ARB files (with value " sats"), and the hardcoded usage here is consistent with other parts of the codebase, such as in background_notification_service.dart.
Your approach to consolidate this in a dedicated i18n cleanup PR makes sense. Feel free to address it when you tackle the broader localization consistency work.
🧠 Learnings used
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-12T19:14:24.080Z
Learning: Applies to lib/shared/widgets/dynamic_countdown_widget.dart : Use `S.of(context)!.timeLeftLabel()` for internationalized countdown display in DynamicCountdownWidget.
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-12T19:14:24.080Z
Learning: Applies to **/*.dart : Replace all hardcoded user-facing text with localized strings using `S.of(context)!.keyName`.
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-11T16:15:59.891Z
Learning: Applies to **/*.{dart,arb} : Localize all user-facing strings via ARB files and access them with `S.of(context)` rather than hard-coded string literals
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-12T19:14:24.080Z
Learning: Applies to lib/l10n/*.arb : Always use localized strings by adding keys to all ARB files (intl_en.arb, intl_es.arb, intl_it.arb) for English, Spanish, and Italian support.
Learnt from: CR
Repo: MostroP2P/mobile PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-12T19:14:24.080Z
Learning: Maintain zero `flutter analyze` issues by resolving deprecation warnings, BuildContext async gaps, unused imports, and optimizing with const constructors.
Learnt from: grunch
Repo: MostroP2P/mobile PR: 461
File: lib/services/nwc/nwc_client.dart:112-125
Timestamp: 2026-02-13T18:26:17.063Z
Learning: In lib/services/nwc/nwc_client.dart, the disconnect() method intentionally does NOT close relay WebSocket connections because Nostr.instance is a shared app-wide singleton. Calling disconnectFromRelays() would kill all app relay connections (including Mostro trading relays), not just NWC wallet relays. The disconnect() method correctly cleans up subscriptions and sends CLOSE frames. This is documented as a known limitation until NWC gets its own dedicated Nostr instance in Phase 2.
- Use integer division (~/) instead of round() for msats→sats conversion to avoid overstating balance by 1 sat - Wrap storage.saveConnection/deleteConnection in try/catch so persistence failures don't break the active connection state - Localize 'sats' suffix in WalletBalanceWidget and WalletStatusCard using S.of(context)!.sats instead of hardcoded string
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@lib/features/wallet/providers/nwc_provider.dart`:
- Around line 143-148: The catch-all block currently sets state =
NwcState(status: NwcStatus.error, errorMessage: e.toString()), which can leak
raw exception details to the UI; instead change the fallback to assign a generic
user-facing message (e.g., "An unexpected error occurred. Please try again.") to
NwcState.errorMessage and send the raw exception to the logger using logger.e(e)
(or logger.e('NWC error', e)). Keep existing NwcException and
NwcInvalidUriException handling unchanged and ensure this change is applied
inside the catch block that wraps the NwcState assignment.
🧹 Nitpick comments (1)
lib/features/wallet/widgets/wallet_status_card.dart (1)
130-137: Consider edge case: negative balance display.
_formatSatsdoesn't handle negative values — ifbalanceMsatswere ever negative (e.g., due to a buggy wallet response), the formatting with/ 1000000would produce unexpected strings like"-0.00M". Low risk since balances shouldn't be negative, but worth noting.
The test expected round() behavior (1999 msats → 2 sats) but the implementation now uses integer division (1999 msats → 1 sat) which is the correct behavior — you shouldn't show more sats than you have.
Replace e.toString() in the generic catch with a user-friendly message and log the raw error instead. NwcException and NwcInvalidUriException already use curated .message strings.
- Fix back navigation from ConnectWalletScreen: use context.pop()
instead of context.go('/wallet_settings') which replaced the
navigation stack and caused 'nothing to pop' error
- Add debug logging to NwcClient._sendRequest for troubleshooting
balance fetch: log received events, e-tag matching, response
matching, and timeout events
Use context.canPop() guard before popping. If there's nothing to
pop (e.g., user arrived via context.go() which replaces the stack),
fall back to context.go('/settings') instead of crashing with
'There is nothing to pop' GoError.
Some NWC relay implementations (e.g. Primal) don't support #e/#p tag filters in REQ subscriptions. Remove these from the NostrFilter and rely on the existing e-tag validation in the event handler. This fixes get_balance timeout when connecting to Primal wallets.
CI flagged invalid_null_aware_operator — pubkey is non-nullable in the event model used by the CI's Dart version.
Remove null-aware and non-null assertion operators — pubkey is non-nullable, so plain '.' is the correct access.
Replace Nostr.instance (shared singleton) with a dedicated Nostr() instance for NWC. This ensures subscriptions and events are sent only to the NWC wallet relay(s), not broadcast to all Mostro relays. The shared singleton caused subscriptions to go to Mostro relays instead of the NWC relay, so wallet responses were never received. Also properly disconnects NWC relays on disconnect() since we now own the instance.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
lib/services/nwc/nwc_client.dart (2)
260-296:⚠️ Potential issue | 🟡 MinorPotential
substringissue on short strings in diagnostic log (line 271).If
eTag[1]is somehow shorter than 8 characters,eTag[1].substring(0, 8)would throw aRangeError. While Nostr event IDs should always be 64-char hex, a malformed response could trigger this in the debug log path. A minor defensive fix:Suggested fix
- logger.d('NWC: Ignoring event — e tag mismatch (expected: ${requestId.substring(0, 8)}..., got: ${eTag != null && eTag.length >= 2 ? eTag[1].substring(0, 8) : "none"}...)'); + final gotTag = (eTag != null && eTag.length >= 2) ? eTag[1] : 'none'; + logger.d('NWC: Ignoring event — e tag mismatch (expected: ${requestId.substring(0, 8)}..., got: ${gotTag.length >= 8 ? gotTag.substring(0, 8) : gotTag}...)');
100-125:⚠️ Potential issue | 🟠 MajorMake
disconnect()async andawaitthedisconnectFromRelays()call.The
disconnectFromRelays()method is async (returnsFuture<void>), but the synchronousdisconnect()method calls it withoutawaiton line 118. This creates a fire-and-forget pattern where relay cleanup happens asynchronously in the background. Ifconnect()is called immediately afterdisconnect()(e.g., when updating the connection URI), the reconnection may race against the ongoing disconnect, potentially causing connection failures or state inconsistencies.Change
disconnect()to returnFuture<void>and await the call:Future<void> disconnect() async { // ... subscription cleanup ... try { await _nostr.services.relays.disconnectFromRelays(); } catch (e) { logger.w('NWC: Failed to disconnect relays: $e'); } // ... }Update all call sites to await the result.
🧹 Nitpick comments (5)
lib/services/nwc/nwc_client.dart (1)
31-32:_nostrServiceis injected but entirely unused — dead dependency.With the introduction of the dedicated
_nostrinstance (line 48), all relay interactions now go through_nostr.services.relaysdirectly. The_nostrServicefield is never referenced, and the// ignore: unused_fieldsuppression confirms this. Consider removing it from the constructor signature as well, or documenting why it's retained (e.g., planned future use).Also applies to: 48-48
lib/features/wallet/providers/nwc_provider.dart (3)
32-49:copyWithcannot set nullable fields tonullwithoutclearWalletInfo/clearError.The
??fallback meanscopyWith(walletAlias: null)retains the old alias rather than clearing it. This is fine for current usage, but if you ever need to null out just one field (e.g., alias becomes unavailable while balance stays), you'd need separate clear flags. Just noting as a design consideration for future extensibility.
67-69: Unawaited_autoReconnect()in constructor — safe but worth documenting.The fire-and-forget async call is a common StateNotifier pattern. If the notifier is disposed mid-reconnect, exceptions from setting state after disposal will be caught by
_autoReconnect's try-catch. This is safe, but a brief comment noting the fire-and-forget nature and its safety would aid future readers.
192-196:dispose()only disconnects — does not cancel in-flight_autoReconnectorconnect.If
_autoReconnect()(which callsconnect()) is still running whendispose()fires, the async operations continue after the notifier is disposed. While the try-catch in_autoReconnectcatches the resultingStateError, it's somewhat unclean. Consider tracking the in-flight future and cancelling it, or at minimum guardingstate =assignments with amountedcheck insideconnect().#!/bin/bash # Check if StateNotifier has a `mounted` property in flutter_riverpod 2.6.x rg -n 'bool get mounted' --type dartlib/features/wallet/screens/wallet_settings_screen.dart (1)
342-346: Unawaiteddisconnect()future in dialog callback.
ref.read(nwcProvider.notifier).disconnect()returnsFuture<void>but isn't awaited. SinceNwcNotifier.disconnect()catches all internal errors, this is practically safe. However, the unawaited future means any unexpected throws would be silently lost. Consider making the callback async:Suggested fix
TextButton( - onPressed: () { + onPressed: () async { Navigator.of(dialogContext).pop(); - ref.read(nwcProvider.notifier).disconnect(); + await ref.read(nwcProvider.notifier).disconnect(); },
Add NwcCrypto helper that supports both NIP-04 (AES-256-CBC) and NIP-44 (ChaCha20-Poly1305) encryption modes. On connect(), the client now fetches the wallet's info event (kind 13194) to detect supported encryption. If the wallet advertises nip44_v2, NIP-44 is used. Otherwise, NIP-04 is assumed per the NIP-47 spec for backwards compatibility. Response decryption auto-detects the format from the content (NIP-04 has '?iv=' separator, NIP-44 does not). This fixes communication with wallets like Coinos that use NIP-04.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/services/nwc/nwc_client.dart (1)
330-370:⚠️ Potential issue | 🟡 MinorDebug log
substringcalls on event fields could throw on malformed events.Lines 332 and 341 call
.substring(0, 8)onevent.pubkeyandeTag[1]. While these are normally 64-char hex strings, a malformed or unexpected event would cause aRangeError. These are inside the innertry/catchso the impact is contained (completer gets an error), but a crash in a debug log statement masking the real event data is confusing.Safer logging helper
You could use a small helper or inline a safe truncation:
// Inline approach: event.pubkey.length >= 8 ? event.pubkey.substring(0, 8) : event.pubkeyOr extract a utility:
String _short(String s, [int len = 8]) => s.length >= len ? s.substring(0, len) : s;
🤖 Fix all issues with AI agents
In `@lib/services/nwc/nwc_client.dart`:
- Around line 113-166: In _detectEncryptionMode wrap the period between starting
the stream.listen (stream.stream.listen(...)) and awaiting
completer.future.timeout in a try/finally so that both subscription.cancel() and
_nostr.services.relays.closeEventsSubscription(subId) are always executed;
specifically ensure the created `subscription` is cancelled in the finally block
and that closeEventsSubscription(subId) is called there as well (so regardless
of exceptions from completer.future.timeout or other errors you never leave a
dangling relay subscription).
In `@lib/services/nwc/nwc_crypto.dart`:
- Around line 168-173: The _pkcs7Unpad function currently only checks padLength
bounds but does not verify that every padding byte equals padLength; update
_pkcs7Unpad to validate that the last padLength bytes all equal padLength and if
not, throw a clear error (e.g., FormatException or StateError) instead of
silently returning a truncated buffer; keep the existing bounds checks
(data.isEmpty, padLength==0, padLength>16) and then iterate or use a range check
on data.sublist(data.length - padLength) to confirm all bytes match before
returning Uint8List.fromList(data.sublist(0, data.length - padLength)).
🧹 Nitpick comments (1)
lib/services/nwc/nwc_client.dart (1)
281-414: Request/response flow looks solid overall.The encryption negotiation, content-based detection fallback for responses, e-tag validation, timeout handling with cleanup in
finally, and proper error propagation are all well-structured. The approach of filtering only bykind + authorand verifying the e-tag in the handler is a pragmatic workaround for relay compatibility.One minor note: the doc comment on Line 276 still says "Encrypts the request payload with NIP-44" but the implementation now uses the negotiated mode, which may be NIP-04.
Update stale doc comment
/// Sends a NWC request and waits for the response. /// - /// 1. Encrypts the request payload with NIP-44 + /// 1. Encrypts the request payload with the negotiated encryption mode /// 2. Creates a kind 23194 event signed with the connection secret /// 3. Subscribes to kind 23195 responses tagged with the request event ID /// 4. Publishes the request event /// 5. Waits for the response or times out
Verify every byte in the padding region equals padLength, not just the last byte. Prevents silently producing garbage output when decryption fails with a wrong key.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@lib/services/nwc/nwc_crypto.dart`:
- Around line 103-126: In _decryptNip04 validate inputs before decryption:
ensure parts parsing succeeded, check that encryptedBytes.length > 0 and
encryptedBytes.length % 16 == 0 (AES-CBC requires full 16-byte blocks) and that
iv.length == 16; if any check fails, throw a clear ArgumentError (e.g., "Invalid
NIP-04 ciphertext: ciphertext must be base64 of 16-byte blocks" or "Invalid IV
length: expected 16 bytes"). Keep the sharedSecret computation via
_computeNip04SharedSecret and retain PKCS#7 unpadding via _pkcs7Unpad, but
perform these length checks first to avoid opaque PointyCastle errors and
misleading padding behavior.
Phase 1: - Document dedicated Nostr instance (replaces shared singleton) - Document dual NIP-04/NIP-44 encryption support - Document NwcCrypto helper and encryption negotiation - Update architecture diagram (5 files now, including nwc_crypto.dart) - Remove outdated 'known limitation' about relay disconnection Phase 2: - Document all bugs found during real-world testing and their fixes - Add tested wallets table (Coinos NIP-04, Primal custom relay) - Document encryption negotiation flow - Document simplified relay filter rationale - Document security improvements (PKCS#7 validation, relay isolation)
Check that IV is exactly 16 bytes and ciphertext is a non-zero multiple of 16 before attempting decryption. Prevents cryptic pointycastle errors on malformed NIP-04 content.
Summary
Implements NWC wallet connection management UI as described in #457. Builds on Phase 1 (#456 / #461).
What's included
New files (1500 lines)
lib/data/repositories/nwc_storage.dart— Secure storage for NWC connection URI via FlutterSecureStoragelib/features/wallet/providers/nwc_provider.dart— StateNotifier for NWC state (disconnected/connecting/connected/error), auto-reconnect from storagelib/features/wallet/screens/wallet_settings_screen.dart— Main wallet management screen (alias, balance, disconnect)lib/features/wallet/screens/connect_wallet_screen.dart— URI paste & validation screenlib/features/wallet/widgets/wallet_status_card.dart— Compact status card for Settingslib/features/wallet/widgets/wallet_balance_widget.dart— Balance display in satsdocs/NWC_PHASE2_IMPLEMENTATION.md— Implementation documentationtest/data/repositories/nwc_storage_test.dart— Storage teststest/features/wallet/providers/nwc_provider_test.dart— Provider testsModified files
lib/core/app_routes.dart— Added/wallet_settingsand/connect_walletrouteslib/data/models/enums/storage_keys.dart— AddednwcConnectionUrito SecureStorageKeyslib/features/settings/settings_screen.dart— Added wallet status cardlib/l10n/intl_en.arb,intl_es.arb,intl_it.arb— Wallet localization stringsDesign decisions
UX Flow
Closes #457
Summary by CodeRabbit
New Features
Improvements
Documentation
Localization
Tests