diff --git a/packages/ndk_flutter/android/build.gradle b/packages/ndk_flutter/android/build.gradle index 26305fd27..6312bb6fd 100644 --- a/packages/ndk_flutter/android/build.gradle +++ b/packages/ndk_flutter/android/build.gradle @@ -1,6 +1,4 @@ -package android - -group = 'com.sebdeveloper6952.amberflutter.amberflutter' +group = 'relaystr.ndk' version = '1.0-SNAPSHOT' buildscript { @@ -11,7 +9,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.12.3' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.20" } } @@ -27,7 +25,7 @@ apply plugin: 'kotlin-android' android { if (project.android.hasProperty("namespace")) { - namespace = 'relastr.ndk' + namespace = 'relaystr.ndk' } compileSdk = 36 diff --git a/packages/ndk_flutter/android/settings.gradle b/packages/ndk_flutter/android/settings.gradle index cbdd0a405..aa877f0ab 100644 --- a/packages/ndk_flutter/android/settings.gradle +++ b/packages/ndk_flutter/android/settings.gradle @@ -1,7 +1,5 @@ -package android - plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0' } -rootProject.name = 'amberflutter' +rootProject.name = 'ndk_flutter' diff --git a/packages/ndk_flutter/android/src/main/AndroidManifest.xml b/packages/ndk_flutter/android/src/main/AndroidManifest.xml index a2f47b605..44bda0c13 100644 --- a/packages/ndk_flutter/android/src/main/AndroidManifest.xml +++ b/packages/ndk_flutter/android/src/main/AndroidManifest.xml @@ -1,2 +1,10 @@ + + + + + + + diff --git a/packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/DartNdkPlugin.kt b/packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/DartNdkPlugin.kt index fb8ff76c2..45edafc76 100644 --- a/packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/DartNdkPlugin.kt +++ b/packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/DartNdkPlugin.kt @@ -1,13 +1,13 @@ package relaystr.ndk import android.app.Activity +import android.content.ContentResolver +import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Handler +import android.os.Looper import android.util.Log -import androidx.annotation.NonNull -import com.google.gson.Gson -import android.content.Context - import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall @@ -19,6 +19,13 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.PluginRegistry +/// ndk_flutter native plugin. +/// +/// Hosts the NIP-55 "Android Signer Application" bridge (external signer apps +/// such as Amber, Primal, Aegis, ...). Communication happens either silently +/// through a ContentResolver query (when the user has pre-authorized the +/// permission) or, as a fallback, by launching the signer via an Intent and +/// reading the result in [onActivityResult]. class DartNdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener { private lateinit var _channel : MethodChannel @@ -29,29 +36,135 @@ class DartNdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, private val _intentRequestCode = 0 - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { _channel = MethodChannel(flutterPluginBinding.binaryMessenger, "ndk") _channel.setMethodCallHandler(this) _context = flutterPluginBinding.applicationContext } - override fun onMethodCall(call: MethodCall, result: Result) { - _result = result + private fun isPackageInstalled(context: Context, target: String): Boolean { + return context.packageManager.getInstalledApplications(0) + .find { info -> info.packageName == target } != null + } + + private fun isExternalSignerInstalled(context: Context): Boolean { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("$nostrSignerScheme:")) + return context.packageManager.queryIntentActivities(intent, 0).isNotEmpty() + } + + override fun onMethodCall(call: MethodCall, result: Result) { when (call.method) { - "example" -> { - Log.i("example", "example onMethodCall") + nostrSignerScheme -> { + _result = MethodResultWrapper(result) + + val paramsMap = call.arguments as? HashMap<*, *> + if (paramsMap == null) { + Log.d("onMethodCall", "paramsMap is null") + return + } + + val requestType = paramsMap[keyType] as? String ?: "" + val currentUser = paramsMap[keyCurrentUser] as? String ?: "" + val pubKey = paramsMap[keyPubKey] as? String + ?: paramsMap["pubkey"] as? String + ?: "" + val id = paramsMap[keyId] as? String ?: "" + val uriData = paramsMap[keyUriData] as? String ?: "" + val permissions = paramsMap[keyPermissions] as? String ?: "" + // Signer app package captured at login (Amber, Primal, ...). + // Empty for get_public_key / legacy accounts. + val signerPackage = paramsMap[keyPackage] as? String ?: "" + + // First try the silent ContentResolver path (pre-authorized + // permissions). Only attempt it when we know which signer to + // query (a captured package): querying a foreign provider + // (e.g. Amber for a Primal account) returns wrong/empty data. + // get_public_key (login) is Intent-only per NIP-55. + if (requestType != "get_public_key" && signerPackage.isNotEmpty()) { + val data = getDataFromContentResolver( + requestType.uppercase(), + arrayOf(uriData, pubKey, currentUser), + _context.contentResolver, + signerPackage, + ) + if (!data.isNullOrEmpty()) { + Log.d("onMethodCall", "content resolver got data") + _result.success(data) + return + } + } + + // Fallback: launch the signer app via Intent. + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse("$nostrSignerScheme:$uriData") + ) + intent.putExtra(keyType, requestType) + intent.putExtra(keyCurrentUser, currentUser) + intent.putExtra(keyPubKey, pubKey) + intent.putExtra("pubkey", pubKey) + intent.putExtra(keyId, id) + intent.putExtra(keyPermissions, permissions) + // Target the captured signer directly (no app chooser). Empty + // for login, so the user can pick a signer the first time. + if (signerPackage.isNotEmpty()) { + intent.setPackage(signerPackage) + intent.putExtra(keyPackage, signerPackage) + } + + try { + _activity?.startActivityForResult(intent, _intentRequestCode) + } catch (e: Exception) { + Log.d("onMethodCall", "startActivityForResult failed for '$signerPackage': ${e.message}") + _result.success(HashMap()) + } + } + + "isAppInstalled" -> { + val paramsMap = call.arguments as? HashMap<*, *> + val packageName = paramsMap?.get("packageName") as? String + val isInstalled = if (packageName.isNullOrEmpty()) { + isExternalSignerInstalled(_context) + } else { + isPackageInstalled(_context, packageName) + } + result.success(isInstalled) } else -> { result.notImplemented() - return } } } override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?): Boolean { - return false; + if (requestCode == _intentRequestCode) { + if (resultCode == Activity.RESULT_OK && intent != null) { + val dataMap: HashMap = HashMap() + if (intent.hasExtra(keyResult)) { + val result = intent.getStringExtra(keyResult) + dataMap[keyResult] = result + // keep `signature` populated for backwards compatibility + dataMap[keySignature] = result + } + if (intent.hasExtra(keySignature)) { + dataMap[keySignature] = intent.getStringExtra(keySignature) + } + if (intent.hasExtra(keyPackage)) { + dataMap[keyPackage] = intent.getStringExtra(keyPackage) + } + if (intent.hasExtra(keyId)) { + dataMap[keyId] = intent.getStringExtra(keyId) + } + if (intent.hasExtra(keyEvent)) { + dataMap[keyEvent] = intent.getStringExtra(keyEvent) + } + + _result.success(dataMap) + return true + } + } + return false } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { @@ -76,5 +189,86 @@ class DartNdkPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, _activity = null } + /* + Content resolver path adapted from: + https://github.com/0xchat-app/nostr-dart/blob/main/android/src/main/kotlin/com/oxchat/nostrcore/ChatcorePlugin.kt + */ + private fun getDataFromContentResolver( + type: String, + uriData: Array, + resolver: ContentResolver, + signerPackage: String, + ): HashMap? { + try { + resolver.query( + Uri.parse("content://${signerPackage}.$type"), + uriData, + null, + null, + null + ).use { + if (it == null) { + Log.d("getDataFromResolver", "resolver query is NULL") + return null + } + if (it.moveToFirst()) { + // The signer reports it cannot answer silently (permission + // not granted / user denied): fall back to the Intent so + // the user can approve, instead of returning empty data. + val rejectedIndex = it.getColumnIndex("rejected") + if (rejectedIndex >= 0) { + Log.d("getDataFromResolver", "request rejected -> fallback to intent") + return null + } + + val dataMap: HashMap = HashMap() + val resultIndex = it.getColumnIndex("result") + if (resultIndex >= 0) { + val result = it.getString(resultIndex) + dataMap["result"] = result + dataMap["signature"] = result + } + val index = it.getColumnIndex("signature") + if (index >= 0) { + dataMap["signature"] = it.getString(index) + } + val indexJson = it.getColumnIndex("event") + if (indexJson >= 0) { + dataMap["event"] = it.getString(indexJson) + } + + // Only short-circuit the Intent if we actually got a result; + // an empty/absent result means the signer didn't answer. + if (dataMap["signature"].isNullOrEmpty()) { + Log.d("getDataFromResolver", "empty result -> fallback to intent") + return null + } + return dataMap + } + } + } catch (e: Exception) { + Log.d("contentResolver", e.message ?: "unknown error") + return null + } + return null + } +} + + +private class MethodResultWrapper internal constructor(result: MethodChannel.Result) : + MethodChannel.Result { + private val methodResult: MethodChannel.Result = result + private val handler: Handler = Handler(Looper.getMainLooper()) + + override fun success(result: Any?) { + handler.post { methodResult.success(result) } + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + handler.post { methodResult.error(errorCode, errorMessage, errorDetails) } + } + override fun notImplemented() { + handler.post { methodResult.notImplemented() } + } } diff --git a/packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/Nip55Constants.kt b/packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/Nip55Constants.kt new file mode 100644 index 000000000..5ab3ef5fb --- /dev/null +++ b/packages/ndk_flutter/android/src/main/kotlin/relaystr/ndk/Nip55Constants.kt @@ -0,0 +1,19 @@ +package relaystr.ndk + +/// Constants for the NIP-55 "Android Signer Application" protocol. +/// +/// NIP-55 is a protocol implemented by several external signer apps +/// (Amber, Primal, Aegis, ...). The wire format below is generic. + +const val nostrSignerScheme = "nostrsigner" + +const val keyType = "type" +const val keyCurrentUser = "current_user" +const val keyUriData = "uri_data" +const val keyPubKey = "pubKey" +const val keyId = "id" +const val keyPermissions = "permissions" +const val keyResult = "result" +const val keySignature = "signature" +const val keyPackage = "package" +const val keyEvent = "event" diff --git a/packages/ndk_flutter/lib/data_layer/data_sources/amber_flutter.dart b/packages/ndk_flutter/lib/data_layer/data_sources/amber_flutter.dart deleted file mode 100644 index 88d18d31d..000000000 --- a/packages/ndk_flutter/lib/data_layer/data_sources/amber_flutter.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:amberflutter/amberflutter.dart'; - -/// amber DS -class AmberFlutterDS { - /// the amber obj - final Amberflutter amber; - - /// ... - AmberFlutterDS(this.amber); -} diff --git a/packages/ndk_flutter/lib/data_layer/data_sources/nip55_signer.dart b/packages/ndk_flutter/lib/data_layer/data_sources/nip55_signer.dart new file mode 100644 index 000000000..be67eab09 --- /dev/null +++ b/packages/ndk_flutter/lib/data_layer/data_sources/nip55_signer.dart @@ -0,0 +1,218 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:ndk/ndk.dart'; + +/// A permission that can be requested when logging in with a NIP-55 external +/// signer, so the signer can pre-authorize silent (ContentResolver) responses. +/// +/// See https://github.com/nostr-protocol/nips/blob/master/55.md +class Nip55Permission { + const Nip55Permission({required this.type, this.kind}); + + /// The request type to authorize, e.g. `sign_event`, `nip04_encrypt`. + final String type; + + /// Optional event kind, only relevant for `sign_event`. + final int? kind; + + Map toJson() { + return {'type': type, if (kind != null) 'kind': kind}; + } +} + +/// Result of a NIP-55 login (`get_public_key`). +class Nip55LoginResult { + const Nip55LoginResult({required this.pubkey, this.package}); + + /// The user's public key, in hex format. + final String pubkey; + + /// The signer app package name (e.g. Amber, Primal), if the signer returned + /// it. Used to target the same signer for subsequent requests. + final String? package; +} + +/// Dart bridge to a NIP-55 "Android Signer Application". +/// +/// NIP-55 is a protocol implemented by several external signer apps +/// (Amber, Primal, Aegis, ...). This class talks to whichever compatible +/// signer is installed through the native `ndk` method channel +/// ([DartNdkPlugin]). +/// +/// Every method resolves to a `Map` that contains (at least) a `signature` +/// key with the result, mirroring the historical `amberflutter` API. +class Nip55Signer { + /// The method channel shared with the native [DartNdkPlugin]. + static const MethodChannel _channel = MethodChannel('ndk'); + + /// Default permissions requested at login so common operations can be + /// answered silently (via ContentResolver) without reopening the signer. + static const List defaultPermissions = [ + Nip55Permission(type: 'sign_event'), + Nip55Permission(type: 'nip04_encrypt'), + Nip55Permission(type: 'nip04_decrypt'), + Nip55Permission(type: 'nip44_encrypt'), + Nip55Permission(type: 'nip44_decrypt'), + ]; + + /// The signer app package (e.g. Amber, Primal), captured at login. Used to + /// target the right signer for both the ContentResolver and the Intent. + /// When `null`, the native side lets Android route through a compatible + /// signer app. + final String? package; + + const Nip55Signer({this.package}); + + /// Whether a NIP-55 compatible external signer is installed. + Future isAppInstalled() async { + final data = await _channel.invokeMethod('isAppInstalled'); + return data ?? false; + } + + /// Requests the user's public key (login). Optionally pre-authorizes + /// [permissions] so subsequent requests can be answered silently. + /// + /// Returns the raw signer response map. Most callers want + /// [getPublicKeyHex] instead. + Future> getPublicKey({ + List? permissions, + }) async { + final arguments = { + 'type': 'get_public_key', + 'uri_data': 'login', + }; + if (permissions != null) { + arguments['permissions'] = jsonEncode(permissions); + } + return _invoke(arguments); + } + + /// Requests the user's public key and returns it as a hex pubkey, or `null` + /// if the user rejected or no key was returned. + /// + /// NIP-55 returns the key in the `result` field, in hex format. Older + /// signers (and Amber legacy) may return an npub instead, so both are + /// accepted. See https://github.com/nostr-protocol/nips/blob/master/55.md + Future getPublicKeyHex({List? permissions}) async { + return (await login(permissions: permissions))?.pubkey; + } + + /// Logs in: requests the user's public key, pre-authorizing [permissions] + /// (defaults to [defaultPermissions]), and captures the signer app package. + /// Returns `null` if the user rejected or no key was returned. + Future login({List? permissions}) async { + final response = await getPublicKey( + permissions: permissions ?? defaultPermissions, + ); + final raw = (response['result'] ?? response['signature']) as String?; + if (raw == null || raw.isEmpty) return null; + final pubkey = raw.startsWith('npub') ? Nip19.decode(raw) : raw; + final pkg = response['package'] as String?; + return Nip55LoginResult( + pubkey: pubkey, + package: (pkg != null && pkg.isNotEmpty) ? pkg : null, + ); + } + + Future> signEvent({ + required String currentUser, + required String eventJson, + String? id, + }) { + return _invoke({ + 'type': 'sign_event', + 'current_user': currentUser, + 'uri_data': eventJson, + 'id': id, + }); + } + + Future> nip04Encrypt({ + required String plaintext, + required String currentUser, + required String pubKey, + String? id, + }) { + return _invoke( + _encryptArgs('nip04_encrypt', plaintext, currentUser, pubKey, id), + ); + } + + Future> nip04Decrypt({ + required String ciphertext, + required String currentUser, + required String pubKey, + String? id, + }) { + return _invoke( + _encryptArgs('nip04_decrypt', ciphertext, currentUser, pubKey, id), + ); + } + + Future> nip44Encrypt({ + required String plaintext, + required String currentUser, + required String pubKey, + String? id, + }) { + return _invoke( + _encryptArgs('nip44_encrypt', plaintext, currentUser, pubKey, id), + ); + } + + Future> nip44Decrypt({ + required String ciphertext, + required String currentUser, + required String pubKey, + String? id, + }) { + return _invoke( + _encryptArgs('nip44_decrypt', ciphertext, currentUser, pubKey, id), + ); + } + + Future> decryptZapEvent({ + required String eventJson, + required String currentUser, + String? id, + }) { + return _invoke({ + 'type': 'decrypt_zap_event', + 'uri_data': eventJson, + 'current_user': currentUser, + 'id': id, + }); + } + + Map _encryptArgs( + String type, + String uriData, + String currentUser, + String pubKey, + String? id, + ) { + return { + 'type': type, + 'uri_data': uriData, + 'current_user': currentUser, + // both casings are sent to stay compatible with signer implementations + 'pubKey': pubKey, + 'pubkey': pubKey, + 'id': id, + }; + } + + Future> _invoke(Map arguments) async { + // Target the signer captured at login (if any), so the request goes to the + // right app and can be answered silently via its ContentResolver. + if (package != null) { + arguments['package'] = package; + } + final data = await _channel.invokeMethod>( + 'nostrsigner', + arguments, + ); + return data ?? {}; + } +} diff --git a/packages/ndk_flutter/lib/data_layer/repositories/signers/amber_event_signer.dart b/packages/ndk_flutter/lib/data_layer/repositories/signers/nip55_event_signer.dart similarity index 74% rename from packages/ndk_flutter/lib/data_layer/repositories/signers/amber_event_signer.dart rename to packages/ndk_flutter/lib/data_layer/repositories/signers/nip55_event_signer.dart index ad5295bd3..937d09762 100644 --- a/packages/ndk_flutter/lib/data_layer/repositories/signers/amber_event_signer.dart +++ b/packages/ndk_flutter/lib/data_layer/repositories/signers/nip55_event_signer.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:ndk/ndk.dart'; import 'package:rxdart/rxdart.dart'; -import '../../data_sources/amber_flutter.dart'; +import '../../data_sources/nip55_signer.dart'; /// Internal class to track a pending request with its completer class _PendingRequestEntry { @@ -12,11 +12,13 @@ class _PendingRequestEntry { _PendingRequestEntry(this.completer, this.request); } -/// amber (external app) https://github.com/greenart7c3/Amber singer -class AmberEventSigner - with ConcurrencyLimiterMixin - implements EventSigner { - final AmberFlutterDS amberFlutterDS; +/// Event signer backed by a NIP-55 external signer application. +/// +/// NIP-55 is a protocol implemented by several external signer apps +/// (Amber, Primal, Aegis, ...). This signer delegates all cryptographic +/// operations to whichever compatible signer is installed via [Nip55Signer]. +class Nip55EventSigner with ConcurrencyLimiterMixin implements EventSigner { + final Nip55Signer nip55Signer; final String publicKey; @@ -29,31 +31,32 @@ class AmberEventSigner @override final int maxConcurrentRequests; - /// Default Amber concurrency. Lower this if you target a flow that + /// Default external-signer concurrency. Lower this if you target a flow that /// prompts the user via Android intents for every request. static const int defaultMaxConcurrentRequests = 100; - /// get a amber event signer - AmberEventSigner({ + /// get a NIP-55 external signer + Nip55EventSigner({ required this.publicKey, - required this.amberFlutterDS, + required this.nip55Signer, this.maxConcurrentRequests = defaultMaxConcurrentRequests, - }) : assert(maxConcurrentRequests > 0, - 'maxConcurrentRequests must be > 0'); + }) : assert(maxConcurrentRequests > 0, 'maxConcurrentRequests must be > 0'); - String get _npub => - publicKey.startsWith('npub') ? publicKey : Nip19.encodePubKey(publicKey); + /// NIP-55 expects pubkeys in hex format ("All pubkeys in this NIP are in hex + /// format"), so `current_user` is always sent as hex. + String get _currentUser => + publicKey.startsWith('npub') ? Nip19.decode(publicKey) : publicKey; String _extractResult(Map map) { final result = map['signature'] as String?; if (result == null || result.isEmpty) { - throw Exception('Empty result from Amber'); + throw Exception('Empty result from external signer'); } return result; } String _generateRequestId() { - return 'amber_${DateTime.now().millisecondsSinceEpoch}_${_requestCounter++}'; + return 'nip55_${DateTime.now().millisecondsSinceEpoch}_${_requestCounter++}'; } void _notifyPendingRequestsChange() { @@ -90,15 +93,15 @@ class AmberEventSigner ); _notifyPendingRequestsChange(); - // Throttle the actual call to Amber; queued requests still appear in + // Throttle the actual call to the signer; queued requests still appear in // `pendingRequests` so the UI sees the full backlog. If the request was - // cancelled while queued, skip the Amber call entirely. + // cancelled while queued, skip the signer call entirely. runThrottled(() async { - if (!_pendingRequests.containsKey(requestId)) { - throw SignerRequestCancelledException(requestId); - } - return await operation(); - }) + if (!_pendingRequests.containsKey(requestId)) { + throw SignerRequestCancelledException(requestId); + } + return await operation(); + }) .then((result) { if (!completer.isCompleted) { completer.complete(result); @@ -121,8 +124,8 @@ class AmberEventSigner @override Future sign(Nip01Event event) async { return _trackRequest(SignerMethod.signEvent, () async { - final map = await amberFlutterDS.amber.signEvent( - currentUser: _npub, + final map = await nip55Signer.signEvent( + currentUser: _currentUser, eventJson: Nip01EventModel.fromEntity(event).toJsonString(), id: event.id, ); @@ -140,9 +143,9 @@ class AmberEventSigner return _trackRequest( SignerMethod.nip04Decrypt, () async { - final map = await amberFlutterDS.amber.nip04Decrypt( + final map = await nip55Signer.nip04Decrypt( ciphertext: msg, - currentUser: _npub, + currentUser: _currentUser, pubKey: destPubKey, id: id, ); @@ -158,9 +161,9 @@ class AmberEventSigner return _trackRequest( SignerMethod.nip04Encrypt, () async { - final map = await amberFlutterDS.amber.nip04Encrypt( + final map = await nip55Signer.nip04Encrypt( plaintext: msg, - currentUser: _npub, + currentUser: _currentUser, pubKey: destPubKey, id: id, ); @@ -184,9 +187,9 @@ class AmberEventSigner return _trackRequest( SignerMethod.nip44Encrypt, () async { - final map = await amberFlutterDS.amber.nip44Encrypt( + final map = await nip55Signer.nip44Encrypt( plaintext: plaintext, - currentUser: _npub, + currentUser: _currentUser, pubKey: recipientPubKey, ); return _extractResult(map); @@ -204,9 +207,9 @@ class AmberEventSigner return _trackRequest( SignerMethod.nip44Decrypt, () async { - final map = await amberFlutterDS.amber.nip44Decrypt( + final map = await nip55Signer.nip44Decrypt( ciphertext: ciphertext, - currentUser: _npub, + currentUser: _currentUser, pubKey: senderPubKey, ); return _extractResult(map); diff --git a/packages/ndk_flutter/lib/l10n/app_de.arb b/packages/ndk_flutter/lib/l10n/app_de.arb index c366fba14..7f6bcad28 100644 --- a/packages/ndk_flutter/lib/l10n/app_de.arb +++ b/packages/ndk_flutter/lib/l10n/app_de.arb @@ -29,7 +29,7 @@ } }, "showNostrConnectQrcode": "Nostr-Connect-QR-Code anzeigen", - "loginWithAmber": "Mit Amber anmelden", + "loginWithSignerApp": "Mit Signer-App anmelden", "nostrConnectUrl": "Nostr-Connect-URL", "copy": "Kopieren", "addAccount": "Konto hinzufügen", diff --git a/packages/ndk_flutter/lib/l10n/app_en.arb b/packages/ndk_flutter/lib/l10n/app_en.arb index 5c9b9bc40..13d2278c9 100644 --- a/packages/ndk_flutter/lib/l10n/app_en.arb +++ b/packages/ndk_flutter/lib/l10n/app_en.arb @@ -86,9 +86,9 @@ "@showNostrConnectQrcode": { "description": "Button text to show nostr connect QR code" }, - "loginWithAmber": "Login with amber", - "@loginWithAmber": { - "description": "Button text to login with Amber" + "loginWithSignerApp": "Login with signer app", + "@loginWithSignerApp": { + "description": "Button text to login with an external Nostr signer app" }, "nostrConnectUrl": "Nostr connect URL", "@nostrConnectUrl": { diff --git a/packages/ndk_flutter/lib/l10n/app_es.arb b/packages/ndk_flutter/lib/l10n/app_es.arb index f07fab16c..17111b111 100644 --- a/packages/ndk_flutter/lib/l10n/app_es.arb +++ b/packages/ndk_flutter/lib/l10n/app_es.arb @@ -20,7 +20,7 @@ "bunkerAuthentication": "Autenticación Bunker", "tapToOpen": "Toca para abrir: {url}", "showNostrConnectQrcode": "Mostrar código QR de nostr connect", - "loginWithAmber": "Iniciar sesión con Amber", + "loginWithSignerApp": "Iniciar sesión con app de firma", "nostrConnectUrl": "URL de conexión Nostr", "copy": "Copiar", "addAccount": "Añadir cuenta", diff --git a/packages/ndk_flutter/lib/l10n/app_fi.arb b/packages/ndk_flutter/lib/l10n/app_fi.arb index b83f118f7..977e290ec 100644 --- a/packages/ndk_flutter/lib/l10n/app_fi.arb +++ b/packages/ndk_flutter/lib/l10n/app_fi.arb @@ -29,7 +29,7 @@ } }, "showNostrConnectQrcode": "Näytä Nostr Connect -QR-koodi", - "loginWithAmber": "Kirjaudu Amberilla", + "loginWithSignerApp": "Kirjaudu allekirjoitussovelluksella", "nostrConnectUrl": "Nostr Connect -URL", "copy": "Kopioi", "addAccount": "Lisää tili", diff --git a/packages/ndk_flutter/lib/l10n/app_fr.arb b/packages/ndk_flutter/lib/l10n/app_fr.arb index 30c9eab98..ea41ab3f3 100644 --- a/packages/ndk_flutter/lib/l10n/app_fr.arb +++ b/packages/ndk_flutter/lib/l10n/app_fr.arb @@ -20,7 +20,7 @@ "bunkerAuthentication": "Authentification Bunker", "tapToOpen": "Appuyez pour ouvrir: {url}", "showNostrConnectQrcode": "Afficher le code QR nostr connect", - "loginWithAmber": "Se connecter avec Amber", + "loginWithSignerApp": "Se connecter avec une app de signature", "nostrConnectUrl": "URL de connexion Nostr", "copy": "Copier", "addAccount": "Ajouter un compte", diff --git a/packages/ndk_flutter/lib/l10n/app_it.arb b/packages/ndk_flutter/lib/l10n/app_it.arb index d8fe82e2a..706ec9910 100644 --- a/packages/ndk_flutter/lib/l10n/app_it.arb +++ b/packages/ndk_flutter/lib/l10n/app_it.arb @@ -29,7 +29,7 @@ } }, "showNostrConnectQrcode": "Mostra QR code Nostr Connect", - "loginWithAmber": "Accedi con Amber", + "loginWithSignerApp": "Accedi con app di firma", "nostrConnectUrl": "URL Nostr Connect", "copy": "Copia", "addAccount": "Aggiungi account", diff --git a/packages/ndk_flutter/lib/l10n/app_ja.arb b/packages/ndk_flutter/lib/l10n/app_ja.arb index efbf7f105..68c4e8391 100644 --- a/packages/ndk_flutter/lib/l10n/app_ja.arb +++ b/packages/ndk_flutter/lib/l10n/app_ja.arb @@ -20,7 +20,7 @@ "bunkerAuthentication": "バンカー認証", "tapToOpen": "タップして開く:{url}", "showNostrConnectQrcode": "nostr connect QRコードを表示", - "loginWithAmber": "Amberでログイン", + "loginWithSignerApp": "署名アプリでログイン", "nostrConnectUrl": "Nostr接続URL", "copy": "コピー", "addAccount": "アカウントを追加", diff --git a/packages/ndk_flutter/lib/l10n/app_localizations.dart b/packages/ndk_flutter/lib/l10n/app_localizations.dart index 14b432684..0f9236703 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations.dart @@ -237,11 +237,11 @@ abstract class AppLocalizations { /// **'Show nostr connect qrcode'** String get showNostrConnectQrcode; - /// Button text to login with Amber + /// Button text to login with an external Nostr signer app /// /// In en, this message translates to: - /// **'Login with amber'** - String get loginWithAmber; + /// **'Login with signer app'** + String get loginWithSignerApp; /// Title for nostr connect URL dialog /// diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_de.dart b/packages/ndk_flutter/lib/l10n/app_localizations_de.dart index a11e26516..ca770a131 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_de.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_de.dart @@ -71,7 +71,7 @@ class AppLocalizationsDe extends AppLocalizations { String get showNostrConnectQrcode => 'Nostr-Connect-QR-Code anzeigen'; @override - String get loginWithAmber => 'Mit Amber anmelden'; + String get loginWithSignerApp => 'Mit Signer-App anmelden'; @override String get nostrConnectUrl => 'Nostr-Connect-URL'; diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_en.dart b/packages/ndk_flutter/lib/l10n/app_localizations_en.dart index 09e888f98..846b5037f 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_en.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_en.dart @@ -71,7 +71,7 @@ class AppLocalizationsEn extends AppLocalizations { String get showNostrConnectQrcode => 'Show nostr connect qrcode'; @override - String get loginWithAmber => 'Login with amber'; + String get loginWithSignerApp => 'Login with signer app'; @override String get nostrConnectUrl => 'Nostr connect URL'; diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_es.dart b/packages/ndk_flutter/lib/l10n/app_localizations_es.dart index efd0b0846..3f7c7bf2c 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_es.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_es.dart @@ -71,7 +71,7 @@ class AppLocalizationsEs extends AppLocalizations { String get showNostrConnectQrcode => 'Mostrar código QR de nostr connect'; @override - String get loginWithAmber => 'Iniciar sesión con Amber'; + String get loginWithSignerApp => 'Iniciar sesión con app de firma'; @override String get nostrConnectUrl => 'URL de conexión Nostr'; diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_fi.dart b/packages/ndk_flutter/lib/l10n/app_localizations_fi.dart index 2c5496f30..acb2e3941 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_fi.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_fi.dart @@ -71,7 +71,7 @@ class AppLocalizationsFi extends AppLocalizations { String get showNostrConnectQrcode => 'Näytä Nostr Connect -QR-koodi'; @override - String get loginWithAmber => 'Kirjaudu Amberilla'; + String get loginWithSignerApp => 'Kirjaudu allekirjoitussovelluksella'; @override String get nostrConnectUrl => 'Nostr Connect -URL'; diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_fr.dart b/packages/ndk_flutter/lib/l10n/app_localizations_fr.dart index 8405372f4..05ee238e4 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_fr.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_fr.dart @@ -71,7 +71,7 @@ class AppLocalizationsFr extends AppLocalizations { String get showNostrConnectQrcode => 'Afficher le code QR nostr connect'; @override - String get loginWithAmber => 'Se connecter avec Amber'; + String get loginWithSignerApp => 'Se connecter avec une app de signature'; @override String get nostrConnectUrl => 'URL de connexion Nostr'; diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_it.dart b/packages/ndk_flutter/lib/l10n/app_localizations_it.dart index 1a5d377fd..7429d18cb 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_it.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_it.dart @@ -71,7 +71,7 @@ class AppLocalizationsIt extends AppLocalizations { String get showNostrConnectQrcode => 'Mostra QR code Nostr Connect'; @override - String get loginWithAmber => 'Accedi con Amber'; + String get loginWithSignerApp => 'Accedi con app di firma'; @override String get nostrConnectUrl => 'URL Nostr Connect'; diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_ja.dart b/packages/ndk_flutter/lib/l10n/app_localizations_ja.dart index 63dbfd7ad..e2c190737 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_ja.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_ja.dart @@ -71,7 +71,7 @@ class AppLocalizationsJa extends AppLocalizations { String get showNostrConnectQrcode => 'nostr connect QRコードを表示'; @override - String get loginWithAmber => 'Amberでログイン'; + String get loginWithSignerApp => '署名アプリでログイン'; @override String get nostrConnectUrl => 'Nostr接続URL'; diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_pl.dart b/packages/ndk_flutter/lib/l10n/app_localizations_pl.dart index c7f4aa4ef..80b8685a8 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_pl.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_pl.dart @@ -71,7 +71,7 @@ class AppLocalizationsPl extends AppLocalizations { String get showNostrConnectQrcode => 'Pokaż kod QR Nostr Connect'; @override - String get loginWithAmber => 'Zaloguj się przez Amber'; + String get loginWithSignerApp => 'Zaloguj się przez aplikację podpisującą'; @override String get nostrConnectUrl => 'URL Nostr Connect'; diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_pt.dart b/packages/ndk_flutter/lib/l10n/app_localizations_pt.dart index c4b73a7cc..eeb363e61 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_pt.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_pt.dart @@ -71,7 +71,7 @@ class AppLocalizationsPt extends AppLocalizations { String get showNostrConnectQrcode => 'Mostrar código QR do Nostr Connect'; @override - String get loginWithAmber => 'Iniciar sessão com Amber'; + String get loginWithSignerApp => 'Iniciar sessão com app de assinatura'; @override String get nostrConnectUrl => 'URL de ligação Nostr'; @@ -1142,7 +1142,7 @@ class AppLocalizationsPtBr extends AppLocalizationsPt { String get showNostrConnectQrcode => 'Mostrar QR code de conexão Nostr'; @override - String get loginWithAmber => 'Entrar com Amber'; + String get loginWithSignerApp => 'Entrar com app de assinatura'; @override String get nostrConnectUrl => 'URL de conexão Nostr'; diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_ru.dart b/packages/ndk_flutter/lib/l10n/app_localizations_ru.dart index 895c45771..9674a3ca4 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_ru.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_ru.dart @@ -71,7 +71,7 @@ class AppLocalizationsRu extends AppLocalizations { String get showNostrConnectQrcode => 'Показать QR-код nostr connect'; @override - String get loginWithAmber => 'Войти через Amber'; + String get loginWithSignerApp => 'Войти через приложение для подписи'; @override String get nostrConnectUrl => 'URL подключения Nostr'; diff --git a/packages/ndk_flutter/lib/l10n/app_localizations_zh.dart b/packages/ndk_flutter/lib/l10n/app_localizations_zh.dart index 2e4dc1aa8..3b8281867 100644 --- a/packages/ndk_flutter/lib/l10n/app_localizations_zh.dart +++ b/packages/ndk_flutter/lib/l10n/app_localizations_zh.dart @@ -71,7 +71,7 @@ class AppLocalizationsZh extends AppLocalizations { String get showNostrConnectQrcode => '显示 nostr connect 二维码'; @override - String get loginWithAmber => '使用 Amber 登录'; + String get loginWithSignerApp => '使用签名应用登录'; @override String get nostrConnectUrl => 'Nostr 连接 URL'; diff --git a/packages/ndk_flutter/lib/l10n/app_pl.arb b/packages/ndk_flutter/lib/l10n/app_pl.arb index 282d46136..88cb2ba3d 100644 --- a/packages/ndk_flutter/lib/l10n/app_pl.arb +++ b/packages/ndk_flutter/lib/l10n/app_pl.arb @@ -29,7 +29,7 @@ } }, "showNostrConnectQrcode": "Pokaż kod QR Nostr Connect", - "loginWithAmber": "Zaloguj się przez Amber", + "loginWithSignerApp": "Zaloguj się przez aplikację podpisującą", "nostrConnectUrl": "URL Nostr Connect", "copy": "Kopiuj", "addAccount": "Dodaj konto", diff --git a/packages/ndk_flutter/lib/l10n/app_pt.arb b/packages/ndk_flutter/lib/l10n/app_pt.arb index 9066de071..b14b61015 100644 --- a/packages/ndk_flutter/lib/l10n/app_pt.arb +++ b/packages/ndk_flutter/lib/l10n/app_pt.arb @@ -20,7 +20,7 @@ "bunkerAuthentication": "Autenticação Bunker", "tapToOpen": "Toque para abrir: {url}", "showNostrConnectQrcode": "Mostrar código QR do Nostr Connect", - "loginWithAmber": "Iniciar sessão com Amber", + "loginWithSignerApp": "Iniciar sessão com app de assinatura", "nostrConnectUrl": "URL de ligação Nostr", "copy": "Copiar", "addAccount": "Adicionar conta", diff --git a/packages/ndk_flutter/lib/l10n/app_pt_BR.arb b/packages/ndk_flutter/lib/l10n/app_pt_BR.arb index 19f40afa9..345f48091 100644 --- a/packages/ndk_flutter/lib/l10n/app_pt_BR.arb +++ b/packages/ndk_flutter/lib/l10n/app_pt_BR.arb @@ -20,7 +20,7 @@ "bunkerAuthentication": "Autenticação Bunker", "tapToOpen": "Toque para abrir: {url}", "showNostrConnectQrcode": "Mostrar QR code de conexão Nostr", - "loginWithAmber": "Entrar com Amber", + "loginWithSignerApp": "Entrar com app de assinatura", "nostrConnectUrl": "URL de conexão Nostr", "copy": "Copiar", "addAccount": "Adicionar conta", diff --git a/packages/ndk_flutter/lib/l10n/app_ru.arb b/packages/ndk_flutter/lib/l10n/app_ru.arb index a65b35999..bb983d907 100644 --- a/packages/ndk_flutter/lib/l10n/app_ru.arb +++ b/packages/ndk_flutter/lib/l10n/app_ru.arb @@ -20,7 +20,7 @@ "bunkerAuthentication": "Аутентификация Bunker", "tapToOpen": "Нажмите, чтобы открыть: {url}", "showNostrConnectQrcode": "Показать QR-код nostr connect", - "loginWithAmber": "Войти через Amber", + "loginWithSignerApp": "Войти через приложение для подписи", "nostrConnectUrl": "URL подключения Nostr", "copy": "Копировать", "addAccount": "Добавить аккаунт", diff --git a/packages/ndk_flutter/lib/l10n/app_zh.arb b/packages/ndk_flutter/lib/l10n/app_zh.arb index 0568fb94b..9f61b3e24 100644 --- a/packages/ndk_flutter/lib/l10n/app_zh.arb +++ b/packages/ndk_flutter/lib/l10n/app_zh.arb @@ -20,7 +20,7 @@ "bunkerAuthentication": "Bunker 身份验证", "tapToOpen": "点击打开:{url}", "showNostrConnectQrcode": "显示 nostr connect 二维码", - "loginWithAmber": "使用 Amber 登录", + "loginWithSignerApp": "使用签名应用登录", "nostrConnectUrl": "Nostr 连接 URL", "copy": "复制", "addAccount": "添加账户", diff --git a/packages/ndk_flutter/lib/main/ndk_flutter.dart b/packages/ndk_flutter/lib/main/ndk_flutter.dart index d7a93fb4b..5bb776f20 100644 --- a/packages/ndk_flutter/lib/main/ndk_flutter.dart +++ b/packages/ndk_flutter/lib/main/ndk_flutter.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:amberflutter/amberflutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:ndk/data_layer/repositories/signers/nip46_event_signer.dart'; @@ -13,8 +12,8 @@ import 'package:nip07_event_signer/nip07_event_signer.dart'; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; -import '../data_layer/data_sources/amber_flutter.dart'; -import '../data_layer/repositories/signers/amber_event_signer.dart'; +import '../data_layer/data_sources/nip55_signer.dart'; +import '../data_layer/repositories/signers/nip55_event_signer.dart'; class NdkFlutter { final Ndk ndk; @@ -109,9 +108,16 @@ class NdkFlutter { continue; } - if (account.signer is AmberEventSigner) { + if (account.signer is Nip55EventSigner) { + // `signerSeed` holds the signer app package (Amber, Primal, ...) so + // silent signing keeps working after restart. + final signer = account.signer as Nip55EventSigner; accounts.accounts.add( - NostrAccount(kind: AccountKinds.amber, pubkey: account.pubkey), + NostrAccount( + kind: AccountKinds.nip55, + pubkey: account.pubkey, + signerSeed: signer.nip55Signer.package, + ), ); continue; } @@ -175,16 +181,15 @@ class NdkFlutter { continue; } - if (account.kind == AccountKinds.amber) { - final amber = Amberflutter(); - final amberFlutterDS = AmberFlutterDS(amber); - + if (account.kind == AccountKinds.nip55) { ndk.accounts.addAccount( pubkey: account.pubkey, type: AccountType.externalSigner, - signer: AmberEventSigner( + signer: Nip55EventSigner( publicKey: account.pubkey, - amberFlutterDS: amberFlutterDS, + // restore the signer app package captured at login (null for + // legacy accounts -> Android routes through a compatible signer). + nip55Signer: Nip55Signer(package: account.signerSeed), ), ); continue; @@ -236,6 +241,11 @@ class NdkFlutter { if (accounts.loggedAccount == null) return; if (!ndk.accounts.hasAccount(accounts.loggedAccount!)) return; - ndk.accounts.switchAccount(pubkey: accounts.loggedAccount!); + try { + ndk.accounts.switchAccount(pubkey: accounts.loggedAccount!); + } catch (_) { + // stored logged account could not be restored (e.g. stale/corrupted + // state); ignore rather than crash app startup. + } } } diff --git a/packages/ndk_flutter/lib/models/accounts.dart b/packages/ndk_flutter/lib/models/accounts.dart index edd7d0cc0..3e3d1ddfe 100644 --- a/packages/ndk_flutter/lib/models/accounts.dart +++ b/packages/ndk_flutter/lib/models/accounts.dart @@ -23,7 +23,7 @@ class NostrWidgetsAccounts { } } -enum AccountKinds { nip07, amber, bunker, pubkey, privkey } +enum AccountKinds { nip07, nip55, bunker, pubkey, privkey } class NostrAccount { AccountKinds kind; @@ -41,9 +41,12 @@ class NostrAccount { } factory NostrAccount.fromJson(Map json) { + // Legacy: NIP-55 external-signer accounts were stored as 'amber' before + // the rename to the protocol-generic 'nip55'. + final kindName = json['kind'] == 'amber' ? 'nip55' : json['kind']; return NostrAccount( kind: AccountKinds.values.firstWhere( - (e) => e.toString().split('.').last == json['kind'], + (e) => e.toString().split('.').last == kindName, ), pubkey: json['pubkey'], signerSeed: json['signerSeed'], diff --git a/packages/ndk_flutter/lib/ndk_flutter.dart b/packages/ndk_flutter/lib/ndk_flutter.dart index 4b71738ad..ad7dea367 100644 --- a/packages/ndk_flutter/lib/ndk_flutter.dart +++ b/packages/ndk_flutter/lib/ndk_flutter.dart @@ -1,12 +1,11 @@ export 'main/config.dart'; export 'main/ndk_flutter.dart'; export 'repositories/flutter_secure_storage_wallets_repo.dart'; -export 'data_layer/repositories/signers/amber_event_signer.dart'; -export 'data_layer/data_sources/amber_flutter.dart'; +export 'data_layer/repositories/signers/nip55_event_signer.dart'; +export 'data_layer/data_sources/nip55_signer.dart'; export 'widgets/widgets.dart'; export 'verifiers/ndk_event_verifier.dart'; export 'verifiers/web_event_verifier.dart'; export 'signers/web_event_signer.dart'; export 'signers/ndk_event_signer.dart'; export 'utils/nostr_kinds.dart'; -export 'package:amberflutter/amberflutter.dart'; diff --git a/packages/ndk_flutter/lib/ndk_platform_interface.dart b/packages/ndk_flutter/lib/ndk_platform_interface.dart index 9f14f0c14..4edb82a10 100644 --- a/packages/ndk_flutter/lib/ndk_platform_interface.dart +++ b/packages/ndk_flutter/lib/ndk_platform_interface.dart @@ -3,7 +3,7 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'ndk_method_channel.dart'; abstract class NdkPlatform extends PlatformInterface { - /// Constructs a AmberflutterPlatform. + /// Constructs a NdkPlatform. NdkPlatform() : super(token: _token); static final Object _token = Object(); diff --git a/packages/ndk_flutter/lib/widgets/login/login_controller.dart b/packages/ndk_flutter/lib/widgets/login/login_controller.dart index 6ce88cea7..f50d3de28 100644 --- a/packages/ndk_flutter/lib/widgets/login/login_controller.dart +++ b/packages/ndk_flutter/lib/widgets/login/login_controller.dart @@ -1,4 +1,3 @@ -import 'package:amberflutter/amberflutter.dart'; import 'package:flutter/material.dart'; import 'package:ndk/ndk.dart'; import 'package:ndk_flutter/ndk_flutter.dart'; @@ -40,10 +39,10 @@ class LoginController extends ChangeNotifier { bool isNostrConnectDialogOpen = false; List challengeToasts = []; - bool _isWaitingForAmber = false; - bool get isWaitingForAmber => _isWaitingForAmber; - set isWaitingForAmber(bool value) { - _isWaitingForAmber = value; + bool _isWaitingForExternalSigner = false; + bool get isWaitingForExternalSigner => _isWaitingForExternalSigner; + set isWaitingForExternalSigner(bool value) { + _isWaitingForExternalSigner = value; notifyListeners(); } @@ -95,35 +94,39 @@ class LoginController extends ChangeNotifier { } } - Future loginWithAmber() async { - isWaitingForAmber = true; - - final amber = Amberflutter(); - - final isAmberInstalled = await amber.isAppInstalled(); - - if (!isAmberInstalled) { - launchUrl(Uri.parse('https://github.com/greenart7c3/Amber')); - return; - } + Future loginWithExternalSigner() async { + isWaitingForExternalSigner = true; + try { + const signer = Nip55Signer(); - final amberFlutterDS = AmberFlutterDS(amber); + final isInstalled = await signer.isAppInstalled(); - final amberResponse = await amber.getPublicKey(); + if (!isInstalled) { + isWaitingForExternalSigner = false; + launchUrl(Uri.parse('https://github.com/greenart7c3/Amber')); + return; + } - 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(); + await loggedIn(); + } finally { + isWaitingForExternalSigner = false; + } } Future loggedIn() async { diff --git a/packages/ndk_flutter/lib/widgets/login/n_login.dart b/packages/ndk_flutter/lib/widgets/login/n_login.dart index 28e9b7b3b..e84e6162f 100644 --- a/packages/ndk_flutter/lib/widgets/login/n_login.dart +++ b/packages/ndk_flutter/lib/widgets/login/n_login.dart @@ -17,7 +17,7 @@ class NLogin extends StatefulWidget { final bool enableNsecLogin; final bool enableNip07Login; final bool enableBunkerLogin; - final bool enableAmberLogin; + final bool enableSignerAppLogin; final bool enablePubkeyLogin; final NostrConnect? nostrConnect; final String? nsecLabelText; @@ -36,7 +36,7 @@ class NLogin extends StatefulWidget { this.enableNsecLogin = true, this.enableNip07Login = true, this.enableBunkerLogin = true, - this.enableAmberLogin = true, + this.enableSignerAppLogin = true, this.enablePubkeyLogin = true, this.nostrConnect, this.nsecLabelText, @@ -250,14 +250,13 @@ class _NLoginState extends State { ), ); - final amberView = Padding( + final signerAppView = Padding( padding: EdgeInsetsGeometry.only(bottom: bottomPadding), - child: FilledButton.icon( - onPressed: controller.isWaitingForAmber + child: FilledButton( + onPressed: controller.isWaitingForExternalSigner ? null - : controller.loginWithAmber, - label: Text(AppLocalizations.of(context)!.loginWithAmber), - icon: Icon(Icons.diamond), + : controller.loginWithExternalSigner, + child: Text(AppLocalizations.of(context)!.loginWithSignerApp), ), ); @@ -272,7 +271,7 @@ class _NLoginState extends State { if (widget.enableNip07Login && kIsWeb) nip07View, if (widget.enableBunkerLogin || widget.enableNostrConnectLogin) bunkerView, - if (widget.enableAmberLogin && isAndroid) amberView, + if (widget.enableSignerAppLogin && isAndroid) signerAppView, if (widget.enableAccountCreation) createAccountView, ], ); diff --git a/packages/ndk_flutter/lib/widgets/pending_requests/n_pending_requests.dart b/packages/ndk_flutter/lib/widgets/pending_requests/n_pending_requests.dart index 252f20aaa..f20664a3c 100644 --- a/packages/ndk_flutter/lib/widgets/pending_requests/n_pending_requests.dart +++ b/packages/ndk_flutter/lib/widgets/pending_requests/n_pending_requests.dart @@ -7,7 +7,7 @@ import 'package:ndk_flutter/ndk_flutter.dart'; /// State of the pending requests widget enum NPendingRequestsState { hidden, collapsed, expanded } -/// A widget that displays pending signer requests (for Amber, bunker, extension). +/// A widget that displays pending signer requests (for signer app, bunker, extension). /// /// This widget shows a floating panel at the bottom of the screen when there are /// pending requests waiting for user approval on external signers. @@ -115,7 +115,7 @@ class _NPendingRequestsState extends State { final signer = account.signer; final signerType = signer.runtimeType.toString(); - if (signerType.contains('Amber')) return 'Amber'; + if (signerType.contains('Nip55')) return 'signer app'; if (signerType.contains('Nip46')) return 'bunker'; if (signerType.contains('Nip07')) return 'extension'; diff --git a/packages/ndk_flutter/pubspec.yaml b/packages/ndk_flutter/pubspec.yaml index a367cd310..f24f80e43 100644 --- a/packages/ndk_flutter/pubspec.yaml +++ b/packages/ndk_flutter/pubspec.yaml @@ -22,7 +22,6 @@ environment: resolution: workspace dependencies: - amberflutter: ^0.0.9 android_intent_plus: ^6.0.0 crypto: ">=3.0.0 <4.0.0" flutter: @@ -50,5 +49,10 @@ dev_dependencies: flutter: generate: true + plugin: + platforms: + android: + package: relaystr.ndk + pluginClass: DartNdkPlugin assets: - assets/images/ diff --git a/packages/sample-app/README.md b/packages/sample-app/README.md index 7bc23ff90..943586321 100644 --- a/packages/sample-app/README.md +++ b/packages/sample-app/README.md @@ -17,7 +17,8 @@ Main package: [🔗 Dart Nostr Development Kit (NDK)](https://pub.dev/packages/n 4. start app `flutter run` -## Amber +## NIP-55 signer -Amber is a external nostr signer. Its not required in order to run this application. \ -You can read more about it here: [https://github.com/greenart7c3/Amber](https://github.com/greenart7c3/Amber) +A NIP-55 compatible external Nostr signer is optional for running this +application. Amber is one app that implements NIP-55, but the sample app is not +specific to Amber. diff --git a/packages/sample-app/android/app/build.gradle b/packages/sample-app/android/app/build.gradle index f55417ea9..a741026ec 100644 --- a/packages/sample-app/android/app/build.gradle +++ b/packages/sample-app/android/app/build.gradle @@ -33,8 +33,8 @@ if (flutterVersionName == null) { android { namespace "com.example.example" - compileSdkVersion 35 - ndkVersion "27.0.12077973" + compileSdk = 36 + ndkVersion "28.2.13676358" compileOptions { sourceCompatibility JavaVersion.VERSION_17 diff --git a/packages/sample-app/android/build.gradle b/packages/sample-app/android/build.gradle index 8b7b3622e..f2d7ed74c 100644 --- a/packages/sample-app/android/build.gradle +++ b/packages/sample-app/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '2.1.0' + ext.kotlin_version = '2.2.20' repositories { google() mavenCentral() @@ -21,7 +21,7 @@ subprojects { afterEvaluate { project -> if (project.hasProperty('android')) { project.android { - compileSdkVersion 35 + compileSdkVersion 36 } } } diff --git a/packages/sample-app/android/gradle.properties b/packages/sample-app/android/gradle.properties index 598d13fee..f2122b21a 100644 --- a/packages/sample-app/android/gradle.properties +++ b/packages/sample-app/android/gradle.properties @@ -1,3 +1,7 @@ org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/packages/sample-app/android/gradle/wrapper/gradle-wrapper.properties b/packages/sample-app/android/gradle/wrapper/gradle-wrapper.properties index afa1e8eb0..e4ef43fb9 100644 --- a/packages/sample-app/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/sample-app/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/packages/sample-app/android/settings.gradle b/packages/sample-app/android/settings.gradle index 220471060..0e4b36226 100644 --- a/packages/sample-app/android/settings.gradle +++ b/packages/sample-app/android/settings.gradle @@ -23,8 +23,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.7.1" apply false - id "org.jetbrains.kotlin.android" version "2.1.0" apply false + id "com.android.application" version "8.11.1" apply false + id "org.jetbrains.kotlin.android" version "2.2.20" apply false } include ":app" diff --git a/packages/sample-app/lib/main.dart b/packages/sample-app/lib/main.dart index f99d3c575..d75cc6d23 100644 --- a/packages/sample-app/lib/main.dart +++ b/packages/sample-app/lib/main.dart @@ -16,7 +16,7 @@ import 'package:protocol_handler/protocol_handler.dart'; import 'demo_app_config.dart'; -bool amberAvailable = false; +bool signerAppAvailable = false; late Ndk ndk; final ndkFlutter = NdkFlutter(ndk: ndk); @@ -32,10 +32,10 @@ Future main() async { } try { - final amber = Amberflutter(); - amberAvailable = await amber.isAppInstalled(); + const signer = Nip55Signer(); + signerAppAvailable = await signer.isAppInstalled(); } catch (e) { - // not on android or amber not installed + // not on android or no signer app installed } final cacheManager = kIsWeb diff --git a/packages/sample-app/lib/amber_page.dart b/packages/sample-app/lib/nip55_signer_page.dart similarity index 84% rename from packages/sample-app/lib/amber_page.dart rename to packages/sample-app/lib/nip55_signer_page.dart index badf2258a..ea9013200 100644 --- a/packages/sample-app/lib/amber_page.dart +++ b/packages/sample-app/lib/nip55_signer_page.dart @@ -1,22 +1,22 @@ import 'dart:convert'; -import 'package:amberflutter/amberflutter.dart'; import 'package:flutter/material.dart'; import 'package:ndk/ndk.dart'; +import 'package:ndk_flutter/ndk_flutter.dart'; -class AmberPage extends StatefulWidget { - const AmberPage({super.key}); +class Nip55SignerPage extends StatefulWidget { + const Nip55SignerPage({super.key}); @override - State createState() => _AmberPageState(); + State createState() => _Nip55SignerPageState(); } -class _AmberPageState extends State { +class _Nip55SignerPageState extends State { String _npub = ''; String _pubkeyHex = ''; String _text = ''; String _cipherText = ''; - final amber = Amberflutter(); + final nip55Signer = const Nip55Signer(); @override Widget build(BuildContext context) { @@ -25,12 +25,12 @@ class _AmberPageState extends State { children: [ FilledButton( onPressed: () async { - amber.getPublicKey( + nip55Signer.getPublicKey( permissions: [ - const Permission( + const Nip55Permission( type: "nip04_encrypt", ), - const Permission( + const Nip55Permission( type: "nip04_decrypt", ), ], @@ -50,14 +50,14 @@ class _AmberPageState extends State { 'id': '', 'pubkey': Nip19.decode(_npub), 'kind': 1, - 'content': 'Hello from Amber Flutter!', + 'content': 'Hello from NDK Flutter!', 'created_at': (DateTime.now().millisecondsSinceEpoch / 1000).round(), 'tags': [], 'sig': '', }); - amber + nip55Signer .signEvent( currentUser: _npub, eventJson: eventJson, @@ -76,14 +76,14 @@ class _AmberPageState extends State { 'id': '', 'pubkey': Nip19.decode(_npub), 'kind': 1, - 'content': 'Hello from Amber Flutter!', + 'content': 'Hello from NDK Flutter!', 'created_at': (DateTime.now().millisecondsSinceEpoch / 1000).round(), 'tags': [], 'sig': '', }); - var value = await amber.signEvent( + var value = await nip55Signer.signEvent( currentUser: _npub, eventJson: eventJson, ); @@ -100,9 +100,9 @@ class _AmberPageState extends State { ), FilledButton( onPressed: () { - amber + nip55Signer .nip04Encrypt( - plaintext: "Hello from Amber Flutter, Nip 04!", + plaintext: "Hello from NDK Flutter, Nip 04!", currentUser: _npub, pubKey: _pubkeyHex, ) @@ -117,7 +117,7 @@ class _AmberPageState extends State { ), FilledButton( onPressed: () async { - amber + nip55Signer .nip04Decrypt( ciphertext: _cipherText, currentUser: _npub, @@ -129,7 +129,7 @@ class _AmberPageState extends State { }); }); // ; - amber + nip55Signer .nip04Decrypt( ciphertext: _cipherText, currentUser: _npub, @@ -141,7 +141,7 @@ class _AmberPageState extends State { }); }); // , - amber + nip55Signer .nip04Decrypt( ciphertext: _cipherText, currentUser: _npub, @@ -157,9 +157,9 @@ class _AmberPageState extends State { ), FilledButton( onPressed: () { - amber + nip55Signer .nip44Encrypt( - plaintext: "Hello from Amber Flutter, Nip 44!", + plaintext: "Hello from NDK Flutter, Nip 44!", currentUser: _npub, pubKey: _pubkeyHex, ) @@ -174,7 +174,7 @@ class _AmberPageState extends State { ), FilledButton( onPressed: () { - amber + nip55Signer .nip44Decrypt( ciphertext: _cipherText, currentUser: _npub, diff --git a/packages/sample-app/lib/widgets_demo_page.dart b/packages/sample-app/lib/widgets_demo_page.dart index 12dab5a0d..8aa794b5b 100644 --- a/packages/sample-app/lib/widgets_demo_page.dart +++ b/packages/sample-app/lib/widgets_demo_page.dart @@ -215,7 +215,7 @@ class _WidgetsDemoPageState extends State { setState(() => _showLogin = false); }, enableNip07Login: false, - enableAmberLogin: false, + enableSignerAppLogin: false, nostrConnect: NostrConnect( appName: 'NDK sample app', relays: [ diff --git a/packages/sample-app/pubspec.lock b/packages/sample-app/pubspec.lock index 145170021..ffe22d91d 100644 --- a/packages/sample-app/pubspec.lock +++ b/packages/sample-app/pubspec.lock @@ -1,14 +1,6 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - amberflutter: - dependency: "direct main" - description: - name: amberflutter - sha256: "5101b36419a93e0620a227ada25690286efc93ab9de59dc80e7c3a0c29ccd348" - url: "https://pub.dev" - source: hosted - version: "0.0.9" android_intent_plus: dependency: transitive description: @@ -587,7 +579,7 @@ packages: path: "../ndk" relative: true source: path - version: "0.8.4-dev.1" + version: "0.8.4-dev.2" ndk_bip32_keys: dependency: transitive description: @@ -602,14 +594,14 @@ packages: path: "../drift" relative: true source: path - version: "0.1.1-dev.1" + version: "0.1.1-dev.3" ndk_flutter: dependency: "direct main" description: path: "../ndk_flutter" relative: true source: path - version: "0.8.4-dev.1" + version: "0.8.4-dev.3" nested: dependency: transitive description: @@ -624,7 +616,7 @@ packages: path: "../nip07_event_signer" relative: true source: path - version: "1.1.0-dev.1" + version: "1.1.0-dev.2" objective_c: dependency: transitive description: diff --git a/packages/sample-app/pubspec.yaml b/packages/sample-app/pubspec.yaml index 0692ed068..e28387bba 100644 --- a/packages/sample-app/pubspec.yaml +++ b/packages/sample-app/pubspec.yaml @@ -52,7 +52,6 @@ dependencies: protocol_handler: ^0.2.0 http: ^1.2.0 qr_flutter: ^4.1.0 - amberflutter: any dev_dependencies: flutter_test: