diff --git a/packages/ndk/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/packages/ndk/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 000000000..539ab022f --- /dev/null +++ b/packages/ndk/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,19 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + } +} diff --git a/packages/ndk/example/nwc/list_transactions.dart b/packages/ndk/example/nwc/list_transactions.dart index c03f52ea0..8c0bfc0f0 100644 --- a/packages/ndk/example/nwc/list_transactions.dart +++ b/packages/ndk/example/nwc/list_transactions.dart @@ -14,11 +14,11 @@ void main() async { final connection = await ndk.nwc.connect(nwcUri); ListTransactionsResponse response = - await ndk.nwc.listTransactions(connection, unpaid: false); + await ndk.nwc.listTransactions(connection, unpaid: true); for (final transaction in response.transactions) { print( - "Transaction ${transaction.type} ${transaction.amountSat} sats ${transaction.description!}"); + "Transaction ${transaction.type} state ${transaction.state} ${transaction.amountSat} sats ${transaction.description!}"); } await ndk.destroy(); } diff --git a/packages/ndk/example/nwc/make_hold_invoice.dart b/packages/ndk/example/nwc/make_hold_invoice.dart new file mode 100644 index 000000000..9356b3cff --- /dev/null +++ b/packages/ndk/example/nwc/make_hold_invoice.dart @@ -0,0 +1,127 @@ +// ignore_for_file: avoid_print + +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; +import 'package:crypto/crypto.dart'; +import 'package:convert/convert.dart'; +import 'package:ascii_qr/ascii_qr.dart'; +import 'package:ndk/domain_layer/usecases/nwc/nwc_notification.dart'; + +import 'package:ndk/ndk.dart'; + +void main() async { + // We use an empty bootstrap relay list, + // since NWC will provide the relay we connect to so we don't need default relays + final ndk = Ndk.emptyBootstrapRelaysConfig(); + + // You need an NWC_URI env var or to replace with your NWC uri connection + final nwcUri = Platform.environment['NWC_URI']!; + final connection = await ndk.nwc.connect(nwcUri); + + final amount = 29; + final description = "hello hold"; + + // Generate a random 32-byte preimage + final random = Random.secure(); + final preimageBytes = + Uint8List.fromList(List.generate(32, (_) => random.nextInt(256))); + final preimageHex = + hex.encode(preimageBytes); // Optional: hex encode for printing + + // Calculate the payment hash (SHA256 of the preimage) + final paymentHashBytes = sha256.convert(preimageBytes).bytes; + final paymentHash = hex.encode(paymentHashBytes); + + print("Generated Preimage: $preimageHex"); + print("Generated Payment Hash: $paymentHash"); + + try { + // 1. Create the hold invoice + print("Creating hold invoice..."); + final makeResponse = await ndk.nwc.makeHoldInvoice(connection, + amountSats: amount, description: description, paymentHash: paymentHash); + + if (makeResponse.errorCode == null) { + // Check if errorCode is null for success + final invoice = makeResponse.invoice; + print( + "Hold invoice created successfully. Invoice: $invoice, Payment Hash: ${makeResponse.paymentHash}"); + + if (invoice.isNotEmpty) { + print("\nScan QR Code to pay/hold:"); + try { + final asciiQr = AsciiQrGenerator.generate( + invoice.toUpperCase(), + ); + print(asciiQr.toString()); + } catch (e) { + print("Error generating ASCII QR code: $e"); + } + print("\nOr copy Bolt11 invoice:\n$invoice\n"); + } + + final duration = makeResponse.expiresAt!-DateTime.now().millisecondsSinceEpoch ~/ 1000; + print( + "Waiting for hold invoice acceptance notification (max $duration seconds)..."); + try { + final acceptedNotification = await connection.holdInvoiceStateStream + .firstWhere((notification) { + return notification.notificationType == NwcNotification.kHoldInvoiceAccepted; + }).timeout(Duration(seconds: duration.toInt())); + + print( + "Hold invoice accepted by wallet! (Notification: ${acceptedNotification.notificationType})"); + + // 3. Ask user whether to settle or cancel + print("Settle this accepted invoice? (Y/N)"); + String? input = stdin.readLineSync()?.trim().toLowerCase(); + + if (input == 'n') { + // 4a. Cancel the hold invoice + print("Canceling hold invoice with payment hash: $paymentHash..."); + final cancelResponse = await ndk.nwc + .cancelHoldInvoice(connection, paymentHash: paymentHash); + + if (cancelResponse.errorCode == null) { + // Check if errorCode is null for success + print("Hold invoice canceled successfully."); + } else { + print( + "Failed to cancel hold invoice. Error: ${cancelResponse.errorMessage} (Code: ${cancelResponse.errorCode})"); + } + } else if (input == 'y') { + // 4b. Settle the hold invoice using the preimage + print("Settling hold invoice with preimage: $preimageHex..."); + final settleResponse = await ndk.nwc + .settleHoldInvoice(connection, preimage: preimageHex); + + if (settleResponse.errorCode == null) { + print( + "Hold invoice settled successfully. Preimage used: $preimageHex"); + } else { + print( + "Failed to settle hold invoice. Error: ${settleResponse.errorMessage} (Code: ${settleResponse.errorCode})"); + } + } else { + print("Invalid input. Not settling or canceling."); + } + } on TimeoutException { + print( + "Timed out waiting for hold invoice acceptance notification. The invoice might not be held by the wallet."); + // Optionally, try to cancel here as a fallback? + } catch (e) { + print("Error waiting for notification: $e"); + } + } else { + print( + "Failed to create hold invoice. Error: ${makeResponse.errorMessage} (Code: ${makeResponse.errorCode})"); + } + } catch (e) { + print("An error occurred: $e"); + } finally { + // Ensure ndk.destroy() is called in the finally block + await ndk.destroy(); + } +} diff --git a/packages/ndk/lib/domain_layer/usecases/nwc/consts/nwc_method.dart b/packages/ndk/lib/domain_layer/usecases/nwc/consts/nwc_method.dart index 41ae70010..f5ba6f11b 100644 --- a/packages/ndk/lib/domain_layer/usecases/nwc/consts/nwc_method.dart +++ b/packages/ndk/lib/domain_layer/usecases/nwc/consts/nwc_method.dart @@ -15,6 +15,9 @@ class NwcMethod { static const NwcMethod PAY_KEYSEND = NwcMethod('pay_keysend'); static const NwcMethod MULTI_PAY_KEYSEND = NwcMethod('multi_pay_keysend'); static const NwcMethod MAKE_INVOICE = NwcMethod('make_invoice'); + static const NwcMethod MAKE_HOLD_INVOICE = NwcMethod('make_hold_invoice'); + static const NwcMethod CANCEL_HOLD_INVOICE = NwcMethod('cancel_hold_invoice'); + static const NwcMethod SETTLE_HOLD_INVOICE = NwcMethod('settle_hold_invoice'); static const NwcMethod LOOKUP_INVOICE = NwcMethod('lookup_invoice'); static const NwcMethod LIST_TRANSACTIONS = NwcMethod('list_transactions'); static const NwcMethod UNKNOWN = NwcMethod('unknown'); @@ -26,6 +29,9 @@ class NwcMethod { PAY_KEYSEND.name: PAY_KEYSEND, MULTI_PAY_KEYSEND.name: MULTI_PAY_KEYSEND, MAKE_INVOICE.name: MAKE_INVOICE, + MAKE_HOLD_INVOICE.name: MAKE_HOLD_INVOICE, + CANCEL_HOLD_INVOICE.name: CANCEL_HOLD_INVOICE, + SETTLE_HOLD_INVOICE.name: SETTLE_HOLD_INVOICE, LOOKUP_INVOICE.name: LOOKUP_INVOICE, LIST_TRANSACTIONS.name: LIST_TRANSACTIONS, GET_BALANCE.name: GET_BALANCE, diff --git a/packages/ndk/lib/domain_layer/usecases/nwc/nwc.dart b/packages/ndk/lib/domain_layer/usecases/nwc/nwc.dart index 7bd19f7c6..afff6b99f 100644 --- a/packages/ndk/lib/domain_layer/usecases/nwc/nwc.dart +++ b/packages/ndk/lib/domain_layer/usecases/nwc/nwc.dart @@ -17,6 +17,9 @@ import 'requests/get_info.dart'; import 'requests/list_transactions.dart'; import 'requests/lookup_invoice.dart'; import 'requests/make_invoice.dart'; +import 'requests/make_hold_invoice.dart'; // Add import for MakeHoldInvoiceRequest +import 'requests/cancel_hold_invoice.dart'; // Add import for CancelHoldInvoiceRequest +import 'requests/settle_hold_invoice.dart'; // Add import for SettleHoldInvoiceRequest import 'requests/nwc_request.dart'; import 'requests/pay_invoice.dart'; import 'responses/nwc_response.dart'; @@ -150,7 +153,8 @@ class Nwc { response = GetBalanceResponse.deserialize(data); } else if (data['result_type'] == NwcMethod.GET_BUDGET.name) { response = GetBudgetResponse.deserialize(data); - } else if (data['result_type'] == NwcMethod.MAKE_INVOICE.name) { + } else if (data['result_type'] == NwcMethod.MAKE_INVOICE.name || + data['result_type'] == NwcMethod.MAKE_HOLD_INVOICE.name) { response = MakeInvoiceResponse.deserialize(data); } else if (data['result_type'] == NwcMethod.PAY_INVOICE.name) { response = PayInvoiceResponse.deserialize(data); @@ -158,6 +162,9 @@ class Nwc { response = ListTransactionsResponse.deserialize(data); } else if (data['result_type'] == NwcMethod.LOOKUP_INVOICE.name) { response = LookupInvoiceResponse.deserialize(data); + } else if (data['result_type'] == NwcMethod.CANCEL_HOLD_INVOICE.name || data['result_type'] == NwcMethod.SETTLE_HOLD_INVOICE.name) { + response = + NwcResponse(resultType: data['result_type']); // Generic response } } else { response = NwcResponse(resultType: data['result_type']); @@ -192,7 +199,7 @@ class Nwc { if (data.containsKey("notification_type") && data['notification'] != null) { NwcNotification notification = - NwcNotification.fromMap(data['notification']); + NwcNotification.fromMap(data["notification_type"],data['notification']); connection.notificationStream.add(notification); } else if (data.containsKey("error")) { // TODO: Define what to do when data has an error @@ -213,7 +220,7 @@ class Nwc { if (data.containsKey("notification_type") && data['notification'] != null) { NwcNotification notification = - NwcNotification.fromMap(data['notification']); + NwcNotification.fromMap(data["notification_type"],data['notification']); connection.notificationStream.add(notification); } else if (data.containsKey("error")) { // TODO: Define what to do when data has an error @@ -282,7 +289,7 @@ class Nwc { return _executeRequest(connection, GetBudgetRequest()); } - /// Does a `make_invoie` request + /// Does a `make_invoice` request Future makeInvoice(NwcConnection connection, {required int amountSats, String? description, @@ -297,6 +304,37 @@ class Nwc { expiry: expiry)); } + /// Does a `make_hold_invoice` request + Future makeHoldInvoice(NwcConnection connection, + {required int amountSats, + String? description, + String? descriptionHash, + int? expiry, + required String paymentHash}) async { + return _executeRequest( + connection, + MakeHoldInvoiceRequest( + amountMsat: amountSats * 1000, + description: description, + descriptionHash: descriptionHash, + expiry: expiry, + paymentHash: paymentHash)); + } + + /// Does a `cancel_hold_invoice` request + Future cancelHoldInvoice(NwcConnection connection, + {required String paymentHash}) async { + return _executeRequest( + connection, CancelHoldInvoiceRequest(paymentHash: paymentHash)); + } + + /// Does a `settle_hold_invoice` request + Future settleHoldInvoice(NwcConnection connection, + {required String preimage}) async { + return _executeRequest( + connection, SettleHoldInvoiceRequest(preimage: preimage)); + } + /// Does a `pay_invoice` request Future payInvoice(NwcConnection connection, {required String invoice, Duration? timeout}) async { diff --git a/packages/ndk/lib/domain_layer/usecases/nwc/nwc_connection.dart b/packages/ndk/lib/domain_layer/usecases/nwc/nwc_connection.dart index 6f7a6f81f..29f2ebe6c 100644 --- a/packages/ndk/lib/domain_layer/usecases/nwc/nwc_connection.dart +++ b/packages/ndk/lib/domain_layer/usecases/nwc/nwc_connection.dart @@ -23,11 +23,16 @@ class NwcConnection { Stream get paymentsReceivedStream => notificationStream.stream - .where((notification) => notification.isIncoming) + .where((notification) => notification.isPaymentReceived) .asBroadcastStream(); + Stream get paymentsSentStream => notificationStream.stream - .where((notification) => !notification.isIncoming) + .where((notification) => notification.isPaymentSent) + .asBroadcastStream(); + + Stream get holdInvoiceStateStream => notificationStream.stream + .where((notification) => notification.isHoldInvoiceAccepted) .asBroadcastStream(); /// listen diff --git a/packages/ndk/lib/domain_layer/usecases/nwc/nwc_notification.dart b/packages/ndk/lib/domain_layer/usecases/nwc/nwc_notification.dart index c77b1f7bc..4d0d784f8 100644 --- a/packages/ndk/lib/domain_layer/usecases/nwc/nwc_notification.dart +++ b/packages/ndk/lib/domain_layer/usecases/nwc/nwc_notification.dart @@ -3,39 +3,46 @@ import 'consts/transaction_type.dart'; class NwcNotification { static const kPaymentReceived = "payment_received"; static const kPaymentSent = "payment_sent"; + static const kHoldInvoiceAccepted = "hold_invoice_accepted"; + String notificationType; String type; String invoice; String? description; String? descriptionHash; - String preimage; + String? preimage; String paymentHash; int amount; - int feesPaid; + int? feesPaid; int createdAt; int? expiresAt; - int settledAt; + int? settledAt; Map? metadata; get isIncoming => type == TransactionType.incoming.value; + get isPaymentReceived => notificationType == kPaymentReceived; + get isPaymentSent => notificationType == kPaymentSent; + get isHoldInvoiceAccepted => notificationType == kHoldInvoiceAccepted; NwcNotification({ + required this.notificationType, required this.type, required this.invoice, this.description, this.descriptionHash, - required this.preimage, + this.preimage, required this.paymentHash, required this.amount, - required this.feesPaid, + this.feesPaid, required this.createdAt, this.expiresAt, - required this.settledAt, - required this.metadata, + this.settledAt, + this.metadata, }); - factory NwcNotification.fromMap(Map map) { + factory NwcNotification.fromMap(String notificationType, Map map) { return NwcNotification( + notificationType: notificationType, type: map['type'] as String, invoice: map['invoice'] as String, description: map['description'] as String?, @@ -46,10 +53,15 @@ class NwcNotification { feesPaid: map['fees_paid'] as int, createdAt: map['created_at'] as int, expiresAt: map['expires_at'] as int?, - settledAt: map['settled_at'] as int, + settledAt: map['settled_at'] as int?, metadata: map.containsKey('metadata') && map['metadata'] != null ? Map.from(map['metadata']) : null, ); } + + @override + toString() { + return 'NwcNotification{type: $type, invoice: $invoice, description: $description, descriptionHash: $descriptionHash, preimage: $preimage, paymentHash: $paymentHash, amount: $amount, feesPaid: $feesPaid, createdAt: $createdAt, expiresAt: $expiresAt, settledAt: $settledAt, metadata: $metadata}'; + } } diff --git a/packages/ndk/lib/domain_layer/usecases/nwc/requests/cancel_hold_invoice.dart b/packages/ndk/lib/domain_layer/usecases/nwc/requests/cancel_hold_invoice.dart new file mode 100644 index 000000000..bee4c482c --- /dev/null +++ b/packages/ndk/lib/domain_layer/usecases/nwc/requests/cancel_hold_invoice.dart @@ -0,0 +1,21 @@ +import 'package:ndk/domain_layer/usecases/nwc/consts/nwc_method.dart'; + +import 'nwc_request.dart'; + +class CancelHoldInvoiceRequest extends NwcRequest { + final String paymentHash; + + const CancelHoldInvoiceRequest({ + required this.paymentHash, + }) : super(method: NwcMethod.CANCEL_HOLD_INVOICE); + + @override + Map toMap() { + return { + ...super.toMap(), + 'params': { + 'payment_hash': paymentHash, + } + }; + } +} diff --git a/packages/ndk/lib/domain_layer/usecases/nwc/requests/make_hold_invoice.dart b/packages/ndk/lib/domain_layer/usecases/nwc/requests/make_hold_invoice.dart new file mode 100644 index 000000000..5d5c11ab2 --- /dev/null +++ b/packages/ndk/lib/domain_layer/usecases/nwc/requests/make_hold_invoice.dart @@ -0,0 +1,22 @@ +import 'package:ndk/domain_layer/usecases/nwc/consts/nwc_method.dart'; + +import 'make_invoice.dart'; + +class MakeHoldInvoiceRequest extends MakeInvoiceRequest { + final String paymentHash; + + const MakeHoldInvoiceRequest({ + required super.amountMsat, + super.description, + super.descriptionHash, + super.expiry, + required this.paymentHash, + }) : super(method: NwcMethod.MAKE_HOLD_INVOICE); + + @override + Map toMap() { + final map = super.toMap(); + (map['params'] as Map)['payment_hash'] = paymentHash; + return map; + } +} diff --git a/packages/ndk/lib/domain_layer/usecases/nwc/requests/make_invoice.dart b/packages/ndk/lib/domain_layer/usecases/nwc/requests/make_invoice.dart index 9f7573507..007d0b118 100644 --- a/packages/ndk/lib/domain_layer/usecases/nwc/requests/make_invoice.dart +++ b/packages/ndk/lib/domain_layer/usecases/nwc/requests/make_invoice.dart @@ -14,8 +14,11 @@ class MakeInvoiceRequest extends NwcRequest { this.description, this.descriptionHash, this.expiry, + NwcMethod? method, // Add optional method parameter }) : amountSat = amountMsat ~/ 1000, - super(method: NwcMethod.MAKE_INVOICE); + super( + method: method ?? + NwcMethod.MAKE_INVOICE); // Use provided method or default @override Map toMap() { diff --git a/packages/ndk/lib/domain_layer/usecases/nwc/requests/settle_hold_invoice.dart b/packages/ndk/lib/domain_layer/usecases/nwc/requests/settle_hold_invoice.dart new file mode 100644 index 000000000..21c0d2b2f --- /dev/null +++ b/packages/ndk/lib/domain_layer/usecases/nwc/requests/settle_hold_invoice.dart @@ -0,0 +1,21 @@ +import 'package:ndk/domain_layer/usecases/nwc/consts/nwc_method.dart'; + +import 'nwc_request.dart'; + +class SettleHoldInvoiceRequest extends NwcRequest { + final String preimage; + + const SettleHoldInvoiceRequest({ + required this.preimage, + }) : super(method: NwcMethod.SETTLE_HOLD_INVOICE); + + @override + Map toMap() { + return { + ...super.toMap(), + 'params': { + 'preimage': preimage, + } + }; + } +} diff --git a/packages/ndk/lib/domain_layer/usecases/nwc/responses/list_transactions_response.dart b/packages/ndk/lib/domain_layer/usecases/nwc/responses/list_transactions_response.dart index bfebf4a04..c1cd8a0dc 100644 --- a/packages/ndk/lib/domain_layer/usecases/nwc/responses/list_transactions_response.dart +++ b/packages/ndk/lib/domain_layer/usecases/nwc/responses/list_transactions_response.dart @@ -46,6 +46,9 @@ class TransactionResult extends Equatable { /// The hash of the transaction description. final String? descriptionHash; + /// The hash of the transaction description. + final String? state; + /// The preimage of the transaction. final String? preimage; @@ -58,8 +61,8 @@ class TransactionResult extends Equatable { /// The amount of the invoice (in SATS) get amountSat => amount ~/ 1000; - /// The fees paid for the transaction (in MSATs). - final int feesPaid; + /// The fees paid for the transaction (in MSATs). Optional. + final int? feesPaid; /// The timestamp when the transaction was created. final int createdAt; @@ -79,9 +82,10 @@ class TransactionResult extends Equatable { this.description, this.descriptionHash, this.preimage, + this.state, required this.paymentHash, required this.amount, - required this.feesPaid, + this.feesPaid, required this.createdAt, this.expiresAt, this.settledAt, @@ -96,8 +100,9 @@ class TransactionResult extends Equatable { descriptionHash: input['description_hash'] as String?, preimage: input['preimage'] as String?, paymentHash: input['payment_hash'] as String, + state: input['state'] as String?, amount: input['amount'] as int, - feesPaid: input['fees_paid'] as int, + feesPaid: input['fees_paid'] as int?, createdAt: input['created_at'] as int, expiresAt: input['expires_at'] as int?, settledAt: input['settled_at'] as int?, diff --git a/packages/ndk/pubspec.lock b/packages/ndk/pubspec.lock index b9cde9608..455fa7acf 100644 --- a/packages/ndk/pubspec.lock +++ b/packages/ndk/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.0" + ascii_qr: + dependency: "direct main" + description: + name: ascii_qr + sha256: "2046e400a0fa4ea0de5df44c87b992cdd1f76403bb15e64513b89263598750ae" + url: "https://pub.dev" + source: hosted + version: "1.0.1" async: dependency: transitive description: @@ -433,6 +441,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" rxdart: dependency: "direct main" description: diff --git a/packages/ndk/pubspec.yaml b/packages/ndk/pubspec.yaml index 450d39556..20125124c 100644 --- a/packages/ndk/pubspec.yaml +++ b/packages/ndk/pubspec.yaml @@ -32,6 +32,7 @@ dependencies: cryptography: ^2.7.0 meta: ^1.15.0 xxh3: ^1.2.0 + ascii_qr: ^1.0.1 # Add ascii_qr dependency dev_dependencies: build_runner: ^2.4.11 diff --git a/packages/ndk/test/usecases/nwc/nwc_notification_test.dart b/packages/ndk/test/usecases/nwc/nwc_notification_test.dart index bfdaeef86..233b7f903 100644 --- a/packages/ndk/test/usecases/nwc/nwc_notification_test.dart +++ b/packages/ndk/test/usecases/nwc/nwc_notification_test.dart @@ -5,7 +5,7 @@ void main() { group('NwcNotification', () { test('should create an instance from a map', () { final map = { - 'type': 'payment_received', + 'type': 'incoming', 'invoice': 'invoice_123', 'description': 'Test payment', 'description_hash': 'hash_123', @@ -19,9 +19,10 @@ void main() { 'metadata': {'key': 'value'}, }; - final notification = NwcNotification.fromMap(map); + final notification = NwcNotification.fromMap(NwcNotification.kPaymentReceived, map); - expect(notification.type, equals('payment_received')); + expect(notification.notificationType, equals('payment_received')); + expect(notification.type, equals('incoming')); expect(notification.invoice, equals('invoice_123')); expect(notification.description, equals('Test payment')); expect(notification.descriptionHash, equals('hash_123')); @@ -37,6 +38,7 @@ void main() { test('should identify incoming transactions', () { final notification = NwcNotification( + notificationType: NwcNotification.kPaymentReceived, type: 'incoming', invoice: 'invoice_123', preimage: 'preimage_123', @@ -53,7 +55,7 @@ void main() { test('should handle null metadata', () { final map = { - 'type': 'payment_sent', + 'type': 'outgoing', 'invoice': 'invoice_123', 'preimage': 'preimage_123', 'payment_hash': 'payment_hash_123', @@ -63,7 +65,7 @@ void main() { 'settled_at': 1633040400, }; - final notification = NwcNotification.fromMap(map); + final notification = NwcNotification.fromMap(NwcNotification.kPaymentSent,map); expect(notification.metadata, isNull); }); diff --git a/packages/sample-app/android/app/.gitignore b/packages/sample-app/android/app/.gitignore new file mode 100644 index 000000000..0e60033f2 --- /dev/null +++ b/packages/sample-app/android/app/.gitignore @@ -0,0 +1 @@ +.cxx diff --git a/packages/sample-app/android/app/src/main/AndroidManifest.xml b/packages/sample-app/android/app/src/main/AndroidManifest.xml index 183784b00..3e726ebaa 100644 --- a/packages/sample-app/android/app/src/main/AndroidManifest.xml +++ b/packages/sample-app/android/app/src/main/AndroidManifest.xml @@ -23,6 +23,17 @@ + + + + + + + + + diff --git a/packages/sample-app/lib/accounts_page.dart b/packages/sample-app/lib/accounts_page.dart index 7c3f5f96a..8076669dd 100644 --- a/packages/sample-app/lib/accounts_page.dart +++ b/packages/sample-app/lib/accounts_page.dart @@ -1,10 +1,10 @@ -import 'dart:convert'; - import 'package:amberflutter/amberflutter.dart'; import 'package:flutter/material.dart'; -import 'package:ndk/ndk.dart'; -import 'package:ndk/shared/nips/nip19/nip19.dart'; -import 'package:ndk_rust_verifier/ndk_rust_verifier.dart'; +import 'package:ndk/domain_layer/entities/metadata.dart'; +import 'package:ndk/shared/nips/nip01/bip340.dart' as bip340_utils; +import 'package:ndk/shared/nips/nip19/nip19.dart' as nip19_decoder; + +import 'main.dart'; class AccountsPage extends StatefulWidget { const AccountsPage({super.key}); @@ -14,184 +14,453 @@ class AccountsPage extends StatefulWidget { } class _AccountsPageState extends State { - String _npub = ''; - String _pubkeyHex = ''; - String _text = ''; - String _cipherText = ''; - final amber = Amberflutter(); + final _privateKeyController = TextEditingController(); + final _publicKeyController = TextEditingController(); + String? _currentAccount; // Stores hex pubkey of the current user + List _accounts = // Stores list of known hex pubkeys + []; // This would ideally be managed by NDK's account manager + bool _isAddingAccount = + false; // To toggle account addition form when logged in + Map _userMetadataCache = {}; // Cache for user metadata + + late Amberflutter _amberService; // Amberflutter instance + bool _amberIsAvailable = false; + + @override + void initState() { + super.initState(); + // _ndkInstance = ndk; // Use global 'ndk' directly + _amberService = Amberflutter(); // Initialize Amberflutter + _amberIsAvailable = amberAvailable; // Use amberAvailable from main.dart + _loadCurrentUser(); + _loadAccounts(); + } + + Future _fetchAndCacheMetadata(String pubkeyHex) async { + if (_userMetadataCache.containsKey(pubkeyHex) && + _userMetadataCache[pubkeyHex]!.name != null) { + // Already fetched and has a name, or a fetch attempt was made. + // To avoid refetching constantly for users with no set name, we might need a more nuanced check + // or rely on NDK's internal caching for fetch, but for UI, once attempted, we use what we have. + return; + } + try { + // Corrected: use loadMetadata instead of fetch + final metadata = await ndk.metadata.loadMetadata(pubkeyHex); + if (mounted && metadata != null) { + setState(() { + _userMetadataCache[pubkeyHex] = metadata; + }); + } else if (mounted && !_userMetadataCache.containsKey(pubkeyHex)) { + // If fetch returned null and we don't have any old cache, + // store a placeholder or an empty Metadata to prevent refetch loops for non-existent metadata. + // For simplicity, we can just ensure the key exists to prevent re-fetch, + // or handle it by checking metadata.name presence. + // Let's assume NDK handles not finding metadata gracefully and won't spam relays. + // If metadata is null, we'll just display the pubkey. + } + } catch (e) { + if (mounted) { + // Optionally, show a snackbar or log error + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text('Failed to fetch metadata for $pubkeyHex: $e')), + // ); + print('Failed to fetch metadata for $pubkeyHex: $e'); + } + } + } + + Future _loginWithPrivateKey() async { + final privateKey = _privateKeyController.text; + if (privateKey.isNotEmpty) { + try { + // NDK loginPrivateKey expects pubkey and privkey. + // We need to derive pubkey from privkey or ask the user for it. + // For simplicity, let's assume privkey is nsec and derive pubkey. + // This is a simplification; robust key handling is more complex. + + // Validate that the input is an nsec key. + if (!nip19_decoder.Nip19.isPrivateKey(privateKey)) { + // Use isPrivateKey for validation + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Invalid private key format, expected nsec.')), + ); + return; + } + final String hexPrivkey = + nip19_decoder.Nip19.decode(privateKey); // Returns hex string + if (hexPrivkey.isEmpty) { + // Check if decoding failed + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to decode private key.')), + ); + return; + } + + // Use NDK's Bip340 utility to get the public key from the private key. + final String pubkeyHex = bip340_utils.Bip340.getPublicKey(hexPrivkey); + + // Now call NDK's login method which will create its own signer internally. + ndk.accounts.loginPrivateKey( + pubkey: pubkeyHex, privkey: hexPrivkey); // Pass hex private key + _loadCurrentUser(); + _loadAccounts(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Logged in with private key!')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Private key login failed: $e')), + ); + } + } + } + + Future _loginWithPublicKey() async { + final publicKey = _publicKeyController.text; + if (publicKey.isNotEmpty) { + try { + // NDK loginPublicKey expects a hex pubkey. + // If user enters npub, we need to decode it. + String hexPubkey = publicKey; + if (nip19_decoder.Nip19.isPubkey(publicKey)) { + // Use isPubkey for validation + hexPubkey = + nip19_decoder.Nip19.decode(publicKey); // Returns hex string + if (hexPubkey.isEmpty) { + // Check if decoding failed + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to decode public key.')), + ); + return; + } + } else if (publicKey.startsWith("npub")) { + // This case handles strings that start with "npub" but might not be valid + // according to the more specific nip19_decoder.Nip19.isPubkey check. + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Invalid npub public key format.')), + ); + return; + } + // If not an npub (or if it was already hex), assume hexPubkey is now the hex public key. + // Further validation for hex format could be added here if necessary. + + ndk.accounts.loginPublicKey(pubkey: hexPubkey); // Use global ndk + _loadCurrentUser(); + _loadAccounts(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Logged in with public key (read-only)!')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Public key login failed: $e')), + ); + } + } + } + + Future _loginWithAmber() async { + if (!_amberIsAvailable) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Amber is not available on this device.')), + ); + return; + } + try { + // Request only the public key, no extra permissions needed for read-only login + final dynamic amberResult = await _amberService.getPublicKey(); + String? pubkeyFromAmberNpub; + + if (amberResult is Map && amberResult.containsKey('signature')) { + pubkeyFromAmberNpub = amberResult['signature'] as String?; + } else if (amberResult is String) { + // Older versions might return string directly + pubkeyFromAmberNpub = amberResult; + } + + if (pubkeyFromAmberNpub != null && pubkeyFromAmberNpub.isNotEmpty) { + String hexPubkey = pubkeyFromAmberNpub; + if (nip19_decoder.Nip19.isPubkey(pubkeyFromAmberNpub)) { + // Use isPubkey for validation + hexPubkey = nip19_decoder.Nip19.decode( + pubkeyFromAmberNpub); // Returns hex string + if (hexPubkey.isEmpty) { + // Check if decoding failed + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to decode public key from Amber.')), + ); + return; + } + } else if (pubkeyFromAmberNpub.startsWith("npub")) { + // This case handles strings that start with "npub" but might not be valid + // according to the more specific nip19_decoder.Nip19.isPubkey check. + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Invalid npub public key format from Amber.')), + ); + return; + } + // If not an npub (or if it was already hex), assume hexPubkey is now the hex public key. + // Further validation for hex format could be added here if necessary. + + ndk.accounts.loginPublicKey(pubkey: hexPubkey); // Use global ndk + _loadCurrentUser(); + _loadAccounts(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Logged in with Amber (read-only): $pubkeyFromAmberNpub')), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Failed to get public key from Amber or key is empty.')), + ); + } + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Amber login failed: $e')), + ); + } + } + + void _loadCurrentUser() { + final currentAccountPubkey = ndk.accounts.getLoggedAccount()?.pubkey; + setState(() { + _currentAccount = currentAccountPubkey; + if (currentAccountPubkey != null) { + if (!_accounts.contains(currentAccountPubkey)) { + _accounts.add(currentAccountPubkey); + } + _fetchAndCacheMetadata(currentAccountPubkey); + } + }); + } + + void _loadAccounts() { + final allKnownAccounts = ndk.accounts.accounts.values.toList(); + final allKnownPubkeys = + allKnownAccounts.map((account) => account.pubkey).toSet().toList(); + + setState(() { + _accounts = allKnownPubkeys; + if (_currentAccount != null && !_accounts.contains(_currentAccount!)) { + // This case should ideally not happen if _loadCurrentUser runs after login + _accounts.add(_currentAccount!); + } + }); + // Fetch metadata for all known accounts + for (var pubkey in _accounts) { + _fetchAndCacheMetadata(pubkey); + } + } + + Future _switchAccount(String pubkey) async { + try { + // NDK switchAccount expects a hex pubkey. + // The list might store npub or hex. Ensure it's hex. + String hexPubkey = pubkey; + if (nip19_decoder.Nip19.isPubkey(pubkey)) { + // Use isPubkey for validation + hexPubkey = nip19_decoder.Nip19.decode(pubkey); // Returns hex string + if (hexPubkey.isEmpty) { + // Check if decoding failed + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to decode public key for switching.')), + ); + return; + } + } else if (pubkey.startsWith("npub")) { + // This case handles strings that start with "npub" but might not be valid + // according to the more specific nip19_decoder.Nip19.isPubkey check. + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Invalid npub public key format for switching.')), + ); + return; + } + // If not an npub (or if it was already hex), assume hexPubkey is now the hex public key. + // Further validation for hex format could be added here if necessary. + ndk.accounts.switchAccount(pubkey: hexPubkey); // Corrected + _loadCurrentUser(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Switched to $pubkey')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to switch account: $e')), + ); + } + } + + Future _logout() async { + ndk.accounts.logout(); // Corrected, it's not async + _loadCurrentUser(); + _loadAccounts(); // This will now reflect that no user is active + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Logged out')), + ); + } @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FilledButton( - onPressed: () async { - amber.getPublicKey( - permissions: [ - const Permission( - type: "nip04_encrypt", + return Scaffold( + appBar: AppBar( + title: const Text('Accounts Management'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current Account: ${(_currentAccount != null ? _userMetadataCache[_currentAccount]?.name ?? _userMetadataCache[_currentAccount]?.displayName ?? nip19_decoder.Nip19.encodePubKey(_currentAccount!) : null) ?? "Not logged in"}', + style: Theme.of(context).textTheme.headlineSmall), + const SizedBox(height: 20), + if (_currentAccount == null || _isAddingAccount) ...[ + // Show login/add account forms if not logged in OR if in "adding account" mode + if (_isAddingAccount && _currentAccount != null) + Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: ElevatedButton( + onPressed: () { + setState(() { + _isAddingAccount = false; + }); + }, + child: const Text('Cancel Adding Account'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange), + ), ), - const Permission( - type: "nip04_decrypt", + TextField( + controller: _privateKeyController, + decoration: + const InputDecoration(hintText: 'Enter your nsec...'), + ), + ElevatedButton( + onPressed: () async { + await _loginWithPrivateKey(); + if (mounted && _currentAccount != null) { + // If login was successful + setState(() { + _isAddingAccount = + false; // Exit adding mode after successful login + }); + } + }, + child: const Text('Login with Private Key'), + ), + const SizedBox(height: 20), + TextField( + controller: _publicKeyController, + decoration: + const InputDecoration(hintText: 'Enter your npub...'), + ), + ElevatedButton( + onPressed: () async { + await _loginWithPublicKey(); + if (mounted && _currentAccount != null) { + // If login was successful + setState(() { + _isAddingAccount = + false; // Exit adding mode after successful login + }); + } + }, + child: const Text('Login with Public Key'), + ), + if (_amberIsAvailable) ...[ + const SizedBox(height: 20), + ElevatedButton( + onPressed: () async { + await _loginWithAmber(); + if (mounted && _currentAccount != null) { + // If login was successful + setState(() { + _isAddingAccount = + false; // Exit adding mode after successful login + }); + } + }, + child: const Text('Login with Amber'), ), ], - ).then((value) { - _npub = value['signature'] ?? ''; - _pubkeyHex = Nip19.decode(_npub); - setState(() { - _text = '$value'; - }); - }); - }, - child: const Text('Get Public Key'), - ), - FilledButton( - onPressed: () { - final eventJson = jsonEncode({ - 'id': '', - 'pubkey': Nip19.decode(_npub), - 'kind': 1, - 'content': 'Hello from Amber Flutter!', - 'created_at': - (DateTime.now().millisecondsSinceEpoch / 1000).round(), - 'tags': [], - 'sig': '', - }); - - amber - .signEvent( - currentUser: _npub, - eventJson: eventJson, - ) - .then((value) { - setState(() { - _text = '$value'; - }); - }); - }, - child: const Text('Sign Event'), - ), - FilledButton( - onPressed: () async { - final eventJson = jsonEncode({ - 'id': '', - 'pubkey': Nip19.decode(_npub), - 'kind': 1, - 'content': 'Hello from Amber Flutter!', - 'created_at': - (DateTime.now().millisecondsSinceEpoch / 1000).round(), - 'tags': [], - 'sig': '', - }); - - var value = await amber.signEvent( - currentUser: _npub, - eventJson: eventJson, - ); - EventVerifier eventVerifier = RustEventVerifier(); - eventVerifier - .verify(Nip01Event.fromJson(json.decode(value['event']))) - .then((valid) { - setState(() { - _text = valid ? "✅ Valid" : "❌ Invalid"; - }); - }); - }, - child: const Text('Verify signature'), - ), - FilledButton( - onPressed: () { - amber - .nip04Encrypt( - plaintext: "Hello from Amber Flutter, Nip 04!", - currentUser: _npub, - pubKey: _pubkeyHex, - ) - .then((value) { - _cipherText = value['signature'] ?? ''; - setState(() { - _text = '$value'; - }); - }); - }, - child: const Text('Nip 04 Encrypt'), - ), - FilledButton( - onPressed: () async { - amber - .nip04Decrypt( - ciphertext: _cipherText, - currentUser: _npub, - pubKey: _pubkeyHex, - ) - .then((value) { - setState(() { - _text = '$value 1'; - }); - }); - // ; - amber - .nip04Decrypt( - ciphertext: _cipherText, - currentUser: _npub, - pubKey: _pubkeyHex, - ) - .then((value) { - setState(() { - _text = '$value 2'; - }); - }); - // , - amber - .nip04Decrypt( - ciphertext: _cipherText, - currentUser: _npub, - pubKey: _pubkeyHex, - ) - .then((value) { - setState(() { - _text = '$value 3'; - }); - }); - }, - child: const Text('Nip 04 Decrypt'), - ), - FilledButton( - onPressed: () { - amber - .nip44Encrypt( - plaintext: "Hello from Amber Flutter, Nip 44!", - currentUser: _npub, - pubKey: _pubkeyHex, - ) - .then((value) { - _cipherText = value['signature'] ?? ''; - setState(() { - _text = '$value'; - }); - }); - }, - child: const Text('Nip 44 Encrypt'), - ), - FilledButton( - onPressed: () { - amber - .nip44Decrypt( - ciphertext: _cipherText, - currentUser: _npub, - pubKey: _pubkeyHex, - ) - .then((value) { - setState(() { - _text = '$value'; - }); - }); - }, - child: const Text('Nip 44 Decrypt'), + ] else ...[ + // Logged in view (not adding account) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ElevatedButton( + onPressed: _logout, + child: const Text('Logout'), + style: + ElevatedButton.styleFrom(backgroundColor: Colors.red), + ), + ElevatedButton( + onPressed: () { + setState(() { + _isAddingAccount = true; + }); + }, + child: const Text('Add Another Account'), + ), + ], + ), + const SizedBox(height: 20), // Added space + Text('Available Accounts:', + style: Theme.of(context).textTheme.titleMedium), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _accounts.length, + itemBuilder: (context, index) { + final accountPubkeyHex = _accounts[index]; + final metadata = _userMetadataCache[accountPubkeyHex]; + final npub = + nip19_decoder.Nip19.encodePubKey(accountPubkeyHex); + final displayName = + metadata?.name ?? metadata?.displayName ?? npub; + + ImageProvider? avatarImage; + if (metadata?.picture != null && + metadata!.picture!.isNotEmpty) { + avatarImage = NetworkImage(metadata.picture!); + } + + return ListTile( + leading: CircleAvatar( + backgroundImage: avatarImage, + onBackgroundImageError: avatarImage != null + ? (exception, stackTrace) { + print( + 'Error loading avatar for $npub: $exception'); + // Optionally update UI to show placeholder if error occurs after initial load attempt + // For simplicity, this example relies on initial null check for placeholder + } + : null, + child: + avatarImage == null ? const Icon(Icons.person) : null, + ), + title: Text(displayName), + subtitle: Text(npub), // Show npub as subtitle + trailing: _currentAccount == accountPubkeyHex + ? const Icon(Icons.check_circle, color: Colors.green) + : ElevatedButton( + onPressed: () => _switchAccount(accountPubkeyHex), + child: const Text('Switch'), + ), + ); + }, + ), + ], + ], ), - Text(_text), - ], + ), ); } } diff --git a/packages/sample-app/lib/base.dart b/packages/sample-app/lib/base.dart new file mode 100644 index 000000000..ac1f60c91 --- /dev/null +++ b/packages/sample-app/lib/base.dart @@ -0,0 +1,727 @@ +import 'package:flutter/material.dart'; +import 'package:zaplab_design/zaplab_design.dart'; +import 'section.dart'; + +class BaseTab extends StatelessWidget { + const BaseTab({super.key}); + + TabData tabData(BuildContext context) { + // final theme = AppTheme.of(context); + + return TabData( + label: 'Base', + icon: const Icon(Icons.abc), + content: Builder( + builder: (context) { + final theme = AppTheme.of(context); + + return AppContainer( + padding: const AppEdgeInsets.all(AppGapSize.s16), + child: Column( + children: [ + Section( + title: 'AppSkeletonLoader', + description: + 'This is a widget that fills the widget it is placed in with a gradient animation.', + children: [ + AppContainer( + height: 144, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration( + color: theme.colors.grey66, + borderRadius: theme.radius.asBorderRadius().rad16, + ), + child: const AppSkeletonLoader(), + ), + ], + ), + Section( + title: 'AppCheckBox', + description: 'This is a classic checkbox widget.', + children: [ + AppPanel( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + AppCheckBox( + value: false, + onChanged: (value) {}, + ), + const AppGap(AppGapSize.s12), + AppText.reg14( + 'Set to false (default)', + color: theme.colors.white66, + ), + ], + ), + const AppGap(AppGapSize.s16), + Row( + children: [ + AppCheckBox( + value: true, + onChanged: (value) {}, + ), + const AppGap(AppGapSize.s12), + AppText.reg14( + 'Set to true', + color: theme.colors.white66, + ), + ], + ), + ], + ), + ), + ], + ), + Section( + title: 'AppSelector', + description: + 'This simple selector widget can be used to select an option or to switch between the Tabs displayed under it. You can specify the mode of the selector by setting the emphasized parameter to true or false. The content of each AppSelectorButton can be separately defined for when it is selected and when it is not.', + children: [ + AppContainer( + width: double.infinity, + child: AppText.h3( + 'DEFAULT MODE', + color: theme.colors.white33, + ), + ), + const AppGap(AppGapSize.s12), + AppSelector( + children: [ + AppSelectorButton( + selectedContent: const [AppText.med14('Option 1')], + unselectedContent: [ + AppText.med14( + 'Option 1', + color: theme.colors.white33, + ) + ], + isSelected: true, + onTap: () {}, + ), + AppSelectorButton( + selectedContent: [AppText.med14('Option 2')], + unselectedContent: [ + AppText.med14( + 'Option 2', + color: theme.colors.white33, + ) + ], + isSelected: false, + onTap: () {}, + ), + AppSelectorButton( + selectedContent: [AppText.med14('Option 3')], + unselectedContent: [ + AppText.med14( + 'Option 3', + color: theme.colors.white33, + ) + ], + isSelected: false, + onTap: () {}, + ), + ], + onChanged: (index) {}, + ), + const AppGap(AppGapSize.s16), + AppContainer( + width: double.infinity, + child: AppText.h3( + 'EMPHASIZED MODE', + color: theme.colors.white33, + ), + ), + const AppGap(AppGapSize.s12), + AppSelector( + emphasized: true, + children: [ + AppSelectorButton( + selectedContent: [ + AppIcon.s16( + theme.icons.characters.bell, + color: AppColorsData.dark().white, + ), + AppGap.s8(), + AppText.med14('21', + color: AppColorsData.dark().white), + ], + unselectedContent: [ + AppIcon.s16( + theme.icons.characters.bell, + outlineColor: theme.colors.white33, + outlineThickness: + LineThicknessData.normal().medium, + ), + AppGap.s8(), + AppText.med14('21', color: theme.colors.white33), + ], + isSelected: true, + onTap: () {}, + ), + AppSelectorButton( + selectedContent: [ + AppIcon.s16( + theme.icons.characters.reply, + outlineColor: AppColorsData.dark().white, + outlineThickness: + LineThicknessData.normal().medium, + ), + AppGap.s8(), + AppText.med14( + '12', + color: AppColorsData.dark().white, + ), + ], + unselectedContent: [ + AppIcon.s16( + theme.icons.characters.reply, + outlineColor: theme.colors.white33, + outlineThickness: + LineThicknessData.normal().medium, + ), + AppGap.s8(), + AppText.med14('12', color: theme.colors.white33), + ], + isSelected: true, + onTap: () {}, + ), + AppSelectorButton( + selectedContent: [ + AppIcon.s18( + theme.icons.characters.zap, + color: AppColorsData.dark().white, + ), + AppGap.s8(), + AppText.med14('5', + color: AppColorsData.dark().white), + ], + unselectedContent: [ + AppIcon.s18( + theme.icons.characters.zap, + outlineColor: theme.colors.white33, + outlineThickness: + LineThicknessData.normal().medium, + ), + AppGap.s8(), + AppText.med14('5', color: theme.colors.white33), + ], + isSelected: true, + onTap: () {}, + ), + AppSelectorButton( + selectedContent: [ + AppIcon.s18( + theme.icons.characters.at, + outlineColor: AppColorsData.dark().white, + outlineThickness: + LineThicknessData.normal().medium, + ), + AppGap.s8(), + AppText.med14('2', + color: AppColorsData.dark().white), + ], + unselectedContent: [ + AppIcon.s18( + theme.icons.characters.at, + outlineColor: theme.colors.white33, + outlineThickness: + LineThicknessData.normal().medium, + ), + AppGap.s8(), + AppText.med14('2', color: theme.colors.white33), + ], + isSelected: true, + onTap: () {}, + ), + AppSelectorButton( + selectedContent: [ + AppIcon.s18( + theme.icons.characters.emojiFill, + color: AppColorsData.dark().white, + ), + AppGap.s8(), + AppText.med14('2', + color: AppColorsData.dark().white), + ], + unselectedContent: [ + AppIcon.s18( + theme.icons.characters.emojiLine, + outlineColor: theme.colors.white33, + outlineThickness: + LineThicknessData.normal().medium, + ), + AppGap.s8(), + AppText.med14('2', color: theme.colors.white33), + ], + isSelected: true, + onTap: () {}, + ), + ], + onChanged: (index) {}, + ), + ], + ), + Section( + title: 'AppPanel', + description: + 'This is an AppContainer with a predefined border radius, a default padding (that can be set to false) and a background color that auto-adjusts when used inside of an AppModal.', + children: [ + AppPanel( + child: AppContainer( + decoration: BoxDecoration( + border: Border.all( + color: theme.colors.white8, + ), + ), + padding: const AppEdgeInsets.all(AppGapSize.s16), + child: Center( + child: AppText.reg14( + 'Content', + color: theme.colors.white66, + ), + ), + ), + ), + ], + ), + // Section( + // title: 'AppScreen', + // description: + // 'Screen that slides in from the bottom and lets you travel back to previous screens and the home screen in multiple ways.', + // children: [ + // AppPanel( + // child: AppSmallButton( + // inactiveColor: theme.colors.white16, + // content: [ + // AppText.med14('Open AppScreen', + // color: theme.colors.white), + // ], + // onTap: () { + // AppScreen.show( + // context, + // topBarContent: AppText.med16( + // 'Current Screen', + // ), + // onHomeTap: () { + // Navigator.of(context).pop(); + // }, + // history: [ + // HistoryItem( + // contentType: 'Community', + // title: 'Nips Out', + // ), + // HistoryItem( + // contentType: 'Wiki', + // title: 'NIP-B4 - History Links', + // ), + // HistoryItem( + // contentType: 'Profile', + // title: 'ثعبان', + // ), + // ], + // child: Column( + // children: [ + // AppPost( + // profileName: 'ثعبان', + // profilePicUrl: + // 'https://nostr.download/1aba957814cac9c324c54d94e0ba6606dc50af17f7c08654e9b9f139a9720d6d.jpeg', + // timestamp: DateTime.now(), + // content: + // 'I don\'t want to have to tap Back twelve times just to go back home 🏠. \n\nThat type of Big Tech UX is why hardly anyone ever wanders beyond like two clicks of tHe FeEd.', + // communities: [ + // Community( + // name: 'Nips Out', + // profilePicUrl: + // 'https://cdn.satellite.earth/1895487e0fcd0db92babfa58501fd7cd319620c818e01d7bb941c4d465e4d685.png', + // ), + // ], + // ), + // AppTabView( + // tabs: [ + // TabData( + // label: 'Replies', + // icon: AppIcon.s20( + // theme.icons.characters.reply, + // outlineColor: theme.colors.white66, + // outlineThickness: + // LineThicknessData.normal().medium, + // ), + // count: 21, + // content: Column( + // children: [ + // AppFeedPost( + // content: + // 'Yeah, this is why I\'m not using Nostr so much on mobile. The browser experience is king, for now.', + // profileName: 'James Lewis', + // profilePicUrl: + // 'https://i.nostr.build/zdMAY.jpg', + // timestamp: DateTime.now(), + // zaps: [ + // Zap( + // amount: 100, + // profileName: 'ثعبان', + // profilePicUrl: + // 'https://nostr.download/1aba957814cac9c324c54d94e0ba6606dc50af17f7c08654e9b9f139a9720d6d.jpeg', + // timestamp: DateTime.now(), + // ), + // Zap( + // amount: 56, + // profileName: 'Pip', + // profilePicUrl: + // 'https://m.primal.net/IfSZ.jpg', + // timestamp: DateTime.now(), + // ), + // ], + // reactions: [ + // Reaction( + // emojiUrl: + // 'https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Femojiguide.org%2Fimages%2Femoji%2Fc%2F1e2fb481tsfvyc.png&f=1&nofb=1&ipt=73d8789f7a055e207ff06bd2278184a2ab6108a8c019f59d0526d05f91d925e7&ipo=images', + // profilePicUrl: + // 'https://nostr.download/1aba957814cac9c324c54d94e0ba6606dc50af17f7c08654e9b9f139a9720d6d.jpeg', + // profileName: "ثعبان", + // timestamp: DateTime.now(), + // ), + // Reaction( + // emojiUrl: + // 'https://cdn.satellite.earth/60a5e73bfa6dfd35bd0b144f38f6ed2aaab0606b2bd68b623f419ae0709fa10a.png', + // profilePicUrl: + // 'https://cdn.satellite.earth/946822b1ea72fd3710806c07420d6f7e7d4a7646b2002e6cc969bcf1feaa1009.png', + // profileName: "Niel Liesmons", + // timestamp: DateTime.now(), + // ), + // ], + // topReplies: [ + // ReplyUserData( + // profileName: 'Vinney', + // profilePicUrl: + // 'https://m.primal.net/HdAt.jpg', + // ), + // ReplyUserData( + // profileName: 'jrm', + // profilePicUrl: + // 'https://pfp.nostr.build/e9e7963637e04d90ad2c33f21c6f112a188c5b001dd697e108991261487aa258.jpg', + // ), + // ReplyUserData( + // profileName: 'elsat', + // profilePicUrl: + // 'https://image.nostr.build/ba781633731cd33bd20f58bbca208ae87db3f87c8f2256e23e4a8df543617c6c.png', + // ), + // ], + // totalReplies: 10, + // ), + // AppFeedPost( + // content: + // 'You might want to look into the AppScreen widget we have in the Zaplab Design package. It gives access to history links and home when the user swipes down on a screen.', + // profileName: 'Niel Liesmons', + // profilePicUrl: + // 'https://cdn.satellite.earth/946822b1ea72fd3710806c07420d6f7e7d4a7646b2002e6cc969bcf1feaa1009.png', + // timestamp: DateTime.now(), + // zaps: [ + // Zap( + // amount: 2100, + // profileName: 'Gzuuus', + // profilePicUrl: + // 'https://pfp.nostr.build/3e72dab77cfcb2339a30a832c891064e38d70ad652cb58306516e34e78e84325.png', + // timestamp: DateTime.now(), + // ), + // ], + // topReplies: [ + // ReplyUserData( + // profileName: 'jrm', + // profilePicUrl: + // 'https://pfp.nostr.build/e9e7963637e04d90ad2c33f21c6f112a188c5b001dd697e108991261487aa258.jpg', + // ), + // ReplyUserData( + // profileName: 'elsat', + // profilePicUrl: + // 'https://image.nostr.build/ba781633731cd33bd20f58bbca208ae87db3f87c8f2256e23e4a8df543617c6c.png', + // ), + // ReplyUserData( + // profileName: 'Pip', + // profilePicUrl: + // 'https://m.primal.net/IfSZ.jpg', + // ), + // ], + // totalReplies: 6, + // ), + // AppFeedPost( + // content: 'I feel you bro.', + // profileName: 'elsat', + // profilePicUrl: + // 'https://image.nostr.build/ba781633731cd33bd20f58bbca208ae87db3f87c8f2256e23e4a8df543617c6c.png', + // timestamp: DateTime.now(), + // ), + // AppFeedPost( + // content: + // 'True story, I rarely dive deep on mobile with the apps we have now.', + // profileName: 'Pip', + // profilePicUrl: + // 'https://m.primal.net/IfSZ.jpg', + // timestamp: DateTime.now(), + // ), + // AppFeedPost( + // content: 'test test', + // profileName: 'elsat', + // profilePicUrl: + // 'https://image.nostr.build/ba781633731cd33bd20f58bbca208ae87db3f87c8f2256e23e4a8df543617c6c.png', + // timestamp: DateTime.now(), + // ), + // AppFeedPost( + // content: 'test test', + // profileName: 'elsat', + // profilePicUrl: + // 'https://image.nostr.build/ba781633731cd33bd20f58bbca208ae87db3f87c8f2256e23e4a8df543617c6c.png', + // timestamp: DateTime.now(), + // ), + // AppFeedPost( + // content: 'test test', + // profileName: 'elsat', + // profilePicUrl: + // 'https://image.nostr.build/ba781633731cd33bd20f58bbca208ae87db3f87c8f2256e23e4a8df543617c6c.png', + // timestamp: DateTime.now(), + // ), + // AppFeedPost( + // content: 'test test', + // profileName: 'elsat', + // profilePicUrl: + // 'https://image.nostr.build/ba781633731cd33bd20f58bbca208ae87db3f87c8f2256e23e4a8df543617c6c.png', + // timestamp: DateTime.now(), + // ), + // ], + // ), + // settingsContent: AppContainer( + // padding: const AppEdgeInsets.all( + // AppGapSize.s64), + // child: const AppText.reg14( + // 'Reply Settings Content'), + // ), + // settingsDescription: + // 'Choose which replies are displayed', + // ), + // TabData( + // label: 'Shares', + // icon: AppIcon.s20( + // theme.icons.characters.share, + // outlineColor: theme.colors.white66, + // outlineThickness: + // LineThicknessData.normal().medium, + // ), + // count: 5, + // content: AppContainer( + // padding: const AppEdgeInsets.all( + // AppGapSize.s16), + // child: Column( + // children: List.generate( + // 5, + // (index) => AppContainer( + // margin: + // const AppEdgeInsets.only( + // bottom: AppGapSize.s12), + // height: 60, + // child: Center( + // child: AppText.reg14( + // 'Share ${index + 1}'), + // ), + // ), + // ), + // ), + // ), + // settingsContent: AppContainer( + // padding: const AppEdgeInsets.all( + // AppGapSize.s16), + // child: Column( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // const AppText.med16( + // 'Share Settings'), + // const AppGap.s16(), + // AppSwitch( + // value: AppResponsiveTheme.of( + // context) + // .colorMode == + // AppThemeColorMode.grey, + // onChanged: (bool value) { + // Future.microtask(() { + // AppResponsiveTheme.of( + // context) + // .setColorMode( + // value + // ? AppThemeColorMode + // .grey + // : null, + // ); + // }); + // }, + // ), + // const AppGap.s8(), + // AppText.reg14( + // 'Show share notifications', + // color: theme.colors.white66, + // ), + // ], + // ), + // ), + // settingsDescription: + // 'Configure share notifications', + // ), + // TabData( + // label: 'Lists', + // icon: AppIcon.s20( + // theme.icons.characters.label, + // outlineColor: theme.colors.white66, + // outlineThickness: + // LineThicknessData.normal().medium, + // ), + // count: 103, + // content: AppContainer( + // padding: const AppEdgeInsets.all( + // AppGapSize.s16), + // child: Column( + // children: List.generate( + // 103, + // (index) => AppContainer( + // margin: + // const AppEdgeInsets.only( + // bottom: AppGapSize.s12), + // height: 60, + // child: Center( + // child: AppText.reg14( + // 'List ${index + 1}'), + // ), + // ), + // ), + // ), + // ), + // settingsContent: AppContainer( + // padding: const AppEdgeInsets.all( + // AppGapSize.s16), + // child: Column( + // crossAxisAlignment: + // CrossAxisAlignment.start, + // children: [ + // const AppText.med16( + // 'Like Settings'), + // const AppGap.s16(), + // const AppSwitch(), + // const AppGap.s8(), + // AppText.reg14( + // 'Show like notifications', + // color: theme.colors.white66, + // ), + // const AppGap.s16(), + // const AppSwitch(), + // const AppGap.s8(), + // AppText.reg14( + // 'Group likes together', + // color: theme.colors.white66, + // ), + // ], + // ), + // ), + // settingsDescription: + // 'Configure like notifications', + // ), + // TabData( + // label: 'Tools', + // icon: AppIcon.s20( + // theme.icons.characters.tools, + // outlineColor: theme.colors.white66, + // outlineThickness: + // LineThicknessData.normal().medium, + // ), + // content: AppContainer( + // padding: const AppEdgeInsets.all( + // AppGapSize.s16), + // child: AppText.reg14( + // 'Display Tools in a Grid'), + // ), + // ), + // TabData( + // label: 'Details', + // icon: AppIcon.s20( + // theme.icons.characters.details, + // outlineColor: theme.colors.white66, + // outlineThickness: + // LineThicknessData.normal().medium, + // ), + // content: AppContainer( + // padding: const AppEdgeInsets.all( + // AppGapSize.s16), + // child: + // AppText.reg14('Details Content'), + // ), + // ), + // ], + // ), + // ], + // ), + // ); + // }, + // ), + // ), + // ]), + Section( + title: 'AppSwitch', + description: + 'This is a switch widget that can be used to toggle between two states.', + children: [ + AppPanel( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AppText.med14( + 'Grey Mode', + color: theme.colors.white, + ), + AppSwitch( + value: AppResponsiveTheme.of(context).colorMode == + AppThemeColorMode.grey, + onChanged: (bool value) { + Future.microtask(() { + AppResponsiveTheme.of(context).setColorMode( + value ? AppThemeColorMode.grey : null, + ); + }); + }, + ), + ], + ), + ), + ], + ), + const Section( + title: 'AppCodeBlock', + description: + 'This is a code block widget that can be used to display code in a readable format.', + children: [ + const AppCodeBlock( + code: r'''{ + kind: 7375, + content: "Thanks for the coffee", + pubkey: "sender-pubkey", + tags: [ + [ "amount", "1000", "msat" ], + [ "token", "cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IjAwNDE0NmJkZjRhOWFmYWIiLCJhbW91bnQiOjEsInNlY3JldCI6IltcIlAyUEtcIix7XCJub25jZVwiOlwiYjI0NDNkZDRmMDQxNjgyYjRkMmEwMzkwNGQ5MDAyNjRiNzI1MzgwZTQ0YWM0MDk2Y2EwZWE2NDAzMGY0Mjc4OFwiLFwiZGF0YVwiOlwiZTlmYmNlZDNhNDJkY2Y1NTE0ODY2NTBjYzc1MmFiMzU0MzQ3ZGQ0MTNiMzA3NDg0ZTRmZDE4MThhYjUzZjk5MTExXCJ9XSIsIkMiOiIwMjYyOTM5ODRjODg1OTFiMzA2MzUxYjY5ZmNjODAxNGQ1NTc5MmYzMTQwYWEyZDlhYmQ0NGZhOWY0Y2Y2ZmQzZjEifV0sIm1pbnQiOiJodHRwczovL3N0YWJsZW51dC51bWludC5jYXNoIn1dLCJ1bml0Ijoic2F0In0="] + [ "u", "https://stablenut.umint.cash", ], + [ "e", "" ], + [ "p", "e9fbced3a42dcf551486650cc752ab354347dd413b307484e4fd1818ab53f991" ] + ] +}''', + language: 'JSON', + ), + ]), + ], + ), + ); + }, + ), + ); + } + + @override + Widget build(BuildContext context) => tabData(context).content; +} diff --git a/packages/sample-app/lib/blossom_page.dart b/packages/sample-app/lib/blossom_page.dart index 5fb0e6272..8a265c520 100644 --- a/packages/sample-app/lib/blossom_page.dart +++ b/packages/sample-app/lib/blossom_page.dart @@ -49,7 +49,7 @@ class _BlossomMediaPageState extends State { sha256: 'fc0066f8d123cf9cbe2bd95e3439cd91b5401e0560dab65a49695ab932ffec59', serverUrls: [ - 'https://blossom.f7z.io', + // 'https://blossom.f7z.io', "https://nostr.download", "https://cdn.hzrd149.com" ], diff --git a/packages/sample-app/lib/homepage.dart b/packages/sample-app/lib/homepage.dart new file mode 100644 index 000000000..4f5ec009d --- /dev/null +++ b/packages/sample-app/lib/homepage.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:zaplab_design/zaplab_design.dart'; +import 'dart:ui'; +import 'dart:io'; + +import 'base.dart'; + +class HomePage extends StatelessWidget { + + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + // final theme = AppTheme.of(context); + + return Stack( + children: [ + AppScaffold( + body: SingleChildScrollView( + child: AppContainer( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const AppGap.s72(), + AppContainer( + child: Column( + children: [ + AppProfilePicSquare.s104( + 'https://cdn.satellite.earth/da67840aae6720f5e5fb9e4c8ce25a85f6d8cbf22f4a04fd44babd58a9badfc6.png'), + const AppGap.s16(), + const AppText.h1('Zaplab'), + const AppGap.s8(), + AppText.med16( + 'Demo App', + color: Colors.white60 + // theme.colors.white66, + ), + const AppGap.s8(), + ], + ), + ), + const AppGap.s16(), + AppTabView( + tabs: [ + const BaseTab().tabData(context), + const BaseTab().tabData(context), + const BaseTab().tabData(context), + // ChatTab.tabData(context), + // const ButtonsTab().tabData(context), + // const LoadersTab().tabData(context), + // const ProfilePicsTab().tabData(context), + // const PostsTab().tabData(context), + // const AsciidocDemoTab().tabData(context), + // const ToastsTab().tabData(context), + ], controller: AppTabController(length: 4) + ), + ], + ), + ), + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24), + child: AppContainer( + height: Platform.isIOS || Platform.isAndroid + ? MediaQuery.of(context).padding.top + : 24, + decoration: BoxDecoration( + color: Colors.black54, + // theme.colors.black66, + border: Border( + bottom: BorderSide( + color: Colors.white12, + width: LineThicknessData.normal().thin, + ), + ), + ), + ), + ), + ), + ), + Positioned( + bottom: 0, + left: 0, + right: 0, + child: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 24, sigmaY: 24), + child: AppContainer( + height: Platform.isIOS || Platform.isAndroid + ? MediaQuery.of(context).padding.bottom + : 0, + decoration: BoxDecoration( + color: Colors.black54, + //theme.colors.black66, + border: Border( + top: BorderSide( + color: Colors.white12, + // theme.colors.white16, + width: LineThicknessData.normal().thin, + ), + ), + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/packages/sample-app/lib/main.dart b/packages/sample-app/lib/main.dart index c46eb32cd..44a9dfa19 100644 --- a/packages/sample-app/lib/main.dart +++ b/packages/sample-app/lib/main.dart @@ -3,18 +3,19 @@ import 'package:flutter/material.dart'; import 'package:media_kit/media_kit.dart'; import 'package:ndk/ndk.dart'; import 'package:ndk_demo/accounts_page.dart'; +import 'package:ndk_demo/blossom_page.dart'; import 'package:ndk_demo/nwc_page.dart'; import 'package:ndk_demo/relays_page.dart'; -import 'package:ndk_rust_verifier/ndk_rust_verifier.dart'; +import 'package:ndk_demo/zaps_page.dart'; +import 'package:protocol_handler/protocol_handler.dart'; import 'amber_page.dart'; -import 'blossom_page.dart'; bool amberAvailable = false; final ndk = Ndk( NdkConfig( - eventVerifier: RustEventVerifier(), + eventVerifier: Bip340EventVerifier(), cache: MemCacheManager(), logLevel: Logger.logLevels.trace, ), @@ -23,6 +24,12 @@ final ndk = Ndk( Future main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); + try { + await protocolHandler.register("ndk"); + } catch (err) { + print(err); + } + try { final amber = Amberflutter(); amberAvailable = await amber.isAppInstalled(); @@ -30,91 +37,280 @@ Future main() async { // not on android or amber not installed } - runApp(const MyApp()); + runApp(MyApp()); } class MyApp extends StatelessWidget { - const MyApp({super.key}); + MyApp({super.key}); + + final GlobalKey<_MyHomePageState> _homePageKey = + GlobalKey<_MyHomePageState>(); + + // final _router = GoRouter( + // routes: [ + // GoRoute( + // path: '/', + // builder: (context, state) => const HomePage(), + // ), + // ], + // ); @override Widget build(BuildContext context) { + // return ProviderScope( + // child: HomePage() + // // child: AppBase( + // // title: 'NDK Demo', + // // routerConfig: _router, + // // appLogo: Icon(Icons.add), + // // // Image.asset( + // // // 'assets/images/logo.png', + // // // fit: BoxFit.contain, + // // // ), + // // darkAppLogo: Icon(Icons.add), + // // + // // // Image.asset( + // // // 'assets/images/logo_dark.png', + // // // fit: BoxFit.contain, + // // // ), + // // ), + // ); + return MaterialApp( title: 'Nostr Developer Kit Demo', theme: ThemeData( primarySwatch: Colors.blue, ), - home: SafeArea(child: const MyHomePage(), top: false), + home: SafeArea( + top: false, + child: MyHomePage(key: _homePageKey)), // Pass the instance key ); } } class MyHomePage extends StatefulWidget { + // Removed static GlobalKey. The key is now passed via constructor by MyApp. + // The constructor now implicitly uses super.key for the key passed by MyApp. const MyHomePage({super.key}); @override State createState() => _MyHomePageState(); } -class _MyHomePageState extends State { +class _MyHomePageState extends State + with TickerProviderStateMixin, ProtocolListener { + late TabController _tabController; + late List _tabs; + late List _tabPages; + + // Define a constant for the NWC tab name to avoid magic strings + static const String nwcTabName = 'NWC'; + + @override + void initState() { + super.initState(); + + // Define tabs and their corresponding pages + // This centralizes the tab definitions. + _tabs = [ + const Tab(text: 'Accounts'), + const Tab(text: 'Metadata'), + const Tab(text: 'Relays'), + const Tab(text: nwcTabName), // Use the constant + const Tab(text: 'Zaps'), + const Tab(text: "Blossom"), + // if (amberAvailable) const Tab(text: 'Amber'), // Conditionally add Amber tab + ]; + + _tabPages = [ + const AccountsPage(), + metadata(ndk, context), // Pass context, assuming metadata is a function + const RelaysPage(), + const NwcPage(), + const ZapsPage(), + BlossomMediaPage(ndk: ndk), + // if (amberAvailable) const AmberPage(), // Conditionally add Amber page + ]; + + // Ensure _tabs and _tabPages have the same length if conditional tabs are complex. + // For now, assuming Amber is handled consistently or not included for simplicity of this refactor. + // If Amber was included, the TabController length and lists would need to adjust. + // Let's stick to the original 6 tabs for this refactor to match the problem description. + + _tabController = TabController(length: _tabs.length, vsync: this); + _tabController.addListener(() { + if (mounted) { + setState(() {}); + } + }); + + protocolHandler.addListener(this); + _handleInitialUri(); + } + + Future _handleInitialUri() async { + try { + final String? initialUrl = await protocolHandler.getInitialUrl(); + if (initialUrl != null && initialUrl.isNotEmpty) { + print("_MyHomePageState: Initial URL: $initialUrl"); + onProtocolUrlReceived(initialUrl); + } + } catch (e) { + print("_MyHomePageState: Error getting initial URL: $e"); + } + } + + void _processUri(Uri uri) { + if (uri.scheme == 'ndk' && uri.host == 'nwc') { + print( + "_MyHomePageState: ndk://nwc URI received, switching to NwcPage tab."); + switchToNwcTab(); + } + } + + @override + void onProtocolUrlReceived(String url) { + print("_MyHomePageState: Received protocol URL: $url"); + if (!mounted) return; + try { + final Uri receivedUri = Uri.parse(url); + _processUri(receivedUri); + } catch (e) { + print("_MyHomePageState: Error parsing received protocol URL: $e"); + } + } + + @override + void dispose() { + _tabController.dispose(); + protocolHandler.removeListener(this); + super.dispose(); + } + + void switchToNwcTab() { + // Find the index of the NWC tab using the centralized _tabs list. + int nwcPageIndex = -1; + for (int i = 0; i < _tabs.length; i++) { + // Tab.text can be null if a child widget is used instead. + // We are assuming Tab(text: 'NWC') is used. + if (_tabs[i].text == nwcTabName) { + // Use the constant + nwcPageIndex = i; + break; + } + } + + if (nwcPageIndex != -1) { + if (_tabController.index != nwcPageIndex) { + _tabController.animateTo(nwcPageIndex); + print("_MyHomePageState: Switched to NWC tab (index $nwcPageIndex)."); + } else { + print("_MyHomePageState: Already on NWC tab (index $nwcPageIndex)."); + } + } else { + print( + "_MyHomePageState: NWC tab not found by name '$nwcTabName'. Cannot switch."); + } + } + @override Widget build(BuildContext context) { - return DefaultTabController( - length: 6, - child: Scaffold( - appBar: AppBar( - title: const Text('Nostr Development Kit Demo'), - bottom: const TabBar( - tabs: [ - Tab(text: 'Accounts'), - Tab(text: 'Metadata'), - Tab(text: 'Amber'), - Tab(text: 'Relays'), - Tab(text: 'NWC'), - // Tab(text: 'Zaps'), - Tab(text: "Blossom") - ], - ), - ), - body: TabBarView( - children: [ - AccountsPage(), - metadata(ndk), - !amberAvailable - ? const Center(child: Text("Amber not available")) - : const AmberPage(), - const RelaysPage(), - const NwcPage(), - // const ZapsPage() - BlossomMediaPage(ndk: ndk), - ], + return Scaffold( + appBar: AppBar( + title: const Text('Nostr Development Kit Demo'), + bottom: TabBar( + controller: _tabController, + tabs: _tabs, // Use the centralized list ), ), + body: TabBarView( + controller: _tabController, + children: _tabPages, // Use the centralized list + ), ); } } /// how to fetch metadata info -Widget metadata(Ndk ndk) { - final Future response = ndk.metadata.loadMetadata( - '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d'); +Widget metadata(Ndk ndk, BuildContext context) { + // Added BuildContext for potential theme access + final loggedInAccount = ndk.accounts.getLoggedAccount(); + final String? pubkey = loggedInAccount?.pubkey; + + if (pubkey == null) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text('Please log in via the "Accounts" tab to view your metadata.', + textAlign: TextAlign.center), + )); + } + + // The Future is created here. If pubkey changes, a new Future will be passed to FutureBuilder. + final Future response = ndk.metadata.loadMetadata(pubkey); return FutureBuilder( future: response, builder: (context, snapshot) { - if (snapshot.hasData) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Name: ${snapshot.data?.name ?? 'not found'}'), - Text('nip05: ${snapshot.data?.nip05 ?? 'not found'}'), - Text('Picture: ${snapshot.data?.picture ?? 'not found'}'), - Text('About: ${snapshot.data?.about ?? 'not found'}'), - ], - ); + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); + return Center( + child: Text('Error fetching metadata: ${snapshot.error}')); + } else if (snapshot.hasData && snapshot.data != null) { + final metadata = snapshot.data!; + return SingleChildScrollView( + // Added for scrollability if content is long + padding: const EdgeInsets.all(16.0), + child: Center( + // Center the column content + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (metadata.picture != null && metadata.picture!.isNotEmpty) + CircleAvatar( + radius: 50, + backgroundImage: NetworkImage(metadata.picture!), + onBackgroundImageError: (exception, stackTrace) { + // This child will be shown if the image fails to load + print("Error loading avatar in metadata tab: $exception"); + // Optionally, you could set a flag here and display a placeholder Icon instead + }, + // Fallback child if backgroundImage is null or on error (though onBackgroundImageError is better for errors) + child: metadata.picture == null || metadata.picture!.isEmpty + ? const Icon(Icons.person, size: 50) + : null, + ) + else + const CircleAvatar( + // Placeholder if no picture URL + radius: 50, + child: Icon(Icons.person, size: 50), + ), + const SizedBox(height: 16), + Text('Name: ${metadata.name ?? 'N/A'}', + style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 8), + Text('Display Name: ${metadata.displayName ?? 'N/A'}'), + Text('NIP-05: ${metadata.nip05 ?? 'N/A'}'), + const SizedBox(height: 8), + Text('About:', style: Theme.of(context).textTheme.titleMedium), + Text(metadata.about ?? 'N/A', textAlign: TextAlign.center), + const SizedBox(height: 8), + Text('Website: ${metadata.website ?? 'N/A'}'), + Text('Lud06: ${metadata.lud06 ?? 'N/A'}'), + Text('Lud16: ${metadata.lud16 ?? 'N/A'}'), + // Add more fields as desired + ], + ), + ), + ); } else { - return const Center(child: CircularProgressIndicator()); + // No data, but not an error and not waiting -> metadata not found for pubkey + return Center( + child: Text( + 'Metadata not found for this account. You might need to set it in a Nostr client.')); } }, ); diff --git a/packages/sample-app/lib/nwc_page.dart b/packages/sample-app/lib/nwc_page.dart index 673b95d78..c79835d89 100644 --- a/packages/sample-app/lib/nwc_page.dart +++ b/packages/sample-app/lib/nwc_page.dart @@ -1,7 +1,20 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:convert/convert.dart' as convert; +import 'package:crypto/crypto.dart' as crypto; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:ndk/domain_layer/usecases/nwc/consts/nwc_method.dart'; +import 'package:ndk/domain_layer/usecases/nwc/nwc_notification.dart'; +import 'package:ndk/domain_layer/usecases/nwc/responses/nwc_response.dart'; import 'package:ndk/ndk.dart'; +import 'package:ndk/shared/nips/nip01/bip340.dart'; +import 'package:ndk/shared/nips/nip01/key_pair.dart'; +import 'package:protocol_handler/protocol_handler.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'main.dart'; @@ -12,36 +25,524 @@ class NwcPage extends StatefulWidget { State createState() => _NwcPageState(); } -class _NwcPageState extends State { +class _NwcPageState extends State with ProtocolListener { TextEditingController uri = TextEditingController(); - TextEditingController amount = TextEditingController(); - TextEditingController invoice = TextEditingController(); + TextEditingController amount = + TextEditingController(); // For normal and hold invoice + // TextEditingController holdAmount = TextEditingController(); // Removed + TextEditingController description = + TextEditingController(); // Used if isHoldInvoice is true + TextEditingController invoice = + TextEditingController(); // For paying invoices NwcConnection? connection; + KeyPair? + nwcAppKey; // Our app's NWC keypair, should be generated once and reused. GetBalanceResponse? balance; - MakeInvoiceResponse? makeInvoice; + + // State variables to hold context from the NIP-47 auth initiation + String? + _pendingDiscoveryRelayUrl; // The relay specified in the nostr+walletauth URI's 'relay=' param, + // where we expect the kind 13194 event to be. + String? + _pendingAppPubkeyForAuth; // Our app's pubkey that was sent in the nostr+walletauth URI's 'pubkey=' param + // and expected in the 'p' tag of the kind 13194 event. + MakeInvoiceResponse? + makeInvoice; // Will store result of normal or hold invoice creation PayInvoiceResponse? payInvoice; + // MakeInvoiceResponse? makeHoldInvoiceResponse; // Removed, merged into makeInvoice + String? holdInvoicePreimage; + String? + holdInvoicePaymentHash; // Still needed to identify the hold invoice for notifications/settle/cancel + bool isHoldInvoice = false; // For the checkbox + bool _currentInvoiceWasHold = + false; // To track if the current 'makeInvoice' is a hold type + bool isHoldInvoiceAccepted = false; + String? holdInvoiceStatusMessage; + StreamSubscription? holdInvoiceStateSubscription; + NwcResponse? settleHoldInvoiceResponse; + NwcResponse? cancelHoldInvoiceResponse; + + // For regular invoice payment notifications + bool isRegularInvoicePaid = false; + String? regularInvoiceStatusMessage; + StreamSubscription? regularInvoicePaymentSubscription; + @override void initState() { super.initState(); + protocolHandler.addListener(this); uri.addListener(() { setState(() { if (uri.text == '') { connection = null; + _resetInvoiceStates(); // Reset invoice states if connection URI is cleared } }); }); amount.addListener(() { setState(() {}); }); + // holdAmount.addListener(() { // Removed + // setState(() {}); + // }); + description.addListener(() { + setState(() {}); + }); invoice.addListener(() { setState(() {}); }); } + @override + void dispose() { + uri.dispose(); + amount.dispose(); + // holdAmount.dispose(); // Removed + description.dispose(); + invoice.dispose(); + holdInvoiceStateSubscription?.cancel(); + regularInvoicePaymentSubscription?.cancel(); + protocolHandler.removeListener(this); + super.dispose(); + } + + void _resetInvoiceStates() { + setState(() { + makeInvoice = null; + // makeHoldInvoiceResponse = null; // Removed + holdInvoicePreimage = null; + holdInvoicePaymentHash = null; + isHoldInvoiceAccepted = false; + _currentInvoiceWasHold = false; + holdInvoiceStatusMessage = null; + holdInvoiceStateSubscription?.cancel(); + holdInvoiceStateSubscription = null; + settleHoldInvoiceResponse = null; + cancelHoldInvoiceResponse = null; + + isRegularInvoicePaid = false; + regularInvoiceStatusMessage = null; + regularInvoicePaymentSubscription?.cancel(); + regularInvoicePaymentSubscription = null; + // isHoldInvoice = false; // Optionally reset checkbox, or leave it as user set + }); + } + + @override + void onProtocolUrlReceived(String url) async { + print('NWC Page: Protocol URL received: $url'); + + if (url.startsWith("ndk://nwc") && // Check if it's our NIP-47 callback + _pendingDiscoveryRelayUrl != null && + _pendingAppPubkeyForAuth != null) { + print('NIP-47 callback received. Processing...'); + print(' Expected discovery relay: $_pendingDiscoveryRelayUrl'); + print(' Expected app pubkey for p tag: $_pendingAppPubkeyForAuth'); + + final discoveryRelay = _pendingDiscoveryRelayUrl!; + final appPubkey = _pendingAppPubkeyForAuth!; + + // Clear pending state now that we are processing it. + // Important to do this early to prevent reprocessing if another callback comes for an old request. + setState(() { + _pendingDiscoveryRelayUrl = null; + _pendingAppPubkeyForAuth = null; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'NIP-47 callback received. Fetching wallet connection info from $discoveryRelay...')), + ); + + try { + Nip01Event? foundWalletAuthEvent; + const int nip47InfoEventKind = 13194; + + // Retrieve the discoveryRelay and appPubkey that were set before launching nostr+walletauth + // These are final and won't change during this try block. + + final String discoveryRelayForQuery = "wss://relay.getalby.com/v1"; + final String appPubkeyForTag = nwcAppKey!.publicKey; + + // Clear pending state now that we are processing it. + // Important to do this early to prevent reprocessing if another callback comes for an old request. + // setState(() { + // _pendingDiscoveryRelayUrl = null; + // _pendingAppPubkeyForAuth = null; + // }); + + if (nwcAppKey == null || nwcAppKey!.privateKey == null) { + print( + 'NIP-47 Error: nwcAppKey or its private key is null. Cannot construct NWC URI.'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error: App NWC key not fully initialized.')), + ); + return; + } + + final stream = ndk.requests + .query( + filters: [ + Filter( + kinds: [nip47InfoEventKind], + tags: { + '#p': [appPubkeyForTag] // Tagged with our app's pubkey + }, + limit: 5, + ) + ], + explicitRelays: { + discoveryRelayForQuery + }, // Use explicitRelays (user feedback) + ) + .stream + .timeout(const Duration(seconds: 15)); + + List potentialEvents = []; + await for (final event in stream) { + potentialEvents.add(event); + } + + if (potentialEvents.isEmpty) { + print( + 'No NWC Info Event (kind $nip47InfoEventKind) found on $discoveryRelayForQuery tagged for $appPubkeyForTag.'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'No NWC Info Event found on $discoveryRelayForQuery.')), + ); + return; + } + + potentialEvents.sort((a, b) => b.createdAt.compareTo(a.createdAt)); + + for (final event in potentialEvents) { + print( + 'Processing potential NWC Info Event ID: ${event.id}, Author: ${event.pubKey}'); + // Use Bip340EventVerifier().verify(event) (user feedback) + final bool isValidSignature = + await Bip340EventVerifier().verify(event); + if (isValidSignature) { + print( + 'Event ID ${event.id} has a VALID signature from ${event.pubKey}. This is the Wallet NWC Service Pubkey.'); + foundWalletAuthEvent = event; + break; + } else { + print('Event ID ${event.id} has an INVALID signature. Skipping.'); + } + } + + if (foundWalletAuthEvent == null) { + print('No NWC Info Event with a valid signature found.'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Could not validate NWC Info Event.')), + ); + return; + } + + print( + 'Successfully fetched and validated NWC Info Event (Kind $nip47InfoEventKind):'); + print( + ' Author (Wallet NWC Service Pubkey): ${foundWalletAuthEvent.pubKey}'); + // Content of kind:13194 is ignored as per user feedback. + + // Construct the NWC URI as per user's explicit instructions: + // walletPubKey is the author of the 13194 event + // relay is the relay passed to nostr+walletauth when connect button is pressed (_pendingDiscoveryRelayUrl) + // secret is the nwcAppKey the private part + + final String walletNwcServicePubkey = foundWalletAuthEvent.pubKey; + final String nwcRelayForConnectionUri = + discoveryRelayForQuery; // This was _pendingDiscoveryRelayUrl + final String appNwcPrivateKeyForSecret = nwcAppKey!.privateKey!; + + final constructedNwcUri = + 'nostr+walletconnect://$walletNwcServicePubkey?relay=${Uri.encodeComponent(nwcRelayForConnectionUri)}&secret=$appNwcPrivateKeyForSecret'; + + print( + 'Constructed NWC connection URI (as per explicit instructions): $constructedNwcUri'); + + setState(() { + uri.text = constructedNwcUri; + }); + + // Now connect using the constructed URI + // As per user feedback, clientKeyPair is not an argument to connect. + // The nwcAppKey (specifically its private key) was used as the 'secret' in the constructedNwcUri. + final NwcConnection? establishedConn = await ndk.nwc.connect( + constructedNwcUri, + // clientKeyPair: nwcAppKey, // Removed as per user feedback + doGetInfoMethod: true, + ); + + if (establishedConn != null) { + setState(() { + connection = establishedConn; + balance = null; + _resetInvoiceStates(); + // If make_hold_invoice is not permitted, ensure isHoldInvoice is false. + if (!(connection!.info?.methods + .contains(NwcMethod.MAKE_HOLD_INVOICE.name) ?? + false)) { + isHoldInvoice = false; + } + }); + print( + 'Successfully connected to NWC wallet: $walletNwcServicePubkey via $nwcRelayForConnectionUri'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text( + // Try using 'alias' as per potential GetInfoResponse field, fallback to pubkey + 'NWC Connected to: ${connection?.info?.alias ?? walletNwcServicePubkey.substring(0, 10)}...')), + ); + } else { + print('Failed to connect to NWC wallet using the constructed URI.'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to establish NWC connection.')), + ); + } + } catch (e) { + print('Error during NIP-47 callback processing: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error processing NWC callback: $e')), + ); + } + } else { + print( + 'Received unhandled protocol URL or missing pending NIP-47 auth state: $url'); + } + } + @override Widget build(BuildContext context) { List widgets = []; + // widgets.add(Expanded( + // child: Column( + // mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // Expanded( + // child: Container( + // width: double.infinity, + // padding: const EdgeInsets.symmetric(horizontal: 16), + // child: Column( + // mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // Container( + // width: double.infinity, + // // height: 112, + // padding: const EdgeInsets.only(bottom: 24), + // child: const Column( + // mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // SizedBox(height: 20), + // SizedBox( + // width: double.infinity, + // child: Text( + // 'Connect Wallet', + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Colors.white, + // fontSize: 24, + // fontFamily: 'Geist', + // fontWeight: FontWeight.w700, + // height: 0.06, + // ), + // ), + // ), + // SizedBox(height: 20), + // SizedBox( + // // width: double.infinity, + // child: Text( + // 'Connect your bitcoin lightning wallet with NWC for better zapping experience.', + // textAlign: TextAlign.center, + // style: TextStyle( + // color: Color(0xFF7A7D81), + // fontSize: 16, + // fontFamily: 'Geist', + // fontWeight: FontWeight.w400, + // // height: 0.09, + // ), + // ), + // ), + // ], + // ), + // ), + // SizedBox( + // width: 80, + // height: 80, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // SizedBox( + // width: 80, + // height: 80, + // child: Column( + // mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Image.asset("assets/imgs/albygo.png"), + // ], + // ), + // ), + // ], + // ), + // ), + // Row( + // mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // FilledButton( + // child: Text("Alby Go"), + // onPressed: () async { + // if (Platform.isAndroid) { + // Uri uri = Uri.parse("nostrnwc://bla?appname=Yana\&appicon=https%3A%2F%2Fyana.do%2Fimages%2Flogo-new.png\&callback=ndk%3A%2F%2Fnwc"); + // await launchUrl(uri); + // // AndroidIntent intent = AndroidIntent( + // // action: 'action_view', + // // data: "nostrnwc://bla?appname=Yana\&appicon=https%3A%2F%2Fyana.do%2Fimages%2Flogo-new.png\&callback=ndk%3A%2F%2Fnwc", + // // ); + // // await intent.launch(); + // } + // }, + // ), + // ], + // ), + // SizedBox(height: 20), + // SizedBox( + // width: 80, + // height: 80, + // child: Row( + // mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // SizedBox( + // width: 80, + // height: 80, + // child: Column( + // mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Image.asset("assets/imgs/nwc.png"), + // ], + // ), + // ), + // ], + // ), + // ), + // Row( + // mainAxisSize: MainAxisSize.min, + // mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // FilledButton( + // child: Text("NWC manual"), + // onPressed: () { + // // TODO + // }, + // ), + // ], + // ), + // ], + // ), + // ), + // ), + // ], + // ), + // )); + // + if (Platform.isAndroid) { + widgets.add( + FilledButton( + onPressed: () async { + // Always generate a new nwcAppKey for NIP-47 auth when this button is pressed. + nwcAppKey = Bip340.generatePrivateKey(); + print( + "Generated new, fresh nwcAppKey for NIP-47 auth: ${nwcAppKey!.publicKey}"); + // No need to call setState here as nwcAppKey is used immediately for URI construction. + + // This is the URI the button currently attempts to launch. + // As per NIP-47, the host of nostr+walletauth should be our app's pubkey. + // The 'relay' param is where the auth service (e.g., Alby) is expected to publish the kind 13194 event. + // The 'pubkey' param in the URI (if NIP-47 spec evolves to include it this way, or if it's part of 'name' or other metadata) + // would be our app's pubkey. The current URI structure in the code is: + // "nostr+walletauth://${nwcAppKey!.publicKey}?relay=...&name=Yana&...&return_to=ndk%3A%2F%2Fnwc" + + // Let's parse the URI string that the button *intends* to launch + // to extract the necessary _pending values. + // The existing code for the URI is: + String appName = "Yana"; // Example from existing code + String appIcon = + "https%3A%2F%2Fyana.do%2Fimages%2Flogo-new.png"; // Example + String methods = + "get_info get_balance get_budget make_invoice pay_invoice lookup_invoice list_transactions sign_message make_hold_invoice cancel_hold_invoice settle_hold_invoice"; // Example + String discoveryRelay = + "wss://relay.getalby.com/v1"; // Example from existing code + String returnTo = "ndk://nwc"; // Example + + // Construct the URI that will be launched. + // The host is our app's pubkey. + final Uri launchUri = Uri( + scheme: 'nostr+walletauth', + host: nwcAppKey!.publicKey, // Our app's pubkey + queryParameters: { + 'relay': + discoveryRelay, // Relay for discovering the kind 13194 event + 'name': appName, + 'request_methods': methods, + 'icon': appIcon, + 'return_to': returnTo, + // NIP-47 also suggests a 'pubkey' param for the app's pubkey, but host is also used. + // Let's ensure our _pendingAppPubkeyForAuth is nwcAppKey!.publicKey + }); + + // Store the context needed for when onProtocolUrlReceived is called. + _pendingDiscoveryRelayUrl = discoveryRelay; + _pendingAppPubkeyForAuth = nwcAppKey! + .publicKey; // This is the pubkey our app uses for this auth flow. + + print( + "Attempting to launch NIP-47 Auth URI: ${launchUri.toString()}"); + print( + " _pendingDiscoveryRelayUrl set to: $_pendingDiscoveryRelayUrl"); + print( + " _pendingAppPubkeyForAuth set to: $_pendingAppPubkeyForAuth"); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Redirecting to wallet for NWC authorization...')), + ); + try { + await launchUrl(launchUri, mode: LaunchMode.externalApplication); + } catch (e) { + print("Error launching NIP-47 Auth URI: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Could not launch wallet app: $e')), + ); + // Clear pending state if launch fails + _pendingDiscoveryRelayUrl = null; + _pendingAppPubkeyForAuth = null; + } + }, + child: const Text('Connect with Alby Go'), // Updated button text + ), + ); + } widgets.add(Container( padding: const EdgeInsets.all(20), width: 400, @@ -80,10 +581,18 @@ class _NwcPageState extends State { FilledButton( onPressed: uri.text.isNotEmpty ? () async { - connection = + _resetInvoiceStates(); // Reset states before new connection + NwcConnection? newConnection = await ndk.nwc.connect(uri.text, doGetInfoMethod: true); setState(() { + connection = newConnection; balance = null; + // If make_hold_invoice is not permitted, ensure isHoldInvoice is false. + if (!(connection?.info?.methods + .contains(NwcMethod.MAKE_HOLD_INVOICE.name) ?? + false)) { + isHoldInvoice = false; + } }); } : null, @@ -94,6 +603,9 @@ class _NwcPageState extends State { ? Text("Methods ${connection!.info!.methods}") : Container()); + widgets.add(const SizedBox( + height: 20, + )); widgets.add( FilledButton( onPressed: connection != null @@ -111,86 +623,414 @@ class _NwcPageState extends State { ? Text("Balance ${balance!.balanceSats} sats") : Container()); - bool canMakeInvoice = connection != null && - connection!.info!.methods.contains(NwcMethod.MAKE_INVOICE.name) && - amount.text != '' && - (int.tryParse(amount.text) ?? 0) > 0; - widgets.add(Row( - children: [ - const SizedBox(width: 30), - SizedBox( - width: 200, - child: TextField( - controller: amount, - decoration: const InputDecoration( - hintText: "amount in sats", - hintStyle: TextStyle(color: Colors.grey), + widgets.add( + const Divider(height: 40, thickness: 1, indent: 20, endIndent: 20)); + + // Determine if the "Make Invoice" button should be enabled + bool canSubmitMakeInvoice = connection != null && + amount.text.isNotEmpty && + (int.tryParse(amount.text) ?? 0) > 0 && + (isHoldInvoice + ? connection!.info!.methods + .contains(NwcMethod.MAKE_HOLD_INVOICE.name) + : connection!.info!.methods.contains(NwcMethod.MAKE_INVOICE.name)); + + // --- Make Invoice Section --- + widgets.add(const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text("Make Invoice", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + )); + widgets.add( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + SizedBox( + width: 200, + child: TextField( + controller: amount, + textAlign: TextAlign.center, // Center text within field + decoration: const InputDecoration( + hintText: "Amount in sats", + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14), + keyboardType: TextInputType.number, + ), ), - style: const TextStyle(fontSize: 14), - ), + const SizedBox(height: 8), + SizedBox( + width: 250, // Smaller width for description field + child: TextField( + controller: description, + textAlign: TextAlign.center, // Center text + decoration: InputDecoration( + hintText: isHoldInvoice + ? "Description (for hold)" + : "Description (optional)", + hintStyle: const TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14), + ), + ), + const SizedBox(height: 8), + SizedBox( + width: 200, // To help center the checkbox + child: CheckboxListTile( + title: const Text("Hold Invoice"), + value: isHoldInvoice, + // Disable checkbox if connection is null, info is null, or method is not permitted + onChanged: (connection != null && + connection!.info != null && + connection!.info!.methods + .contains(NwcMethod.MAKE_HOLD_INVOICE.name)) + ? (bool? value) { + setState(() { + isHoldInvoice = value ?? false; + // Resetting all invoice states might be too aggressive here, + // consider if only relevant parts should be reset or if user expects this. + // For now, keeping existing _resetInvoiceStates() call. + _resetInvoiceStates(); + }); + } + : null, // Setting onChanged to null disables the checkbox + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + ), + const SizedBox(height: 10), + Center( + // Keep the button centered + child: FilledButton( + onPressed: canSubmitMakeInvoice + ? () async { + _resetInvoiceStates(); // Clear previous invoice details first + final int sats = int.tryParse(amount.text) ?? 0; + if (isHoldInvoice) { + setState(() { + _currentInvoiceWasHold = true; + holdInvoiceStatusMessage = + "Generating preimage and payment hash..."; + }); + + final random = Random.secure(); + final preimageBytes = Uint8List.fromList( + List.generate( + 32, (_) => random.nextInt(256))); + final preimageHex = convert.hex.encode(preimageBytes); + + final paymentHashBytes = + crypto.sha256.convert(preimageBytes).bytes; + final paymentHashHex = + convert.hex.encode(paymentHashBytes); + + setState(() { + holdInvoicePreimage = preimageHex; + holdInvoicePaymentHash = paymentHashHex; + holdInvoiceStatusMessage = + "Creating hold invoice..."; + }); + + try { + final response = await ndk.nwc.makeHoldInvoice( + connection!, + amountSats: sats, + description: description.text.isNotEmpty + ? description.text + : null, // Pass null if empty + paymentHash: paymentHashHex); + setState(() { + makeInvoice = + response; // Store in the unified makeInvoice + if (response.errorCode == null) { + holdInvoiceStatusMessage = + "Hold invoice created. Waiting for acceptance..."; + _listenForHoldInvoiceAcceptance(paymentHashHex); + } else { + holdInvoiceStatusMessage = + "Error creating hold invoice: ${response.errorMessage}"; + } + }); + } catch (e) { + setState(() { + holdInvoiceStatusMessage = + "Exception creating hold invoice: $e"; + }); + } + } else { + // Regular invoice + setState(() { + _currentInvoiceWasHold = false; + }); + try { + final response = await ndk.nwc.makeInvoice( + connection!, + amountSats: sats, + description: description.text.isNotEmpty + ? description.text + : null, // Pass description for both types if not empty + ); + setState(() { + makeInvoice = response; + if (response.errorCode == null && + response.paymentHash != null) { + regularInvoiceStatusMessage = + "Regular invoice created. Waiting for payment..."; + _listenForRegularInvoicePayment( + response.paymentHash!); + } else if (response.errorCode != null) { + regularInvoiceStatusMessage = + "Error creating regular invoice: ${response.errorMessage}"; + } + }); + } catch (e) { + setState(() { + regularInvoiceStatusMessage = + "Exception creating regular invoice: $e"; + print("Error making regular invoice: $e"); + }); + } + } + } + : null, + child: + Text(isHoldInvoice ? 'Make Hold Invoice' : 'Make Invoice'), + ), + ), + ], ), - FilledButton( - onPressed: canMakeInvoice - ? () async { - final invoice = await ndk.nwc.makeInvoice(connection!, - amountSats: int.tryParse(amount.text) ?? 0); - setState(() { - makeInvoice = invoice; - }); + ), + ); + + // Display for the created invoice (normal or hold) + if (makeInvoice != null && makeInvoice!.errorCode == null) { + widgets.add(SelectableText("Invoice: ${makeInvoice!.invoice}")); + if (_currentInvoiceWasHold) { + widgets + .add(SelectableText("Payment Hash: ${makeInvoice!.paymentHash}")); + } + if (makeInvoice!.invoice.isNotEmpty) { + widgets.add( + Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onTap: () async { + final Uri launchUri = + Uri.parse('lightning:${makeInvoice!.invoice}'); + if (await canLaunchUrl(launchUri)) { + await launchUrl(launchUri); + } else { + // Optionally, show a message if no app can handle the URI + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Could not launch Lightning invoice. No app found to handle it.')), + ); + print('Could not launch $launchUri'); } - : null, - child: const Text('Make invoice'), - ), - ], + }, + child: QrImageView( + data: makeInvoice!.invoice.toUpperCase(), + version: QrVersions.auto, + size: 200.0, + backgroundColor: Colors.white, + ), + ), + ), + ); + } + } else if (makeInvoice != null && makeInvoice!.errorCode != null) { + // If there was an error creating the invoice (and it's not a hold-specific status message already handled) + if (!_currentInvoiceWasHold) { + // Only show generic error if not a hold invoice with its own status + widgets.add(Text("Error creating invoice: ${makeInvoice!.errorMessage}", + style: const TextStyle(color: Colors.red))); + } + } + + // Display for regular invoice payment status + if (!_currentInvoiceWasHold && + makeInvoice != null && + makeInvoice!.errorCode == null) { + if (regularInvoiceStatusMessage != null) { + widgets.add(Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text(regularInvoiceStatusMessage!, + style: isRegularInvoicePaid + ? const TextStyle( + color: Colors.green, fontWeight: FontWeight.bold) + : null), + )); + } + } + + // Hold invoice specific status messages and buttons (Settle/Cancel) + // are now part of the "Make Invoice" section's output, below the QR code. + if (_currentInvoiceWasHold) { + if (holdInvoiceStatusMessage != null) { + widgets.add(Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text(holdInvoiceStatusMessage!), + )); + } + + if (isHoldInvoiceAccepted) { + widgets.add(const Text("Hold invoice accepted!", + style: + TextStyle(color: Colors.green, fontWeight: FontWeight.bold))); + widgets.add(Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FilledButton( + onPressed: (connection != null && + holdInvoicePreimage != null && + !isSettlingOrCancelling()) + ? () async { + setState(() { + holdInvoiceStatusMessage = "Settling hold invoice..."; + settleHoldInvoiceResponse = null; + cancelHoldInvoiceResponse = null; + }); + try { + final response = await ndk.nwc.settleHoldInvoice( + connection!, + preimage: holdInvoicePreimage!); + setState(() { + settleHoldInvoiceResponse = response; + if (response.errorCode == null) { + holdInvoiceStatusMessage = + "Hold invoice settled successfully. Preimage: $holdInvoicePreimage"; + } else { + holdInvoiceStatusMessage = + "Error settling invoice: ${response.errorMessage}"; + } + }); + } catch (e) { + setState(() { + holdInvoiceStatusMessage = + "Exception settling invoice: $e"; + }); + } + } + : null, + child: const Text("Settle Invoice"), + ), + const SizedBox(width: 20), + FilledButton( + onPressed: (connection != null && + holdInvoicePaymentHash != null && + !isSettlingOrCancelling()) + ? () async { + setState(() { + holdInvoiceStatusMessage = "Cancelling hold invoice..."; + settleHoldInvoiceResponse = null; + cancelHoldInvoiceResponse = null; + }); + try { + final response = await ndk.nwc.cancelHoldInvoice( + connection!, + paymentHash: holdInvoicePaymentHash!); + setState(() { + cancelHoldInvoiceResponse = response; + if (response.errorCode == null) { + holdInvoiceStatusMessage = + "Hold invoice cancelled successfully."; + } else { + holdInvoiceStatusMessage = + "Error cancelling invoice: ${response.errorMessage}"; + } + }); + } catch (e) { + setState(() { + holdInvoiceStatusMessage = + "Exception cancelling invoice: $e"; + }); + } + } + : null, + child: const Text("Cancel Invoice"), + ), + ], + )); + } + + if (settleHoldInvoiceResponse != null) { + widgets.add(Text(settleHoldInvoiceResponse!.errorCode == null + ? "Settle successful! Preimage: $holdInvoicePreimage" + : "Settle failed: ${settleHoldInvoiceResponse!.errorMessage}")); + } + if (cancelHoldInvoiceResponse != null) { + widgets.add(Text(cancelHoldInvoiceResponse!.errorCode == null + ? "Cancel successful!" + : "Cancel failed: ${cancelHoldInvoiceResponse!.errorMessage}")); + } + } + + widgets.add( + const Divider(height: 40, thickness: 1, indent: 20, endIndent: 20)); + + // --- Pay Invoice Section --- + widgets.add(const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), // Reduced top padding + child: Text("Pay Invoice", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), )); - widgets.add(makeInvoice != null - ? SelectableText("bolt11 invoice: ${makeInvoice!.invoice}") - : Container()); bool canPayInvoice = connection != null && connection!.info!.methods.contains(NwcMethod.PAY_INVOICE.name) && invoice.text != ''; - widgets.add(Row( - children: [ - const SizedBox(width: 30), - SizedBox( - width: 200, - child: TextField( - controller: invoice, - decoration: const InputDecoration( - hintText: "invoice to pay", - hintStyle: TextStyle(color: Colors.grey), + + widgets.add(Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + SizedBox( + width: 300, // Centered invoice input field + child: TextField( + controller: invoice, + textAlign: TextAlign.center, + decoration: const InputDecoration( + hintText: "Invoice to pay (bolt11)", + hintStyle: TextStyle(color: Colors.grey), + ), + style: const TextStyle(fontSize: 14), ), - style: const TextStyle(fontSize: 14), ), - ), - FilledButton( - onPressed: canPayInvoice - ? () async { - final p = await ndk.nwc - .payInvoice(connection!, invoice: invoice.text); - setState(() { - payInvoice = p; - }); - } - : null, - child: const Text('Pay invoice'), - ), - ], + const SizedBox(height: 10), + FilledButton( + onPressed: canPayInvoice + ? () async { + final p = await ndk.nwc + .payInvoice(connection!, invoice: invoice.text); + setState(() { + payInvoice = p; + }); + } + : null, + child: const Text('Pay Invoice'), + ), + ], + ), )); widgets.add(payInvoice != null - ? SelectableText("preimage: ${payInvoice!.preimage}") + ? SelectableText("Payment Preimage: ${payInvoice!.preimage}") : Container()); + widgets.add(const Divider(height: 40, thickness: 2)); widgets.add( FilledButton( onPressed: connection != null ? () async { await ndk.nwc.disconnect(connection!); + _resetInvoiceStates(); // Use the new reset function setState(() { connection = null; balance = null; - makeInvoice = null; + payInvoice = null; + nwcAppKey = null; // Reset the app's NWC key + uri.clear(); // Clear the URI input field + // Other NWC specific states are reset by _resetInvoiceStates }); } : null, @@ -206,4 +1046,138 @@ class _NwcPageState extends State { ), ); } + + bool isSettlingOrCancelling() { + return (holdInvoiceStatusMessage != null && + (holdInvoiceStatusMessage!.contains("Settling") || + holdInvoiceStatusMessage!.contains("Cancelling"))) || + settleHoldInvoiceResponse != null || + cancelHoldInvoiceResponse != null; + } + + void _listenForHoldInvoiceAcceptance(String expectedPaymentHash) { + holdInvoiceStateSubscription?.cancel(); + if (connection == null) { + setState(() { + holdInvoiceStatusMessage = + "Connection is null, cannot listen for acceptance."; + }); + return; + } + final stream = connection!.holdInvoiceStateStream; + if (stream == null) { + if (mounted) { + setState(() { + holdInvoiceStatusMessage = "Hold invoice state stream is null."; + }); + } + return; + } + + // Use makeInvoice.expiresAt as it now holds the response for both normal and hold invoices + final duration = makeInvoice?.expiresAt != null + ? (makeInvoice!.expiresAt! - + DateTime.now().millisecondsSinceEpoch ~/ 1000) + : 300; // Default timeout if expiresAt is not available + + holdInvoiceStateSubscription = stream + .timeout( + Duration(seconds: duration.toInt() > 0 ? duration.toInt() : 300)) + .listen((notification) { + if (notification.notificationType == + NwcNotification.kHoldInvoiceAccepted && + notification.paymentHash == expectedPaymentHash) { + setState(() { + isHoldInvoiceAccepted = true; + holdInvoiceStatusMessage = "Hold invoice accepted by wallet!"; + }); + holdInvoiceStateSubscription?.cancel(); + } + }, onError: (error) { + if (mounted) { + setState(() { + if (error is TimeoutException) { + holdInvoiceStatusMessage = + "Timed out waiting for hold invoice acceptance."; + } else { + holdInvoiceStatusMessage = + "Error listening for hold invoice acceptance: $error"; + } + }); + } + }, onDone: () { + if (mounted && + !isHoldInvoiceAccepted && + settleHoldInvoiceResponse == null && + cancelHoldInvoiceResponse == null) { + // setState(() { + // holdInvoiceStatusMessage = "Notification stream closed without acceptance."; + // }); + } + }); + } + + void _listenForRegularInvoicePayment(String expectedPaymentHash) { + regularInvoicePaymentSubscription?.cancel(); + if (connection == null) { + if (mounted) { + setState(() { + regularInvoiceStatusMessage = + "Connection is null, cannot listen for payment."; + }); + } + return; + } + // Use the general notificationsStream and filter for payment notifications + final stream = connection!.paymentsReceivedStream; + if (stream == null) { + if (mounted) { + setState(() { + regularInvoiceStatusMessage = "Payment notification stream is null."; + }); + } + return; + } + + final duration = makeInvoice?.expiresAt != null + ? (makeInvoice!.expiresAt! - + DateTime.now().millisecondsSinceEpoch ~/ 1000) + : 300; // Default timeout + + regularInvoicePaymentSubscription = stream + .timeout( + Duration(seconds: duration.toInt() > 0 ? duration.toInt() : 300)) + .listen((notification) { + if (notification.notificationType == NwcNotification.kPaymentReceived && + notification.paymentHash == expectedPaymentHash) { + if (mounted) { + setState(() { + isRegularInvoicePaid = true; + regularInvoiceStatusMessage = + "Invoice PAID! Preimage: ${notification.preimage}"; + }); + } + regularInvoicePaymentSubscription?.cancel(); + } + // We might also want to listen for kPaymentFailed if the NWC provider sends such for incoming payments that fail. + // Or, more commonly, the invoice just expires. + }, onError: (error) { + if (mounted) { + setState(() { + if (error is TimeoutException) { + regularInvoiceStatusMessage = + "Timed out waiting for invoice payment."; + } else { + regularInvoiceStatusMessage = + "Error listening for invoice payment: $error"; + } + }); + } + }, onDone: () { + if (mounted && !isRegularInvoicePaid) { + // If stream closes and invoice not paid, could update status. + // For now, timeout handles expiration. + } + }); + } } diff --git a/packages/sample-app/lib/relays_page.dart b/packages/sample-app/lib/relays_page.dart index 344dec277..91629b0c7 100644 --- a/packages/sample-app/lib/relays_page.dart +++ b/packages/sample-app/lib/relays_page.dart @@ -14,7 +14,7 @@ class RelaysPage extends StatefulWidget { } class _RelaysPageState extends State { - final amber = Amberflutter(); + // final amber = Amberflutter(); // ignore: unused_field String _text = ''; UserRelayList? relays; @@ -32,21 +32,10 @@ class _RelaysPageState extends State { ), FilledButton( onPressed: () async { - amber.getPublicKey( - permissions: [ - const Permission( - type: "nip04_encrypt", - ), - const Permission( - type: "nip04_decrypt", - ), - ], - ).then((value) async { - UserRelayList? list = await ndk.userRelayLists - .getSingleUserRelayList(Nip19.decode(value['signature'])); - setState(() { - relays = list; - }); + UserRelayList? list = await ndk.userRelayLists + .getSingleUserRelayList(ndk.accounts.getPublicKey()!); + setState(() { + relays = list; }); }, child: const Text("Fetch relay list")) diff --git a/packages/sample-app/lib/section.dart b/packages/sample-app/lib/section.dart new file mode 100644 index 000000000..a3b664762 --- /dev/null +++ b/packages/sample-app/lib/section.dart @@ -0,0 +1,44 @@ +import 'package:zaplab_design/zaplab_design.dart'; + +class Section extends StatelessWidget { + const Section({ + super.key, + required this.title, + required this.description, + this.children = const [], + }); + + final String title; + final String description; + final List children; + + @override + Widget build(BuildContext context) { + final theme = AppTheme.of(context); + + return Column( + children: [ + AppContainer( + width: double.infinity, + decoration: BoxDecoration( + borderRadius: theme.radius.asBorderRadius().rad16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppText.h2(title), + const AppGap(AppGapSize.s4), + AppText.reg14( + description, + color: theme.colors.white66, + ), + ], + ), + ), + const AppGap(AppGapSize.s16), + ...children, + const AppGap(AppGapSize.s24), + ], + ); + } +} diff --git a/packages/sample-app/linux/flutter/generated_plugin_registrant.cc b/packages/sample-app/linux/flutter/generated_plugin_registrant.cc index e4b9cf875..260a5121e 100644 --- a/packages/sample-app/linux/flutter/generated_plugin_registrant.cc +++ b/packages/sample-app/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,9 @@ #include #include +#include +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = @@ -16,4 +19,13 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) media_kit_video_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); + screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); } diff --git a/packages/sample-app/linux/flutter/generated_plugins.cmake b/packages/sample-app/linux/flutter/generated_plugins.cmake index 93d086447..7370544a8 100644 --- a/packages/sample-app/linux/flutter/generated_plugins.cmake +++ b/packages/sample-app/linux/flutter/generated_plugins.cmake @@ -5,6 +5,9 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_linux media_kit_video + screen_retriever_linux + url_launcher_linux + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/sample-app/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/sample-app/macos/Flutter/GeneratedPluginRegistrant.swift index 44e95055d..39a84db3a 100644 --- a/packages/sample-app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/sample-app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,14 +9,22 @@ import media_kit_libs_macos_video import media_kit_video import package_info_plus import path_provider_foundation +import protocol_handler_macos import screen_brightness_macos +import screen_retriever_macos +import url_launcher_macos import wakelock_plus +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ProtocolHandlerMacosPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerMacosPlugin")) ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/packages/sample-app/pubspec.lock b/packages/sample-app/pubspec.lock index ce741e563..df6322edc 100644 --- a/packages/sample-app/pubspec.lock +++ b/packages/sample-app/pubspec.lock @@ -13,26 +13,34 @@ packages: dependency: transitive description: name: archive - sha256: "6199c74e3db4fbfbd04f66d739e72fe11c8a8957d5f219f1f4482dbde6420b5a" + sha256: "0c64e928dcbefddecd234205422bcfc2b5e6d31be0b86fef0d0dd48d7b4c9742" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" + ascii_qr: + dependency: transitive + description: + name: ascii_qr + sha256: "2046e400a0fa4ea0de5df44c87b992cdd1f76403bb15e64513b89263598750ae" + url: "https://pub.dev" + source: hosted + version: "1.0.1" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" bech32: dependency: transitive description: @@ -53,10 +61,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build_cli_annotations: dependency: transitive description: @@ -69,26 +77,26 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" convert: dependency: transitive description: @@ -149,18 +157,18 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" fixnum: dependency: transitive description: @@ -174,14 +182,30 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_lints: + dependency: "direct overridden" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" flutter_rust_bridge: dependency: transitive description: name: flutter_rust_bridge - sha256: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00" + sha256: "35c257fc7f98e34c1314d6c145e5ed54e7c94e8a9f469947e31c9298177d546f" url: "https://pub.dev" source: hosted - version: "2.7.1" + version: "2.7.0" flutter_test: dependency: "direct dev" description: flutter @@ -192,6 +216,22 @@ packages: description: flutter source: sdk version: "0.0.0" + gap: + dependency: transitive + description: + name: gap + sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d + url: "https://pub.dev" + source: hosted + version: "3.0.1" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" hex: dependency: transitive description: @@ -204,26 +244,26 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: transitive description: name: image - sha256: "8346ad4b5173924b5ddddab782fc7d8a6300178c8b1dc427775405a01701c4a6" + sha256: "13d3349ace88f12f4a0d175eb5c12dcdd39d35c4c109a8a13dfeb6d0bd9e31c3" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" js: dependency: transitive description: @@ -232,22 +272,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -256,6 +304,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" logger: dependency: transitive description: @@ -264,14 +320,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.5.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -356,55 +420,63 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" ndk: dependency: "direct main" description: path: "../ndk" relative: true source: path - version: "0.2.6" + version: "0.3.2" ndk_amber: dependency: "direct main" description: path: "../amber" relative: true source: path - version: "0.2.0" + version: "0.3.0" ndk_rust_verifier: dependency: "direct main" description: path: "../rust_verifier" relative: true source: path - version: "0.2.2" + version: "0.3.1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" package_info_plus: dependency: transitive description: name: package_info_plus - sha256: "67eae327b1b0faf761964a1d2e5d323c797f3799db0e85aa232db8d9e922bc35" + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" url: "https://pub.dev" source: hosted - version: "8.2.1" + version: "8.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "205ec83335c2ab9107bbba3f8997f9356d72ca3c715d2f038fc773d0366b4c76" + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: transitive description: @@ -417,18 +489,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "8c4967f8b7cb46dc914e178daa29813d83ae502e0529d7b0478330616a691ef7" + sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" url: "https://pub.dev" source: hosted - version: "2.2.14" + version: "2.2.16" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -457,10 +529,10 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" platform: dependency: transitive description: @@ -493,13 +565,94 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.1" + protocol_handler: + dependency: "direct main" + description: + name: protocol_handler + sha256: dc2e2dcb1e0e313c3f43827ec3fa6d98adee6e17edc0c3923ac67efee87479a9 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + protocol_handler_android: + dependency: transitive + description: + name: protocol_handler_android + sha256: "82eb860ca42149e400328f54b85140329a1766d982e94705b68271f6ca73895c" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + protocol_handler_ios: + dependency: transitive + description: + name: protocol_handler_ios + sha256: "0d3a56b8c1926002cb1e32b46b56874759f4dcc8183d389b670864ac041b6ec2" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + protocol_handler_macos: + dependency: transitive + description: + name: protocol_handler_macos + sha256: "6eb8687a84e7da3afbc5660ce046f29d7ecf7976db45a9dadeae6c87147dd710" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + protocol_handler_platform_interface: + dependency: transitive + description: + name: protocol_handler_platform_interface + sha256: "53776b10526fdc25efdf1abcf68baf57fdfdb75342f4101051db521c9e3f3e5b" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + protocol_handler_windows: + dependency: transitive + description: + name: protocol_handler_windows + sha256: d8f3a58938386aca2c76292757392f4d059d09f11439d6d896d876ebe997f2c4 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" rust_lib_ndk: - dependency: "direct overridden" + dependency: transitive description: - path: "../rust_verifier/rust_builder" - relative: true - source: path - version: "0.1.3" + name: rust_lib_ndk + sha256: "47dcfda62051315e869256c705e7c1c07649772d849eaad0474df11b10a2e1b0" + url: "https://pub.dev" + source: hosted + version: "0.1.5" rxdart: dependency: transitive description: @@ -564,19 +717,67 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.3" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.0" + sliver_tools: + dependency: transitive + description: + name: sliver_tools + sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 + url: "https://pub.dev" + source: hosted + version: "0.2.12" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -589,50 +790,66 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.0.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" url: "https://pub.dev" source: hosted - version: "3.3.0+3" + version: "3.3.1" + tap_builder: + dependency: transitive + description: + name: tap_builder + sha256: "10f00a5acb179aae7b22288042782bbca8624d8877ca425dcd3e87618ad88b09" + url: "https://pub.dev" + source: hosted + version: "0.5.0" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" typed_data: dependency: transitive description: @@ -657,6 +874,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + url: "https://pub.dev" + source: hosted + version: "6.3.15" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" uuid: dependency: transitive description: @@ -677,10 +958,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" volume_controller: dependency: transitive description: @@ -709,10 +990,10 @@ packages: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -725,10 +1006,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" web_socket_client: dependency: transitive description: @@ -741,10 +1022,26 @@ packages: dependency: transitive description: name: win32 - sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + url: "https://pub.dev" + source: hosted + version: "5.12.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + url: "https://pub.dev" + source: hosted + version: "1.1.5" + window_manager: + dependency: transitive + description: + name: window_manager + sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" url: "https://pub.dev" source: hosted - version: "5.10.1" + version: "0.4.3" xdg_directories: dependency: transitive description: @@ -761,6 +1058,23 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + xxh3: + dependency: transitive + description: + name: xxh3 + sha256: "399a0438f5d426785723c99da6b16e136f4953fb1e9db0bf270bd41dd4619916" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + zaplab_design: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: fb1120092d81e9086b1dfce0035edfea9232a4e2 + url: "https://github.com/NielLiesmons/zaplab_design" + source: git + version: "0.0.1" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/packages/sample-app/pubspec.yaml b/packages/sample-app/pubspec.yaml index 42f1e9e8c..c05e77d55 100644 --- a/packages/sample-app/pubspec.yaml +++ b/packages/sample-app/pubspec.yaml @@ -34,15 +34,23 @@ dependencies: media_kit_video: ^1.2.5 media_kit_libs_video: ^1.0.5 - ndk: 0.2.6 - ndk_amber: 0.2.0 - ndk_rust_verifier: 0.2.1 + ndk: 0.3.2 + ndk_amber: 0.3.0 + ndk_rust_verifier: 0.3.1 - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 - + go_router: ^14.6.2 + flutter_riverpod: ^2.6.1 + provider: ^6.0.0 # Added provider package +# android_intent_plus: ^5.3.0 + url_launcher: 6.3.1 + protocol_handler: ^0.2.0 + qr_flutter: ^4.1.0 amberflutter: any + zaplab_design: + git: + url: https://github.com/NielLiesmons/zaplab_design + dev_dependencies: flutter_test: @@ -51,7 +59,10 @@ dev_dependencies: dependency_overrides: ndk: path: ../ndk - + ndk_rust_verifier: + path: ../rust_verifier + ndk_amber: + path: ../amber # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your diff --git a/packages/sample-app/windows/flutter/generated_plugin_registrant.cc b/packages/sample-app/windows/flutter/generated_plugin_registrant.cc index 0594ad421..fef809fc3 100644 --- a/packages/sample-app/windows/flutter/generated_plugin_registrant.cc +++ b/packages/sample-app/windows/flutter/generated_plugin_registrant.cc @@ -8,13 +8,25 @@ #include #include +#include #include +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); + ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi")); ScreenBrightnessWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/packages/sample-app/windows/flutter/generated_plugins.cmake b/packages/sample-app/windows/flutter/generated_plugins.cmake index c1c6d2357..29ca11f56 100644 --- a/packages/sample-app/windows/flutter/generated_plugins.cmake +++ b/packages/sample-app/windows/flutter/generated_plugins.cmake @@ -5,7 +5,11 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_windows_video media_kit_video + protocol_handler_windows screen_brightness_windows + screen_retriever_windows + url_launcher_windows + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST