Skip to content

Feat/nip55 external signer#657

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

Feat/nip55 external signer#657
nogringo wants to merge 2 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

  • External signer login now uses NIP-55 protocol for broader compatibility.
  • Login button text updated to "Login with signer app" across all supported languages.
  • Enhanced Android integration with improved request handling and fallback mechanisms for external signer operations.

@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 the Amber Flutter external signer integration with a new NIP-55 (standardized protocol) implementation across native Android plugin, Dart data layer, event signing, account persistence, UI controllers, and localization strings for 13 languages.

Changes

NIP-55 External Signer Implementation

Layer / File(s) Summary
Android NIP-55 Native Plugin
packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/*, packages/ndk_flutter/android/build.gradle, packages/ndk_flutter/android/settings.gradle, packages/ndk_flutter/android/src/main/AndroidManifest.xml
Kotlin plugin implements NIP-55 via MethodChannel with ContentResolver-based silent signing fallback and intent-based interactive signing. Gradle namespace and group updated to relaystr.ndk; Kotlin plugin version bumped to 2.2.20. Manifest declares nostrsigner URI scheme query for Android 11+ compatibility. Constants file defines NIP-55 protocol field keys and signer URI scheme.
Dart NIP-55 Signer Bridge
packages/ndk_flutter/lib/data_layer/data_sources/nip55_signer.dart
New Nip55Permission and Nip55LoginResult models for login authorization and results. Nip55Signer class bridges Dart to native MethodChannel, with APIs for checking app installation, retrieving public keys, logging in, and invoking signer operations (sign_event, nip04/nip44 encrypt/decrypt, zap decryption). Supports both silent ContentResolver path and interactive intent launch with optional package targeting for follow-up requests.
NIP-55 Event Signer Repository
packages/ndk_flutter/lib/data_layer/repositories/signers/nip55_event_signer.dart
Replaces AmberEventSigner with Nip55EventSigner wrapping Nip55Signer. Normalizes pubkey to hex via Nip19, generates nip55_-prefixed request IDs, throttles concurrent requests, and delegates sign/encrypt/decrypt operations to corresponding nip55Signer methods with proper parameter naming.
Account Model & Persistence
packages/ndk_flutter/lib/models/accounts.dart, packages/ndk_flutter/lib/main/ndk_flutter.dart, packages/ndk_flutter/lib/ndk_flutter.dart
AccountKinds enum replaces amber with nip55. NostrAccount.fromJson normalizes legacy 'amber' kind values to 'nip55' for backward compatibility. saveAccountsState() persists nip55 accounts with signer package seed; restoreAccountsState() recreates them with Nip55EventSigner(Nip55Signer(package)). Account restoration wrapped in try/catch to suppress stale state errors. Barrel export updated to re-export NIP-55 signer components and remove Amber exports.
Login UI & Controller
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
LoginController replaces isWaitingForAmber with isWaitingForExternalSigner and loginWithAmber() with loginWithExternalSigner(). New method checks app availability, calls signer.login(), constructs Nip55EventSigner pinned to returned package, and calls ndk.accounts.loginExternalSigner(). NLogin widget adds enableSignerAppLogin boolean parameter (default true) and new signerAppView UI block. NPendingRequests relabels signer type detection from Amber to Nip55 (shown as "signer app").
Localization Updates
packages/ndk_flutter/lib/l10n/* (13 locales + base interface)
All ARB resource files and generated localization Dart classes replace loginWithAmber key/getter with loginWithSignerApp across German, English, Spanish, Finnish, French, Italian, Japanese, Polish, Portuguese, Brazilian Portuguese, Russian, Chinese, and the base AppLocalizations interface.
Sample App & Build Config
packages/sample-app/lib/*, packages/sample-app/android/*, packages/ndk_flutter/pubspec.yaml
Sample app: amber_page.dart switches from Amberflutter() to const Nip55Signer() and updates permission types to Nip55Permission; main.dart replaces amberAvailable flag with signerAppAvailable; widgets_demo_page toggles enableSignerAppLogin. ndk_flutter/pubspec.yaml removes amberflutter dependency and registers Android plugin with package relaystr.ndk and class DartNdkPlugin. Sample app gradle files bump Kotlin to 2.2.20 and Android SDK to 36; gradle wrapper updated to 8.14.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Possibly related PRs

  • relaystr/ndk#623: Modifies saveAccountsState() in the same file; main PR adds nip55 signer persistence while the related PR refactors privateKey reading for WebEventSigner.
  • relaystr/ndk#542: Main PR removes amberflutter re-export from ndk_flutter.dart (replacing Amber with NIP-55), which directly conflicts with the retrieved PR that added the amberflutter export.
  • relaystr/ndk#604: Main PR updates account/signer restoration to instantiate signers via factory pattern; the related PR introduces the event/local signer factory APIs used in the main flow.

Suggested labels

enhancement


Suggested reviewers

  • frnandu
  • 1-leo

Poem

🐰 A signer app arrives with NIP-55's cheer,
No Amber now—the protocol's clear!
Content and intents in Android dance,
Thirteen tongues sing the new romance,
From Flutter dart to native Kotlin light, 🎉

🚥 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 PR title 'Feat/nip55 external signer' directly corresponds to the main changes: replacing Amber Flutter integration with NIP-55 external signer support across Android native code, Dart data sources, repositories, UI widgets, and localization strings.
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

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.95%. Comparing base (2452e67) to head (d88ebd9).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #657   +/-   ##
=======================================
  Coverage   71.94%   71.95%           
=======================================
  Files         210      210           
  Lines       10698    10698           
=======================================
+ Hits         7697     7698    +1     
+ Misses       3001     3000    -1     

☔ 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 on lines +97 to 127
Future<void> loginWithExternalSigner() async {
isWaitingForExternalSigner = true;

final amber = Amberflutter();
const signer = Nip55Signer();

final isAmberInstalled = await amber.isAppInstalled();
final isInstalled = await signer.isAppInstalled();

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

final amberFlutterDS = AmberFlutterDS(amber);

final amberResponse = await amber.getPublicKey();

final npub = amberResponse['signature'];
final pubkey = Nip19.decode(npub);
final loginResult = await signer.login();
if (loginResult == null) {
isWaitingForExternalSigner = false;
return;
}

final amberSigner = AmberEventSigner(
publicKey: pubkey,
amberFlutterDS: amberFlutterDS,
final externalSigner = Nip55EventSigner(
publicKey: loginResult.pubkey,
// pin the signer captured at login so later requests can be silent
nip55Signer: Nip55Signer(package: loginResult.package),
);

ndk.accounts.loginExternalSigner(signer: amberSigner);
ndk.accounts.loginExternalSigner(signer: externalSigner);

isWaitingForAmber = false;
isWaitingForExternalSigner = false;

await loggedIn();
}

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

Reset isWaitingForExternalSigner in a finally block.

If signer.isAppInstalled() or signer.login() throws, Line 98 sets waiting state to true and it is never cleared, leaving the login button disabled until rebuild/restart.

Suggested fix
 Future<void> loginWithExternalSigner() async {
-  isWaitingForExternalSigner = true;
-
-  const signer = Nip55Signer();
-
-  final isInstalled = await signer.isAppInstalled();
-
-  if (!isInstalled) {
-    isWaitingForExternalSigner = false;
-    launchUrl(Uri.parse('https://github.com/greenart7c3/Amber'));
-    return;
-  }
-
-  final loginResult = await signer.login();
-  if (loginResult == null) {
-    isWaitingForExternalSigner = false;
-    return;
-  }
-
-  final externalSigner = Nip55EventSigner(
-    publicKey: loginResult.pubkey,
-    // pin the signer captured at login so later requests can be silent
-    nip55Signer: Nip55Signer(package: loginResult.package),
-  );
-
-  ndk.accounts.loginExternalSigner(signer: externalSigner);
-
-  isWaitingForExternalSigner = false;
-
-  await loggedIn();
+  isWaitingForExternalSigner = true;
+  try {
+    const signer = Nip55Signer();
+    final isInstalled = await signer.isAppInstalled();
+
+    if (!isInstalled) {
+      await launchUrl(Uri.parse('https://github.com/greenart7c3/Amber'));
+      return;
+    }
+
+    final loginResult = await signer.login();
+    if (loginResult == null) return;
+
+    final externalSigner = Nip55EventSigner(
+      publicKey: loginResult.pubkey,
+      nip55Signer: Nip55Signer(package: loginResult.package),
+    );
+
+    ndk.accounts.loginExternalSigner(signer: externalSigner);
+    await loggedIn();
+  } finally {
+    isWaitingForExternalSigner = false;
+  }
 }
📝 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.

Suggested change
Future<void> loginWithExternalSigner() async {
isWaitingForExternalSigner = true;
final amber = Amberflutter();
const signer = Nip55Signer();
final isAmberInstalled = await amber.isAppInstalled();
final isInstalled = await signer.isAppInstalled();
if (!isAmberInstalled) {
if (!isInstalled) {
isWaitingForExternalSigner = false;
launchUrl(Uri.parse('https://github.com/greenart7c3/Amber'));
return;
}
final amberFlutterDS = AmberFlutterDS(amber);
final amberResponse = await amber.getPublicKey();
final npub = amberResponse['signature'];
final pubkey = Nip19.decode(npub);
final loginResult = await signer.login();
if (loginResult == null) {
isWaitingForExternalSigner = false;
return;
}
final amberSigner = AmberEventSigner(
publicKey: pubkey,
amberFlutterDS: amberFlutterDS,
final externalSigner = Nip55EventSigner(
publicKey: loginResult.pubkey,
// pin the signer captured at login so later requests can be silent
nip55Signer: Nip55Signer(package: loginResult.package),
);
ndk.accounts.loginExternalSigner(signer: amberSigner);
ndk.accounts.loginExternalSigner(signer: externalSigner);
isWaitingForAmber = false;
isWaitingForExternalSigner = false;
await loggedIn();
}
Future<void> loginWithExternalSigner() async {
isWaitingForExternalSigner = true;
try {
const signer = Nip55Signer();
final isInstalled = await signer.isAppInstalled();
if (!isInstalled) {
await launchUrl(Uri.parse('https://github.com/greenart7c3/Amber'));
return;
}
final loginResult = await signer.login();
if (loginResult == null) return;
final externalSigner = Nip55EventSigner(
publicKey: loginResult.pubkey,
nip55Signer: Nip55Signer(package: loginResult.package),
);
ndk.accounts.loginExternalSigner(signer: externalSigner);
await loggedIn();
} finally {
isWaitingForExternalSigner = false;
}
}
🤖 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 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.

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

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.

1 participant