Skip to content

Feat/nip55 external signer#657

Open
nogringo wants to merge 6 commits into
masterfrom
feat/nip55-external-signer
Open

Feat/nip55 external signer#657
nogringo wants to merge 6 commits into
masterfrom
feat/nip55-external-signer

Conversation

@nogringo

@nogringo nogringo commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Release Notes

  • New Features

    • Implemented NIP-55 external signer protocol enabling flexible authentication and cryptographic operations with compatible signer applications
  • UI Updates

    • Updated login interface labels across all supported languages: changed "Login with Amber" to "Login with signer app"
    • Enhanced external signer app detection and compatibility
  • Infrastructure

    • Updated Android build system configuration and Kotlin compiler tooling

@nogringo nogringo requested review from 1-leo and frnandu June 10, 2026 17:24
@nogringo nogringo self-assigned this Jun 10, 2026
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces Amber Flutter with NIP-55 external signer protocol. Changes span Android native plugin (Kotlin/Gradle), Dart bridge (MethodChannel data models), event signer repository, account persistence, login UI, localization (13 locales), sample app, and build configuration.

Changes

NIP-55 External Signer Implementation

Layer / File(s) Summary
Android NIP-55 Plugin
packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/*, packages/ndk_flutter/android/src/main/AndroidManifest.xml, packages/ndk_flutter/android/{build,settings}.gradle
Native Kotlin plugin registers MethodChannel ("ndk"), stores context, implements nostrSignerScheme routing with ContentResolver silent path (fallback to intent), and isAppInstalled queries. Manifest declares nostrsigner URI scheme queries. Gradle updates package group to relaystr.ndk, bumps Kotlin to 2.2.20, sets namespace.
Dart NIP-55 Signer Bridge
packages/ndk_flutter/lib/data_layer/data_sources/nip55_signer.dart
Defines Nip55Permission (type, kind, toJson), Nip55LoginResult (pubkey, package), and Nip55Signer class wrapping MethodChannel. Exposes methods for app installation check, login/pubkey retrieval, event signing, and NIP-04/NIP-44 encrypt/decrypt.
NIP-55 Event Signer Repository
packages/ndk_flutter/lib/data_layer/repositories/signers/nip55_event_signer.dart
Replaces AmberEventSigner with Nip55EventSigner accepting Nip55Signer. Normalizes pubkeys via Nip19, throttles requests to 100 concurrent, updates request IDs to nip55_ prefix, and delegates signing/encryption to NIP-55 signer methods.
Account Persistence & Model
packages/ndk_flutter/lib/models/accounts.dart, packages/ndk_flutter/lib/main/ndk_flutter.dart
AccountKinds enum replaces amber with nip55. Legacy amber accounts normalize to nip55 on deserialize. saveAccountsState persists signer package; restoreAccountsState recreates Nip55EventSigner from package. Account switch wrapped in try/catch.
Login UI & Controller
packages/ndk_flutter/lib/widgets/login/{login_controller,n_login}.dart, packages/ndk_flutter/lib/widgets/pending_requests/n_pending_requests.dart
LoginController adds isWaitingForExternalSigner flag and loginWithExternalSigner() method (checks app, launches GitHub if missing, calls login, creates signer, invokes NDK). NLogin adds enableSignerAppLogin param and signerAppView UI button. Pending requests now label Nip55 as "signer app".
Localization (13 locales)
packages/ndk_flutter/lib/l10n/{app_*.arb, app_localizations*.dart}
All ARB resource files and generated Dart localization classes replace loginWithAmber key/getter with loginWithSignerApp (German, English, Spanish, Finnish, French, Italian, Japanese, Polish, Portuguese, Russian, Chinese).
Sample App Migration
packages/sample-app/{lib/main.dart, lib/widgets_demo_page.dart, lib/nip55_signer_page.dart, README.md, pubspec.yaml}
Replaces Amber availability check and signer with Nip55Signer. Demo widget disables enableSignerAppLogin. Test page renamed Nip55SignerPage, updated all signer operations. README reframed to NIP-55 generic (Amber as one example).
Build Configuration
packages/ndk_flutter/pubspec.yaml, packages/sample-app/{android/build.gradle, android/settings.gradle, android/gradle.properties, android/gradle/wrapper/*}
ndk_flutter pubspec removes amberflutter dependency, registers Android plugin (relaystr.ndk, DartNdkPlugin). Sample app updates Kotlin 2.1.0→2.2.20, compileSdk 35→36, ndkVersion to 28.2.13676358, Gradle wrapper to 8.14, and gradle.properties flags.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • relaystr/ndk#542: PR #542 adds package:amberflutter/amberflutter.dart re-export; this PR removes it.
  • relaystr/ndk#604: This PR's account restoration depends on NdkConfig.eventSignerFactory refactoring introduced in PR #604.
  • relaystr/ndk#643: PR #643 enabled Finnish/Portuguese localization files; this PR updates those files as part of the broader i18n switch from Amber to NIP-55 label.

Suggested labels

enhancement

Suggested reviewers

  • frnandu
  • 1-leo

🐰 A signer app's in the nest,
Replacing Amber—NDK knows best!
NIP-55 protocol shines so bright,
Android plugins work just right.
Localized in thirteen tongues with care,
The external signer magic's here! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/nip55 external signer' clearly and concisely summarizes the main change: adding NIP-55 external signer support to the project. It is directly related to the substantial changeset, which includes Android platform code, Dart/Flutter layers, and sample app updates.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/nip55-external-signer
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/nip55-external-signer

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 71.90%. Comparing base (2452e67) to head (cf17079).
⚠️ Report is 12 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #657      +/-   ##
==========================================
- Coverage   71.94%   71.90%   -0.05%     
==========================================
  Files         210      210              
  Lines       10698    10698              
==========================================
- Hits         7697     7692       -5     
- Misses       3001     3006       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/DartNdkPlugin.kt (1)

34-37: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Correlate results per request instead of using one shared _result and request code 0.

With concurrent calls, Line 58 overwrites _result before prior activity responses arrive, and Line 36 uses a fixed request code (0), so responses can be delivered to the wrong Dart caller or dropped.

Suggested direction
-    private lateinit var _result: MethodChannel.Result
-    private val _intentRequestCode = 0
+    private val _pendingResults = mutableMapOf<Int, MethodChannel.Result>()
+    private var _nextRequestCode = 10000
...
-                _result = MethodResultWrapper(result)
+                val wrapped = MethodResultWrapper(result)
...
-                    activity.startActivityForResult(intent, _intentRequestCode)
+                    val requestCode = _nextRequestCode++
+                    _pendingResults[requestCode] = wrapped
+                    activity.startActivityForResult(intent, requestCode)
...
     override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean {
-        if (requestCode == _intentRequestCode) {
+        val pendingResult = _pendingResults.remove(requestCode) ?: return false
+        if (resultCode == Activity.RESULT_OK && intent != null) {
             ...
-            _result.success(dataMap)
+            pendingResult.success(dataMap)
             return true
-        }
-        return false
+        }
+        pendingResult.success(HashMap<String, String?>())
+        return true
     }

Also applies to: 58-59, 140-165

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/DartNdkPlugin.kt`
around lines 34 - 37, The plugin currently stores a single shared _result and
uses a fixed _intentRequestCode which causes races and mis-delivered responses;
change to correlate each outbound intent with its own request id by introducing
an AtomicInteger (or similar) to generate unique request codes and a concurrent
map (e.g., MutableMap<Int, MethodChannel.Result>) that stores
MethodChannel.Result instances keyed by that request code; when starting an
activity use the generated code instead of _intentRequestCode and put the
corresponding Result into the map, and in your activity result handler (the
methods around where _result is referenced, including lines ~58-59 and ~140-165)
look up and remove the Result from the map by requestCode before invoking
success/error so each Dart caller receives the correct response and no results
are overwritten or leaked.
packages/sample-app/lib/amber_page.dart (1)

27-40: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle both result and signature keys for NIP-55 public key responses.

After switching to Nip55Signer, Line 38 still reads only value['signature']. Some signer responses use result, so this can leave _npub empty and break Nip19.decode.

Suggested fix
-            ).then((value) {
-              _npub = value['signature'] ?? '';
+            ).then((value) {
+              _npub = (value['result'] ?? value['signature'] ?? '') as String;
               _pubkeyHex = Nip19.decode(_npub);
               setState(() {
                 _text = '$value';
               });
             });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sample-app/lib/amber_page.dart` around lines 27 - 40, The onPressed
handler currently extracts the NIP-55 public key from value['signature'] which
can be empty for newer Nip55Signer responses that use value['result']; update
the amber.getPublicKey .then callback to prefer value['result'] and fall back to
value['signature'] (or vice versa) when assigning _npub, then continue to decode
with Nip19.decode(_npub) and call setState as before; update references in this
callback (amber.getPublicKey, Nip55Permission, _npub, _pubkeyHex, Nip19.decode)
so _npub is always populated from either key before decoding.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ndk_flutter/lib/widgets/login/login_controller.dart`:
- Around line 102-107: The code currently calls signer.isAppInstalled() and, if
false, immediately opens the Amber GitHub URL (launchUrl) which hard-codes Amber
as the only acceptable NIP-55 signer; update the logic to detect any compliant
NIP-55 signer and avoid redirecting to Amber. Specifically, modify or extend the
signer/Nip55Signer API to either (a) expose the actually installed package id
(e.g., signer.getInstalledPackage() or signer.installedPackageName) or (b)
accept a list of known package IDs (e.g., isAppInstalled(List<String>
candidates)) and use that to determine installation; then change the fallback
from launchUrl(Uri.parse('https://github.com/greenart7c3/Amber')) to a neutral
flow (show an in-app chooser, an installation help dialog, or open a
configurable signers discovery URL obtained from signer metadata) so that any
valid NIP-55 signer can be used and Amber is not hard-coded. Ensure changes
touch the isAppInstalled() call site and any redirect/launchUrl usage in
login_controller.dart and the Nip55Signer implementation.
- Around line 97-127: In loginWithExternalSigner, the flag
isWaitingForExternalSigner is set true but not guaranteed to be cleared if
signer.isAppInstalled() or signer.login() throws; wrap the main flow in a
try/finally (set isWaitingForExternalSigner = true before the try) and move the
existing "isWaitingForExternalSigner = false" into the finally block so it
always runs, preserving the current early-return logic (e.g., after launchUrl or
null loginResult) inside the try; update references to Nip55Signer,
Nip55EventSigner, ndk.accounts.loginExternalSigner, and loggedIn to remain
unchanged.

---

Outside diff comments:
In `@packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/DartNdkPlugin.kt`:
- Around line 34-37: The plugin currently stores a single shared _result and
uses a fixed _intentRequestCode which causes races and mis-delivered responses;
change to correlate each outbound intent with its own request id by introducing
an AtomicInteger (or similar) to generate unique request codes and a concurrent
map (e.g., MutableMap<Int, MethodChannel.Result>) that stores
MethodChannel.Result instances keyed by that request code; when starting an
activity use the generated code instead of _intentRequestCode and put the
corresponding Result into the map, and in your activity result handler (the
methods around where _result is referenced, including lines ~58-59 and ~140-165)
look up and remove the Result from the map by requestCode before invoking
success/error so each Dart caller receives the correct response and no results
are overwritten or leaked.

In `@packages/sample-app/lib/amber_page.dart`:
- Around line 27-40: The onPressed handler currently extracts the NIP-55 public
key from value['signature'] which can be empty for newer Nip55Signer responses
that use value['result']; update the amber.getPublicKey .then callback to prefer
value['result'] and fall back to value['signature'] (or vice versa) when
assigning _npub, then continue to decode with Nip19.decode(_npub) and call
setState as before; update references in this callback (amber.getPublicKey,
Nip55Permission, _npub, _pubkeyHex, Nip19.decode) so _npub is always populated
from either key before decoding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: afda8421-ee26-45f4-8909-5d4cc23fb950

📥 Commits

Reviewing files that changed from the base of the PR and between 2452e67 and d88ebd9.

⛔ Files ignored due to path filters (1)
  • packages/sample-app/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (49)
  • packages/ndk_flutter/android/build.gradle
  • packages/ndk_flutter/android/settings.gradle
  • packages/ndk_flutter/android/src/main/AndroidManifest.xml
  • packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/DartNdkPlugin.kt
  • packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/Nip55Constants.kt
  • packages/ndk_flutter/lib/data_layer/data_sources/amber_flutter.dart
  • packages/ndk_flutter/lib/data_layer/data_sources/nip55_signer.dart
  • packages/ndk_flutter/lib/data_layer/repositories/signers/nip55_event_signer.dart
  • packages/ndk_flutter/lib/l10n/app_de.arb
  • packages/ndk_flutter/lib/l10n/app_en.arb
  • packages/ndk_flutter/lib/l10n/app_es.arb
  • packages/ndk_flutter/lib/l10n/app_fi.arb
  • packages/ndk_flutter/lib/l10n/app_fr.arb
  • packages/ndk_flutter/lib/l10n/app_it.arb
  • packages/ndk_flutter/lib/l10n/app_ja.arb
  • packages/ndk_flutter/lib/l10n/app_localizations.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_de.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_en.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_es.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_fi.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_fr.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_it.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_ja.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_pl.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_pt.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_ru.dart
  • packages/ndk_flutter/lib/l10n/app_localizations_zh.dart
  • packages/ndk_flutter/lib/l10n/app_pl.arb
  • packages/ndk_flutter/lib/l10n/app_pt.arb
  • packages/ndk_flutter/lib/l10n/app_pt_BR.arb
  • packages/ndk_flutter/lib/l10n/app_ru.arb
  • packages/ndk_flutter/lib/l10n/app_zh.arb
  • packages/ndk_flutter/lib/main/ndk_flutter.dart
  • packages/ndk_flutter/lib/models/accounts.dart
  • packages/ndk_flutter/lib/ndk_flutter.dart
  • packages/ndk_flutter/lib/ndk_platform_interface.dart
  • packages/ndk_flutter/lib/widgets/login/login_controller.dart
  • packages/ndk_flutter/lib/widgets/login/n_login.dart
  • packages/ndk_flutter/lib/widgets/pending_requests/n_pending_requests.dart
  • packages/ndk_flutter/pubspec.yaml
  • packages/sample-app/android/app/build.gradle
  • packages/sample-app/android/build.gradle
  • packages/sample-app/android/gradle.properties
  • packages/sample-app/android/gradle/wrapper/gradle-wrapper.properties
  • packages/sample-app/android/settings.gradle
  • packages/sample-app/lib/amber_page.dart
  • packages/sample-app/lib/main.dart
  • packages/sample-app/lib/widgets_demo_page.dart
  • packages/sample-app/pubspec.yaml
💤 Files with no reviewable changes (2)
  • packages/ndk_flutter/lib/data_layer/data_sources/amber_flutter.dart
  • packages/sample-app/pubspec.yaml

Comment on lines +61 to +64
if (paramsMap == null) {
Log.d("onMethodCall", "paramsMap is null")
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Complete the MethodChannel result on every exit path.

On Line 63, Line 116, and the non-OK branch in Line 141+, execution can exit without calling success/error/notImplemented, which leaves the Dart caller hanging indefinitely.

Suggested fix
                 val paramsMap = call.arguments as? HashMap<*, *>
                 if (paramsMap == null) {
                     Log.d("onMethodCall", "paramsMap is null")
+                    _result.error("invalid_args", "Expected map arguments", null)
                     return
                 }
...
-                try {
-                    _activity?.startActivityForResult(intent, _intentRequestCode)
+                val activity = _activity
+                if (activity == null) {
+                    _result.success(HashMap<String, String?>())
+                    return
+                }
+                try {
+                    activity.startActivityForResult(intent, _intentRequestCode)
                 } catch (e: Exception) {
                     Log.d("onMethodCall", "startActivityForResult failed for '$signerPackage': ${e.message}")
                     _result.success(HashMap<String, String?>())
                 }
...
     override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean {
         if (requestCode == _intentRequestCode) {
             if (resultCode == Activity.RESULT_OK && intent != null) {
                 ...
                 _result.success(dataMap)
                 return true
             }
+            _result.success(HashMap<String, String?>())
+            return true
         }
         return false
     }

Also applies to: 116-120, 141-167

Comment thread packages/ndk_flutter/lib/widgets/login/login_controller.dart
Comment on lines 102 to 107
final isInstalled = await signer.isAppInstalled();

if (!isAmberInstalled) {
if (!isInstalled) {
isWaitingForExternalSigner = false;
launchUrl(Uri.parse('https://github.com/greenart7c3/Amber'));
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Hard-coded install gate restricts “signer app” login to Amber package only.

Line 102 uses Nip55Signer.isAppInstalled(), and the upstream implementation checks only com.greenart7c3.nostrsigner. If a different NIP-55 signer is installed, this path fails and Line 106 redirects to Amber, blocking valid signer-app logins.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ndk_flutter/lib/widgets/login/login_controller.dart` around lines
102 - 107, The code currently calls signer.isAppInstalled() and, if false,
immediately opens the Amber GitHub URL (launchUrl) which hard-codes Amber as the
only acceptable NIP-55 signer; update the logic to detect any compliant NIP-55
signer and avoid redirecting to Amber. Specifically, modify or extend the
signer/Nip55Signer API to either (a) expose the actually installed package id
(e.g., signer.getInstalledPackage() or signer.installedPackageName) or (b)
accept a list of known package IDs (e.g., isAppInstalled(List<String>
candidates)) and use that to determine installation; then change the fallback
from launchUrl(Uri.parse('https://github.com/greenart7c3/Amber')) to a neutral
flow (show an in-app chooser, an installation help dialog, or open a
configurable signers discovery URL obtained from signer metadata) so that any
valid NIP-55 signer can be used and Amber is not hard-coded. Ensure changes
touch the isAppInstalled() call site and any redirect/launchUrl usage in
login_controller.dart and the Nip55Signer implementation.

@nogringo

Copy link
Copy Markdown
Collaborator Author

Metioned in nip55 and not implemented:

  • decrypt_zap_event (not in ndk abstract signer)
  • batch requests
  • gzip for large requests
  • web signing via callbackUrl

@1-leo 1-leo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested and looks good!

Because its a braking change we should publish a migration guide in our docs (basically just renaming stuff) wdyt?

Also amberflutter is still mentioned in some codedocs and skills.md

@frnandu frnandu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Sample app AmberPage class name -- The class is still called AmberPage and the variable is amber. Since this is now generic, renaming to SignerAppPage / signerApp would be cleaner

  • isAppInstalled hardcodes Amber's package name
    - nip55_signer.dart:77 always checks com.greenart7c3.nostrsigner. The whole point of this PR is to be signer-generic, yet install detec
    tion is Amber-specific. If the user has Primal/Aegis but not Amber, isAppInstalled() returns false, and the UI redirects to Amber's GitHub ins
    tead of letting them proceed.
    - The Kotlin side has isExternalSignerInstalled() which checks the nostrsigner: scheme (generic), but the Dart side doesn't call it wit
    h packageName: null -- it always passes the Amber package.

  • isAppInstalled logic in Kotlin has surprising OR
    - DartNdkPlugin.kt:186: isPackageInstalled(...) || isExternalSignerInstalled(_context) -- if the specific package is not installed but ANY external signer is, this returns true. For isAppInstalled called with a specific package, this seems misleading. It may cause the UI to think a specific signer is available when it's actually a different one.

@nogringo nogringo requested a review from frnandu June 12, 2026 19:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/ndk_flutter/lib/main/ndk_flutter.dart (1)

244-249: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add logging for silently suppressed account-switch errors.

The try/catch improves startup resilience by preventing crashes when the stored logged account cannot be restored, but catch (_) silently suppresses all errors with no logging. This makes debugging restoration failures in production difficult.

Consider adding a log statement to capture these errors for debugging:

📊 Proposed logging addition
     try {
       ndk.accounts.switchAccount(pubkey: accounts.loggedAccount!);
-    } catch (_) {
+    } catch (e) {
       // stored logged account could not be restored (e.g. stale/corrupted
       // state); ignore rather than crash app startup.
+      debugPrint('Failed to restore logged account: $e');
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ndk_flutter/lib/main/ndk_flutter.dart` around lines 244 - 249, The
catch block around ndk.accounts.switchAccount(pubkey: accounts.loggedAccount!)
currently suppresses all errors; update it to log the caught exception and
optionally the stacktrace so restoration failures are observable in production
(e.g., use the existing logger or at minimum print) while still swallowing the
error to avoid crashing; reference ndk.accounts.switchAccount and
accounts.loggedAccount when adding the log entry.
packages/sample-app/lib/nip55_signer_page.dart (2)

119-157: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove redundant decrypt calls.

The "Nip 04 Decrypt" button makes three identical nip04Decrypt calls (lines 120, 132, 144) with the same parameters, differing only in the display suffix ("1", "2", "3"). This appears to be leftover debug/test code.

For a sample app, one call is sufficient. The redundant calls waste resources and may confuse developers learning from the example.

🧹 Suggested cleanup
       FilledButton(
         onPressed: () async {
           nip55Signer
               .nip04Decrypt(
             ciphertext: _cipherText,
             currentUser: _npub,
             pubKey: _pubkeyHex,
           )
               .then((value) {
             setState(() {
-              _text = '$value 1';
+              _text = '$value';
             });
           });
-          // ;
-          nip55Signer
-              .nip04Decrypt(
-            ciphertext: _cipherText,
-            currentUser: _npub,
-            pubKey: _pubkeyHex,
-          )
-              .then((value) {
-            setState(() {
-              _text = '$value 2';
-            });
-          });
-          //   ,
-          nip55Signer
-              .nip04Decrypt(
-            ciphertext: _cipherText,
-            currentUser: _npub,
-            pubKey: _pubkeyHex,
-          )
-              .then((value) {
-            setState(() {
-              _text = '$value 3';
-            });
-          });
         },
         child: const Text('Nip 04 Decrypt'),
       ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sample-app/lib/nip55_signer_page.dart` around lines 119 - 157,
Remove the redundant duplicate nip55Signer.nip04Decrypt calls in the onPressed
handler and keep a single asynchronous call that uses the existing parameters
(ciphertext: _cipherText, currentUser: _npub, pubKey: _pubkeyHex); when it
completes call setState to update _text with the decrypted value (remove the
extra calls that append "1" and "2"/"3" so only one nip04Decrypt invocation
remains in the onPressed block).

28-36: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Request all necessary permissions at login for silent operation.

The sample only requests nip04_encrypt and nip04_decrypt permissions, but later buttons invoke signEvent (lines 60, 86), nip44Encrypt (line 160), and nip44Decrypt (line 177) without pre-authorizing those operations. This means every sign/nip44 request will require user interaction instead of being answered silently via ContentResolver.

For a sample app demonstrating best practices, consider using Nip55Signer.defaultPermissions or explicitly including all operations used in the demo.

🔐 Suggested fix to request complete permissions
           nip55Signer.getPublicKey(
             permissions: [
+              const Nip55Permission(
+                type: "sign_event",
+              ),
               const Nip55Permission(
                 type: "nip04_encrypt",
               ),
               const Nip55Permission(
                 type: "nip04_decrypt",
               ),
+              const Nip55Permission(
+                type: "nip44_encrypt",
+              ),
+              const Nip55Permission(
+                type: "nip44_decrypt",
+              ),
             ],

Alternatively, use the default set:

-          nip55Signer.getPublicKey(
-            permissions: [
-              const Nip55Permission(
-                type: "nip04_encrypt",
-              ),
-              const Nip55Permission(
-                type: "nip04_decrypt",
-              ),
-            ],
-          ).then((value) {
+          nip55Signer.login().then((result) {
+            if (result == null) return;
+            _npub = 'npub...';  // or extract from result.pubkey
+            _pubkeyHex = result.pubkey;
             setState(() {
-              _text = '$value';
+              _text = 'Logged in: ${result.pubkey}';
             });
           });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sample-app/lib/nip55_signer_page.dart` around lines 28 - 36, The
code only requests nip04_encrypt and nip04_decrypt in nip55Signer.getPublicKey
which causes later calls (signEvent, nip44Encrypt, nip44Decrypt) to prompt the
user; update the permission request to include all operations used in the demo
by either replacing the explicit Nip55Permission list with
Nip55Signer.defaultPermissions or adding explicit Nip55Permission entries for
signEvent (signing) and nip44Encrypt/nip44Decrypt (nip44_encrypt/nip44_decrypt)
so that nip55Signer.getPublicKey is called with the full set of required
permissions for silent operation.
🧹 Nitpick comments (2)
packages/sample-app/lib/nip55_signer_page.dart (2)

28-43: 💤 Low value

Consider using consistent async patterns.

This button uses .then() callback style, while the "Verify signature" button (line 86) uses await. For consistency and readability in a sample app, prefer one pattern throughout—modern Dart/Flutter code typically favors async/await.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sample-app/lib/nip55_signer_page.dart` around lines 28 - 43, The
nip55Signer.getPublicKey call uses .then() while other handlers use async/await;
change the button handler to be async, await nip55Signer.getPublicKey(...),
assign _npub and _pubkeyHex (using Nip19.decode) and call setState() afterward,
and wrap the await in try/catch to handle errors consistently—update the code
that references nip55Signer.getPublicKey, _npub, _pubkeyHex, and setState to
follow this async/await pattern.

19-19: ⚡ Quick win

Consider demonstrating the package-targeting pattern.

The sample instantiates const Nip55Signer() without a package, which means every request will let Android route to any compatible signer. The recommended NIP-55 pattern is to capture the package from the login result and create a new Nip55Signer(package: capturedPackage) instance, enabling silent ContentResolver-based answers for pre-authorized permissions.

This would better demonstrate the optimal integration flow for developers using this sample.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/sample-app/lib/nip55_signer_page.dart` at line 19, Replace the
unconditional const Nip55Signer() instantiation with a package-targeted signer
by capturing the package identifier from the login result and creating
Nip55Signer(package: capturedPackage) (use the same variable name nip55Signer or
replace its declaration where it's defined) so subsequent requests use the
package-specific signer and enable silent ContentResolver-based responses for
pre-authorized permissions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/ndk_flutter/lib/main/ndk_flutter.dart`:
- Around line 244-249: The catch block around ndk.accounts.switchAccount(pubkey:
accounts.loggedAccount!) currently suppresses all errors; update it to log the
caught exception and optionally the stacktrace so restoration failures are
observable in production (e.g., use the existing logger or at minimum print)
while still swallowing the error to avoid crashing; reference
ndk.accounts.switchAccount and accounts.loggedAccount when adding the log entry.

In `@packages/sample-app/lib/nip55_signer_page.dart`:
- Around line 119-157: Remove the redundant duplicate nip55Signer.nip04Decrypt
calls in the onPressed handler and keep a single asynchronous call that uses the
existing parameters (ciphertext: _cipherText, currentUser: _npub, pubKey:
_pubkeyHex); when it completes call setState to update _text with the decrypted
value (remove the extra calls that append "1" and "2"/"3" so only one
nip04Decrypt invocation remains in the onPressed block).
- Around line 28-36: The code only requests nip04_encrypt and nip04_decrypt in
nip55Signer.getPublicKey which causes later calls (signEvent, nip44Encrypt,
nip44Decrypt) to prompt the user; update the permission request to include all
operations used in the demo by either replacing the explicit Nip55Permission
list with Nip55Signer.defaultPermissions or adding explicit Nip55Permission
entries for signEvent (signing) and nip44Encrypt/nip44Decrypt
(nip44_encrypt/nip44_decrypt) so that nip55Signer.getPublicKey is called with
the full set of required permissions for silent operation.

---

Nitpick comments:
In `@packages/sample-app/lib/nip55_signer_page.dart`:
- Around line 28-43: The nip55Signer.getPublicKey call uses .then() while other
handlers use async/await; change the button handler to be async, await
nip55Signer.getPublicKey(...), assign _npub and _pubkeyHex (using Nip19.decode)
and call setState() afterward, and wrap the await in try/catch to handle errors
consistently—update the code that references nip55Signer.getPublicKey, _npub,
_pubkeyHex, and setState to follow this async/await pattern.
- Line 19: Replace the unconditional const Nip55Signer() instantiation with a
package-targeted signer by capturing the package identifier from the login
result and creating Nip55Signer(package: capturedPackage) (use the same variable
name nip55Signer or replace its declaration where it's defined) so subsequent
requests use the package-specific signer and enable silent ContentResolver-based
responses for pre-authorized permissions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e0cc9a2-67f4-45f7-bec5-fed6f3a47681

📥 Commits

Reviewing files that changed from the base of the PR and between 477db93 and cf17079.

📒 Files selected for processing (7)
  • packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/DartNdkPlugin.kt
  • packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/Nip55Constants.kt
  • packages/ndk_flutter/lib/data_layer/data_sources/nip55_signer.dart
  • packages/ndk_flutter/lib/data_layer/repositories/signers/nip55_event_signer.dart
  • packages/ndk_flutter/lib/main/ndk_flutter.dart
  • packages/sample-app/README.md
  • packages/sample-app/lib/nip55_signer_page.dart
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/ndk_flutter/lib/data_layer/repositories/signers/nip55_event_signer.dart
  • packages/ndk_flutter/lib/data_layer/data_sources/nip55_signer.dart
  • packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/DartNdkPlugin.kt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants