nostr tools added
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'package:nostr_tools/nostr_tools.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
|
|
||||||
/// Represents a Nostr event.
|
/// Represents a Nostr event.
|
||||||
class NostrEvent {
|
class NostrEvent {
|
||||||
@@ -24,6 +23,10 @@ class NostrEvent {
|
|||||||
/// Event signature (64-byte hex string).
|
/// Event signature (64-byte hex string).
|
||||||
final String sig;
|
final String sig;
|
||||||
|
|
||||||
|
/// Event API instance for event operations.
|
||||||
|
static final _eventApi = EventApi();
|
||||||
|
static final _keyApi = KeyApi();
|
||||||
|
|
||||||
/// Creates a [NostrEvent] with the provided values.
|
/// Creates a [NostrEvent] with the provided values.
|
||||||
NostrEvent({
|
NostrEvent({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -50,6 +53,33 @@ class NostrEvent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a [NostrEvent] from a nostr_tools Event object.
|
||||||
|
factory NostrEvent.fromNostrToolsEvent(Event event) {
|
||||||
|
return NostrEvent(
|
||||||
|
id: event.id,
|
||||||
|
pubkey: event.pubkey,
|
||||||
|
createdAt: event.created_at,
|
||||||
|
kind: event.kind,
|
||||||
|
tags: event.tags,
|
||||||
|
content: event.content,
|
||||||
|
sig: event.sig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts this [NostrEvent] to a nostr_tools Event object.
|
||||||
|
Event toNostrToolsEvent() {
|
||||||
|
return Event(
|
||||||
|
id: id,
|
||||||
|
pubkey: pubkey,
|
||||||
|
created_at: createdAt,
|
||||||
|
kind: kind,
|
||||||
|
tags: tags,
|
||||||
|
content: content,
|
||||||
|
sig: sig,
|
||||||
|
verify: false, // Already verified if coming from our model
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts the [NostrEvent] to a JSON array (Nostr event format).
|
/// Converts the [NostrEvent] to a JSON array (Nostr event format).
|
||||||
List<dynamic> toJson() {
|
List<dynamic> toJson() {
|
||||||
return [
|
return [
|
||||||
@@ -63,7 +93,7 @@ class NostrEvent {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an event from content and signs it with a private key.
|
/// Creates an event from content and signs it with a private key using nostr_tools.
|
||||||
///
|
///
|
||||||
/// [content] - Event content.
|
/// [content] - Event content.
|
||||||
/// [kind] - Event kind (default: 1 for text note).
|
/// [kind] - Event kind (default: 1 for text note).
|
||||||
@@ -75,51 +105,42 @@ class NostrEvent {
|
|||||||
required String privateKey,
|
required String privateKey,
|
||||||
List<List<String>>? tags,
|
List<List<String>>? tags,
|
||||||
}) {
|
}) {
|
||||||
// Derive public key from private key (simplified)
|
// Get public key from private key using nostr_tools
|
||||||
final privateKeyBytes = _hexToBytes(privateKey);
|
final pubkey = _keyApi.getPublicKey(privateKey);
|
||||||
final publicKeyBytes = sha256.convert(privateKeyBytes).bytes.sublist(0, 32);
|
|
||||||
final pubkey = publicKeyBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
||||||
|
|
||||||
final createdAt = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
final createdAt = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
final eventTags = tags ?? [];
|
final eventTags = tags ?? [];
|
||||||
|
|
||||||
// Create event data for signing
|
// Create event using nostr_tools Event model
|
||||||
final eventData = [
|
final event = Event(
|
||||||
0,
|
|
||||||
pubkey,
|
|
||||||
createdAt,
|
|
||||||
kind,
|
|
||||||
eventTags,
|
|
||||||
content,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Generate event ID (hash of event data)
|
|
||||||
final eventJson = jsonEncode(eventData);
|
|
||||||
final idBytes = sha256.convert(utf8.encode(eventJson)).bytes.sublist(0, 32);
|
|
||||||
final id = idBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
||||||
|
|
||||||
// Generate signature (simplified - in real Nostr, use secp256k1)
|
|
||||||
final sigBytes = sha256.convert(utf8.encode(id + privateKey)).bytes.sublist(0, 32);
|
|
||||||
final sig = sigBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
||||||
|
|
||||||
return NostrEvent(
|
|
||||||
id: id,
|
|
||||||
pubkey: pubkey,
|
|
||||||
createdAt: createdAt,
|
|
||||||
kind: kind,
|
kind: kind,
|
||||||
tags: eventTags,
|
tags: eventTags,
|
||||||
content: content,
|
content: content,
|
||||||
sig: sig,
|
created_at: createdAt,
|
||||||
|
pubkey: pubkey,
|
||||||
|
verify: false, // We'll sign it next
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Get event hash (ID) using nostr_tools
|
||||||
|
event.id = _eventApi.getEventHash(event);
|
||||||
|
|
||||||
|
// Sign the event using nostr_tools
|
||||||
|
event.sig = _eventApi.signEvent(event, privateKey);
|
||||||
|
|
||||||
|
// Convert to our NostrEvent model
|
||||||
|
return NostrEvent.fromNostrToolsEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts hex string to bytes.
|
/// Verifies the event signature using nostr_tools.
|
||||||
static List<int> _hexToBytes(String hex) {
|
///
|
||||||
final result = <int>[];
|
/// Returns true if the signature is valid, false otherwise.
|
||||||
for (int i = 0; i < hex.length; i += 2) {
|
bool verifySignature() {
|
||||||
result.add(int.parse(hex.substring(i, i + 2), radix: 16));
|
try {
|
||||||
|
final event = toNostrToolsEvent();
|
||||||
|
return _eventApi.verifySignature(event);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -127,4 +148,3 @@ class NostrEvent {
|
|||||||
return 'NostrEvent(id: ${id.substring(0, 8)}..., kind: $kind, content: ${content.substring(0, content.length > 20 ? 20 : content.length)}...)';
|
return 'NostrEvent(id: ${id.substring(0, 8)}..., kind: $kind, content: ${content.substring(0, content.length > 20 ? 20 : content.length)}...)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'package:nostr_tools/nostr_tools.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
|
||||||
|
|
||||||
/// Represents a Nostr keypair (private and public keys).
|
/// Represents a Nostr keypair (private and public keys).
|
||||||
class NostrKeyPair {
|
class NostrKeyPair {
|
||||||
@@ -9,6 +8,12 @@ class NostrKeyPair {
|
|||||||
/// Public key in hex format (32 bytes, 64 hex characters).
|
/// Public key in hex format (32 bytes, 64 hex characters).
|
||||||
final String publicKey;
|
final String publicKey;
|
||||||
|
|
||||||
|
/// Key API instance for key operations.
|
||||||
|
static final _keyApi = KeyApi();
|
||||||
|
|
||||||
|
/// NIP-19 API instance for bech32 encoding/decoding.
|
||||||
|
static final _nip19 = Nip19();
|
||||||
|
|
||||||
/// Creates a [NostrKeyPair] with the provided keys.
|
/// Creates a [NostrKeyPair] with the provided keys.
|
||||||
///
|
///
|
||||||
/// [privateKey] - Private key in hex format.
|
/// [privateKey] - Private key in hex format.
|
||||||
@@ -18,18 +23,12 @@ class NostrKeyPair {
|
|||||||
required this.publicKey,
|
required this.publicKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Generates a new Nostr keypair.
|
/// Generates a new Nostr keypair using nostr_tools.
|
||||||
///
|
///
|
||||||
/// Returns a new [NostrKeyPair] with random private and public keys.
|
/// Returns a new [NostrKeyPair] with random private and public keys.
|
||||||
factory NostrKeyPair.generate() {
|
factory NostrKeyPair.generate() {
|
||||||
// Generate random 32-byte private key
|
final privateKey = _keyApi.generatePrivateKey();
|
||||||
final random = List<int>.generate(32, (i) => DateTime.now().microsecondsSinceEpoch % 256);
|
final publicKey = _keyApi.getPublicKey(privateKey);
|
||||||
final privateKeyBytes = sha256.convert(utf8.encode(DateTime.now().toString() + random.toString())).bytes.sublist(0, 32);
|
|
||||||
final privateKey = privateKeyBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
||||||
|
|
||||||
// Derive public key from private key (simplified - in real Nostr, use secp256k1)
|
|
||||||
final publicKeyBytes = sha256.convert(privateKeyBytes).bytes.sublist(0, 32);
|
|
||||||
final publicKey = publicKeyBytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
||||||
|
|
||||||
return NostrKeyPair(
|
return NostrKeyPair(
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
@@ -37,6 +36,78 @@ class NostrKeyPair {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a [NostrKeyPair] from nsec (private key in bech32 format).
|
||||||
|
///
|
||||||
|
/// [nsec] - Private key in nsec format (e.g., 'nsec1...').
|
||||||
|
///
|
||||||
|
/// Returns a [NostrKeyPair] with the decoded private key and derived public key.
|
||||||
|
///
|
||||||
|
/// Throws [FormatException] if nsec is invalid.
|
||||||
|
factory NostrKeyPair.fromNsec(String nsec) {
|
||||||
|
try {
|
||||||
|
final decoded = _nip19.decode(nsec);
|
||||||
|
if (decoded['type'] != 'nsec') {
|
||||||
|
throw FormatException('Invalid nsec format: expected "nsec" type');
|
||||||
|
}
|
||||||
|
|
||||||
|
final privateKey = decoded['data'] as String;
|
||||||
|
final publicKey = _keyApi.getPublicKey(privateKey);
|
||||||
|
|
||||||
|
return NostrKeyPair(
|
||||||
|
privateKey: privateKey,
|
||||||
|
publicKey: publicKey,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw FormatException('Failed to parse nsec: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [NostrKeyPair] from npub (public key in bech32 format).
|
||||||
|
///
|
||||||
|
/// Note: This creates a keypair with only the public key. Private key operations won't work.
|
||||||
|
///
|
||||||
|
/// [npub] - Public key in npub format (e.g., 'npub1...').
|
||||||
|
///
|
||||||
|
/// Returns a [NostrKeyPair] with the decoded public key and empty private key.
|
||||||
|
///
|
||||||
|
/// Throws [FormatException] if npub is invalid.
|
||||||
|
factory NostrKeyPair.fromNpub(String npub) {
|
||||||
|
try {
|
||||||
|
final decoded = _nip19.decode(npub);
|
||||||
|
if (decoded['type'] != 'npub') {
|
||||||
|
throw FormatException('Invalid npub format: expected "npub" type');
|
||||||
|
}
|
||||||
|
|
||||||
|
final publicKey = decoded['data'] as String;
|
||||||
|
|
||||||
|
// No private key available when importing from npub
|
||||||
|
return NostrKeyPair(
|
||||||
|
privateKey: '', // Empty private key - can't sign events
|
||||||
|
publicKey: publicKey,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw FormatException('Failed to parse npub: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [NostrKeyPair] from a hex private key.
|
||||||
|
///
|
||||||
|
/// [hexPrivateKey] - Private key in hex format (64 hex characters).
|
||||||
|
///
|
||||||
|
/// Returns a [NostrKeyPair] with the provided private key and derived public key.
|
||||||
|
factory NostrKeyPair.fromHexPrivateKey(String hexPrivateKey) {
|
||||||
|
if (hexPrivateKey.length != 64) {
|
||||||
|
throw FormatException('Invalid hex private key: expected 64 hex characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
final publicKey = _keyApi.getPublicKey(hexPrivateKey);
|
||||||
|
|
||||||
|
return NostrKeyPair(
|
||||||
|
privateKey: hexPrivateKey,
|
||||||
|
publicKey: publicKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Creates a [NostrKeyPair] from a JSON map.
|
/// Creates a [NostrKeyPair] from a JSON map.
|
||||||
factory NostrKeyPair.fromJson(Map<String, dynamic> json) {
|
factory NostrKeyPair.fromJson(Map<String, dynamic> json) {
|
||||||
return NostrKeyPair(
|
return NostrKeyPair(
|
||||||
@@ -53,9 +124,21 @@ class NostrKeyPair {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encodes the private key to nsec format.
|
||||||
|
String toNsec() {
|
||||||
|
if (privateKey.isEmpty) {
|
||||||
|
throw StateError('Cannot encode empty private key to nsec');
|
||||||
|
}
|
||||||
|
return _nip19.nsecEncode(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encodes the public key to npub format.
|
||||||
|
String toNpub() {
|
||||||
|
return _nip19.npubEncode(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'NostrKeyPair(publicKey: ${publicKey.substring(0, 8)}...)';
|
return 'NostrKeyPair(publicKey: $publicKey)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
/// Represents a Nostr user profile (metadata from kind 0 events).
|
||||||
|
class NostrProfile {
|
||||||
|
/// Public key (npub or hex).
|
||||||
|
final String publicKey;
|
||||||
|
|
||||||
|
/// Display name or username.
|
||||||
|
final String? name;
|
||||||
|
|
||||||
|
/// About/bio text.
|
||||||
|
final String? about;
|
||||||
|
|
||||||
|
/// Profile picture URL.
|
||||||
|
final String? picture;
|
||||||
|
|
||||||
|
/// Website URL.
|
||||||
|
final String? website;
|
||||||
|
|
||||||
|
/// NIP-05 identifier (e.g., [email protected]).
|
||||||
|
final String? nip05;
|
||||||
|
|
||||||
|
/// Banner image URL.
|
||||||
|
final String? banner;
|
||||||
|
|
||||||
|
/// LUD16 (Lightning address).
|
||||||
|
final String? lud16;
|
||||||
|
|
||||||
|
/// Raw metadata JSON for additional fields.
|
||||||
|
final Map<String, dynamic> rawMetadata;
|
||||||
|
|
||||||
|
/// Timestamp when profile was last updated.
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
/// Creates a [NostrProfile] instance.
|
||||||
|
NostrProfile({
|
||||||
|
required this.publicKey,
|
||||||
|
this.name,
|
||||||
|
this.about,
|
||||||
|
this.picture,
|
||||||
|
this.website,
|
||||||
|
this.nip05,
|
||||||
|
this.banner,
|
||||||
|
this.lud16,
|
||||||
|
Map<String, dynamic>? rawMetadata,
|
||||||
|
this.updatedAt,
|
||||||
|
}) : rawMetadata = rawMetadata ?? {};
|
||||||
|
|
||||||
|
/// Creates a [NostrProfile] from Nostr metadata event content (JSON string).
|
||||||
|
///
|
||||||
|
/// [publicKey] - The public key of the profile owner.
|
||||||
|
/// [content] - JSON string from kind 0 event content.
|
||||||
|
/// [updatedAt] - Optional timestamp when profile was updated.
|
||||||
|
factory NostrProfile.fromEventContent({
|
||||||
|
required String publicKey,
|
||||||
|
required String content,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
final metadata = jsonDecode(content) as Map<String, dynamic>;
|
||||||
|
|
||||||
|
return NostrProfile(
|
||||||
|
publicKey: publicKey,
|
||||||
|
name: metadata['name'] as String?,
|
||||||
|
about: metadata['about'] as String?,
|
||||||
|
picture: metadata['picture'] as String?,
|
||||||
|
website: metadata['website'] as String?,
|
||||||
|
nip05: metadata['nip05'] as String?,
|
||||||
|
banner: metadata['banner'] as String?,
|
||||||
|
lud16: metadata['lud16'] as String?,
|
||||||
|
rawMetadata: metadata,
|
||||||
|
updatedAt: updatedAt ?? DateTime.now(),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Return minimal profile if parsing fails
|
||||||
|
return NostrProfile(
|
||||||
|
publicKey: publicKey,
|
||||||
|
rawMetadata: {},
|
||||||
|
updatedAt: updatedAt ?? DateTime.now(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [NostrProfile] from a JSON map.
|
||||||
|
factory NostrProfile.fromJson(Map<String, dynamic> json) {
|
||||||
|
return NostrProfile(
|
||||||
|
publicKey: json['publicKey'] as String,
|
||||||
|
name: json['name'] as String?,
|
||||||
|
about: json['about'] as String?,
|
||||||
|
picture: json['picture'] as String?,
|
||||||
|
website: json['website'] as String?,
|
||||||
|
nip05: json['nip05'] as String?,
|
||||||
|
banner: json['banner'] as String?,
|
||||||
|
lud16: json['lud16'] as String?,
|
||||||
|
rawMetadata: json['rawMetadata'] as Map<String, dynamic>? ?? {},
|
||||||
|
updatedAt: json['updatedAt'] != null
|
||||||
|
? DateTime.parse(json['updatedAt'] as String)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts [NostrProfile] to JSON.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'publicKey': publicKey,
|
||||||
|
'name': name,
|
||||||
|
'about': about,
|
||||||
|
'picture': picture,
|
||||||
|
'website': website,
|
||||||
|
'nip05': nip05,
|
||||||
|
'banner': banner,
|
||||||
|
'lud16': lud16,
|
||||||
|
'rawMetadata': rawMetadata,
|
||||||
|
'updatedAt': updatedAt?.toIso8601String(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets display name (name, nip05, or public key prefix).
|
||||||
|
String get displayName {
|
||||||
|
if (name != null && name!.isNotEmpty) return name!;
|
||||||
|
if (nip05 != null && nip05!.isNotEmpty) return nip05!;
|
||||||
|
return publicKey.length > 16
|
||||||
|
? '${publicKey.substring(0, 8)}...${publicKey.substring(publicKey.length - 8)}'
|
||||||
|
: publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'NostrProfile(publicKey: ${publicKey.substring(0, 8)}..., name: $name)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
import 'package:nostr_tools/nostr_tools.dart';
|
||||||
import 'models/nostr_keypair.dart';
|
import 'models/nostr_keypair.dart';
|
||||||
import 'models/nostr_event.dart';
|
import 'models/nostr_event.dart';
|
||||||
import 'models/nostr_relay.dart';
|
import 'models/nostr_relay.dart';
|
||||||
|
import 'models/nostr_profile.dart';
|
||||||
|
|
||||||
/// Exception thrown when Nostr operations fail.
|
/// Exception thrown when Nostr operations fail.
|
||||||
class NostrException implements Exception {
|
class NostrException implements Exception {
|
||||||
@@ -104,12 +107,32 @@ class NostrService {
|
|||||||
(message) {
|
(message) {
|
||||||
try {
|
try {
|
||||||
final data = jsonDecode(message as String);
|
final data = jsonDecode(message as String);
|
||||||
if (data is List) {
|
if (data is List && data.isNotEmpty) {
|
||||||
|
final messageType = data[0] as String;
|
||||||
|
if (messageType == 'EVENT' && data.length >= 3) {
|
||||||
|
// EVENT format: ["EVENT", <subscription_id>, <event_json>]
|
||||||
|
// event_json can be either a JSON object or array format
|
||||||
|
final eventData = data[2];
|
||||||
controller.add({
|
controller.add({
|
||||||
'type': data[0] as String,
|
'type': 'EVENT',
|
||||||
|
'subscription_id': data[1],
|
||||||
|
'data': eventData,
|
||||||
|
});
|
||||||
|
} else if (messageType == 'EOSE' && data.length >= 2) {
|
||||||
|
// EOSE format: ["EOSE", <subscription_id>]
|
||||||
|
controller.add({
|
||||||
|
'type': 'EOSE',
|
||||||
|
'subscription_id': data[1],
|
||||||
|
'data': null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Other message types
|
||||||
|
controller.add({
|
||||||
|
'type': messageType,
|
||||||
'data': data.length > 1 ? data[1] : null,
|
'data': data.length > 1 ? data[1] : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore invalid messages
|
// Ignore invalid messages
|
||||||
}
|
}
|
||||||
@@ -165,8 +188,12 @@ class NostrService {
|
|||||||
throw NostrException('Not connected to relay: $relayUrl');
|
throw NostrException('Not connected to relay: $relayUrl');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert to nostr_tools Event and then to JSON
|
||||||
|
final nostrToolsEvent = event.toNostrToolsEvent();
|
||||||
|
final eventJson = nostrToolsEvent.toJson();
|
||||||
|
|
||||||
// Send event in Nostr format: ["EVENT", <event_json>]
|
// Send event in Nostr format: ["EVENT", <event_json>]
|
||||||
final message = jsonEncode(['EVENT', event.toJson()]);
|
final message = jsonEncode(['EVENT', eventJson]);
|
||||||
channel.sink.add(message);
|
channel.sink.add(message);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw NostrException('Failed to publish event: $e');
|
throw NostrException('Failed to publish event: $e');
|
||||||
@@ -229,6 +256,163 @@ class NostrService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches user profile (kind 0 metadata event) from relays.
|
||||||
|
///
|
||||||
|
/// [publicKey] - The public key (hex format) of the user.
|
||||||
|
/// [timeout] - Timeout for the request (default: 10 seconds).
|
||||||
|
///
|
||||||
|
/// Returns [NostrProfile] if found, null otherwise.
|
||||||
|
///
|
||||||
|
/// Throws [NostrException] if fetch fails.
|
||||||
|
Future<NostrProfile?> fetchProfile(String publicKey, {Duration timeout = const Duration(seconds: 10)}) async {
|
||||||
|
if (_relays.isEmpty) {
|
||||||
|
throw NostrException('No relays configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fetch from connected relays first
|
||||||
|
for (final relay in _relays) {
|
||||||
|
if (relay.isConnected) {
|
||||||
|
try {
|
||||||
|
final profile = await _fetchProfileFromRelay(publicKey, relay.url, timeout);
|
||||||
|
if (profile != null) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue to next relay
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no connected relays or all failed, try connecting to first relay
|
||||||
|
if (_relays.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final firstRelay = _relays.first;
|
||||||
|
if (!firstRelay.isConnected) {
|
||||||
|
await connectRelay(firstRelay.url).timeout(timeout);
|
||||||
|
}
|
||||||
|
return await _fetchProfileFromRelay(publicKey, firstRelay.url, timeout);
|
||||||
|
} catch (e) {
|
||||||
|
throw NostrException('Failed to fetch profile: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches profile from a specific relay.
|
||||||
|
Future<NostrProfile?> _fetchProfileFromRelay(String publicKey, String relayUrl, Duration timeout) async {
|
||||||
|
final channel = _connections[relayUrl];
|
||||||
|
if (channel == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send REQ message to request kind 0 events for this public key
|
||||||
|
// Nostr REQ format: ["REQ", <subscription_id>, <filters>]
|
||||||
|
final reqId = 'profile_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
|
final completer = Completer<NostrProfile?>();
|
||||||
|
final subscription = _messageControllers[relayUrl]?.stream.listen(
|
||||||
|
(message) {
|
||||||
|
// Message format from connectRelay:
|
||||||
|
// {'type': 'EVENT', 'subscription_id': <id>, 'data': <event_json>}
|
||||||
|
// or {'type': 'EOSE', 'subscription_id': <id>, 'data': null}
|
||||||
|
if (message['type'] == 'EVENT' &&
|
||||||
|
message['subscription_id'] == reqId &&
|
||||||
|
message['data'] != null) {
|
||||||
|
try {
|
||||||
|
final eventData = message['data'];
|
||||||
|
Event nostrToolsEvent;
|
||||||
|
|
||||||
|
// Handle both JSON object and array formats
|
||||||
|
if (eventData is Map<String, dynamic>) {
|
||||||
|
// JSON object format
|
||||||
|
nostrToolsEvent = Event(
|
||||||
|
id: eventData['id'] as String? ?? '',
|
||||||
|
pubkey: eventData['pubkey'] as String? ?? '',
|
||||||
|
created_at: eventData['created_at'] as int? ?? 0,
|
||||||
|
kind: eventData['kind'] as int? ?? 0,
|
||||||
|
tags: (eventData['tags'] as List<dynamic>?)
|
||||||
|
?.map((tag) => (tag as List<dynamic>).map((e) => e.toString()).toList())
|
||||||
|
.toList() ?? [],
|
||||||
|
content: eventData['content'] as String? ?? '',
|
||||||
|
sig: eventData['sig'] as String? ?? '',
|
||||||
|
verify: false, // Skip verification for profile fetching
|
||||||
|
);
|
||||||
|
} else if (eventData is List && eventData.length >= 7) {
|
||||||
|
// Array format: [id, pubkey, created_at, kind, tags, content, sig]
|
||||||
|
nostrToolsEvent = Event(
|
||||||
|
id: eventData[0] as String? ?? '',
|
||||||
|
pubkey: eventData[1] as String? ?? '',
|
||||||
|
created_at: eventData[2] as int? ?? 0,
|
||||||
|
kind: eventData[3] as int? ?? 0,
|
||||||
|
tags: (eventData[4] as List<dynamic>?)
|
||||||
|
?.map((tag) => (tag as List<dynamic>).map((e) => e.toString()).toList())
|
||||||
|
.toList() ?? [],
|
||||||
|
content: eventData[5] as String? ?? '',
|
||||||
|
sig: eventData[6] as String? ?? '',
|
||||||
|
verify: false, // Skip verification for profile fetching
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return; // Invalid format
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to our NostrEvent model
|
||||||
|
final event = NostrEvent.fromNostrToolsEvent(nostrToolsEvent);
|
||||||
|
|
||||||
|
// Check if it's a kind 0 (metadata) event for this public key
|
||||||
|
if (event.kind == 0 && event.pubkey.toLowerCase() == publicKey.toLowerCase()) {
|
||||||
|
final profile = NostrProfile.fromEventContent(
|
||||||
|
publicKey: publicKey,
|
||||||
|
content: event.content,
|
||||||
|
updatedAt: DateTime.fromMillisecondsSinceEpoch(event.createdAt * 1000),
|
||||||
|
);
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parsing errors
|
||||||
|
debugPrint('Error parsing profile event: $e');
|
||||||
|
}
|
||||||
|
} else if (message['type'] == 'EOSE' &&
|
||||||
|
message['subscription_id'] == reqId) {
|
||||||
|
// End of stored events - no profile found
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.completeError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send REQ message to request kind 0 events for this public key
|
||||||
|
final reqMessage = jsonEncode([
|
||||||
|
'REQ',
|
||||||
|
reqId,
|
||||||
|
{
|
||||||
|
'authors': [publicKey],
|
||||||
|
'kinds': [0],
|
||||||
|
'limit': 1,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
channel.sink.add(reqMessage);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final profile = await completer.future.timeout(timeout);
|
||||||
|
subscription?.cancel();
|
||||||
|
return profile;
|
||||||
|
} catch (e) {
|
||||||
|
subscription?.cancel();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Closes all connections and cleans up resources.
|
/// Closes all connections and cleans up resources.
|
||||||
void dispose() {
|
void dispose() {
|
||||||
for (final relayUrl in _connections.keys.toList()) {
|
for (final relayUrl in _connections.keys.toList()) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import '../../nostr/models/nostr_profile.dart';
|
||||||
|
|
||||||
/// Data model representing a user session.
|
/// Data model representing a user session.
|
||||||
///
|
///
|
||||||
/// This model stores user identification and authentication information
|
/// This model stores user identification and authentication information
|
||||||
@@ -15,17 +17,22 @@ class User {
|
|||||||
/// Timestamp when the session was created (milliseconds since epoch).
|
/// Timestamp when the session was created (milliseconds since epoch).
|
||||||
final int createdAt;
|
final int createdAt;
|
||||||
|
|
||||||
|
/// Optional Nostr profile data (if logged in via Nostr).
|
||||||
|
final NostrProfile? nostrProfile;
|
||||||
|
|
||||||
/// Creates a [User] instance.
|
/// Creates a [User] instance.
|
||||||
///
|
///
|
||||||
/// [id] - Unique identifier for the user.
|
/// [id] - Unique identifier for the user.
|
||||||
/// [username] - Display name or username.
|
/// [username] - Display name or username.
|
||||||
/// [token] - Optional authentication token.
|
/// [token] - Optional authentication token.
|
||||||
/// [createdAt] - Session creation timestamp (defaults to current time).
|
/// [createdAt] - Session creation timestamp (defaults to current time).
|
||||||
|
/// [nostrProfile] - Optional Nostr profile data.
|
||||||
User({
|
User({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.username,
|
required this.username,
|
||||||
this.token,
|
this.token,
|
||||||
int? createdAt,
|
int? createdAt,
|
||||||
|
this.nostrProfile,
|
||||||
}) : createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch;
|
}) : createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch;
|
||||||
|
|
||||||
/// Creates a [User] from a Map (e.g., from database or JSON).
|
/// Creates a [User] from a Map (e.g., from database or JSON).
|
||||||
@@ -35,6 +42,9 @@ class User {
|
|||||||
username: map['username'] as String,
|
username: map['username'] as String,
|
||||||
token: map['token'] as String?,
|
token: map['token'] as String?,
|
||||||
createdAt: map['created_at'] as int?,
|
createdAt: map['created_at'] as int?,
|
||||||
|
nostrProfile: map['nostr_profile'] != null
|
||||||
|
? NostrProfile.fromJson(map['nostr_profile'] as Map<String, dynamic>)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +55,7 @@ class User {
|
|||||||
'username': username,
|
'username': username,
|
||||||
'token': token,
|
'token': token,
|
||||||
'created_at': createdAt,
|
'created_at': createdAt,
|
||||||
|
'nostr_profile': nostrProfile?.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,12 +65,14 @@ class User {
|
|||||||
String? username,
|
String? username,
|
||||||
String? token,
|
String? token,
|
||||||
int? createdAt,
|
int? createdAt,
|
||||||
|
NostrProfile? nostrProfile,
|
||||||
}) {
|
}) {
|
||||||
return User(
|
return User(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
username: username ?? this.username,
|
username: username ?? this.username,
|
||||||
token: token ?? this.token,
|
token: token ?? this.token,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
nostrProfile: nostrProfile ?? this.nostrProfile,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import 'package:path_provider/path_provider.dart';
|
|||||||
import '../local/local_storage_service.dart';
|
import '../local/local_storage_service.dart';
|
||||||
import '../sync/sync_engine.dart';
|
import '../sync/sync_engine.dart';
|
||||||
import '../firebase/firebase_service.dart';
|
import '../firebase/firebase_service.dart';
|
||||||
|
import '../nostr/nostr_service.dart';
|
||||||
|
import '../nostr/models/nostr_keypair.dart';
|
||||||
|
import '../nostr/models/nostr_profile.dart';
|
||||||
import 'models/user.dart';
|
import 'models/user.dart';
|
||||||
|
|
||||||
/// Exception thrown when session operations fail.
|
/// Exception thrown when session operations fail.
|
||||||
@@ -42,6 +45,9 @@ class SessionService {
|
|||||||
/// Firebase service for optional cloud sync (optional).
|
/// Firebase service for optional cloud sync (optional).
|
||||||
final FirebaseService? _firebaseService;
|
final FirebaseService? _firebaseService;
|
||||||
|
|
||||||
|
/// Nostr service for Nostr authentication (optional).
|
||||||
|
final NostrService? _nostrService;
|
||||||
|
|
||||||
/// Map of user IDs to their session storage paths.
|
/// Map of user IDs to their session storage paths.
|
||||||
final Map<String, String> _userDbPaths = {};
|
final Map<String, String> _userDbPaths = {};
|
||||||
|
|
||||||
@@ -59,17 +65,20 @@ class SessionService {
|
|||||||
/// [localStorage] - Local storage service for data persistence.
|
/// [localStorage] - Local storage service for data persistence.
|
||||||
/// [syncEngine] - Optional sync engine for coordinating sync operations.
|
/// [syncEngine] - Optional sync engine for coordinating sync operations.
|
||||||
/// [firebaseService] - Optional Firebase service for cloud sync.
|
/// [firebaseService] - Optional Firebase service for cloud sync.
|
||||||
|
/// [nostrService] - Optional Nostr service for Nostr authentication.
|
||||||
/// [testDbPath] - Optional database path for testing.
|
/// [testDbPath] - Optional database path for testing.
|
||||||
/// [testCacheDir] - Optional cache directory for testing.
|
/// [testCacheDir] - Optional cache directory for testing.
|
||||||
SessionService({
|
SessionService({
|
||||||
required LocalStorageService localStorage,
|
required LocalStorageService localStorage,
|
||||||
SyncEngine? syncEngine,
|
SyncEngine? syncEngine,
|
||||||
FirebaseService? firebaseService,
|
FirebaseService? firebaseService,
|
||||||
|
NostrService? nostrService,
|
||||||
String? testDbPath,
|
String? testDbPath,
|
||||||
Directory? testCacheDir,
|
Directory? testCacheDir,
|
||||||
}) : _localStorage = localStorage,
|
}) : _localStorage = localStorage,
|
||||||
_syncEngine = syncEngine,
|
_syncEngine = syncEngine,
|
||||||
_firebaseService = firebaseService,
|
_firebaseService = firebaseService,
|
||||||
|
_nostrService = nostrService,
|
||||||
_testDbPath = testDbPath,
|
_testDbPath = testDbPath,
|
||||||
_testCacheDir = testCacheDir;
|
_testCacheDir = testCacheDir;
|
||||||
|
|
||||||
@@ -128,6 +137,71 @@ class SessionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Logs in a user using Nostr key (nsec or npub).
|
||||||
|
///
|
||||||
|
/// [nsecOrNpub] - Nostr key in nsec (private) or npub (public) format.
|
||||||
|
///
|
||||||
|
/// Returns the logged-in [User] with fetched profile data.
|
||||||
|
///
|
||||||
|
/// Throws [SessionException] if login fails or if user is already logged in.
|
||||||
|
Future<User> loginWithNostr(String nsecOrNpub) async {
|
||||||
|
if (_currentUser != null) {
|
||||||
|
throw SessionException('User already logged in. Logout first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_nostrService == null) {
|
||||||
|
throw SessionException('Nostr service not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse the key
|
||||||
|
NostrKeyPair keyPair;
|
||||||
|
if (nsecOrNpub.startsWith('nsec')) {
|
||||||
|
keyPair = NostrKeyPair.fromNsec(nsecOrNpub);
|
||||||
|
} else if (nsecOrNpub.startsWith('npub')) {
|
||||||
|
keyPair = NostrKeyPair.fromNpub(nsecOrNpub);
|
||||||
|
} else {
|
||||||
|
throw SessionException('Invalid Nostr key format. Expected nsec or npub.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch profile from relays
|
||||||
|
NostrProfile? profile;
|
||||||
|
try {
|
||||||
|
profile = await _nostrService!.fetchProfile(keyPair.publicKey);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Warning: Failed to fetch Nostr profile: $e');
|
||||||
|
// Continue without profile - offline-first behavior
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user with Nostr profile
|
||||||
|
final user = User(
|
||||||
|
id: keyPair.publicKey,
|
||||||
|
username: profile?.displayName ?? keyPair.publicKey.substring(0, 16),
|
||||||
|
nostrProfile: profile,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create user-specific storage paths
|
||||||
|
await _setupUserStorage(user);
|
||||||
|
|
||||||
|
// Sync with Firebase if enabled
|
||||||
|
if (_firebaseService != null && _firebaseService!.isEnabled) {
|
||||||
|
try {
|
||||||
|
await _firebaseService!.syncItemsFromFirestore(user.id);
|
||||||
|
} catch (e) {
|
||||||
|
// Log error but don't fail login - offline-first behavior
|
||||||
|
debugPrint('Warning: Failed to sync from Firebase on login: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set as current user
|
||||||
|
_currentUser = user;
|
||||||
|
|
||||||
|
return user;
|
||||||
|
} catch (e) {
|
||||||
|
throw SessionException('Failed to login with Nostr: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Logs out the current user and clears session data.
|
/// Logs out the current user and clears session data.
|
||||||
///
|
///
|
||||||
/// [clearCache] - Whether to clear cached data (default: true).
|
/// [clearCache] - Whether to clear cached data (default: true).
|
||||||
|
|||||||
+2
-1
@@ -117,11 +117,12 @@ class _MyAppState extends State<MyApp> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize SessionService with Firebase integration
|
// Initialize SessionService with Firebase and Nostr integration
|
||||||
_sessionService = SessionService(
|
_sessionService = SessionService(
|
||||||
localStorage: _storageService!,
|
localStorage: _storageService!,
|
||||||
syncEngine: _syncEngine,
|
syncEngine: _syncEngine,
|
||||||
firebaseService: _firebaseService,
|
firebaseService: _firebaseService,
|
||||||
|
nostrService: _nostrService,
|
||||||
);
|
);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class _NostrEventsScreenState extends State<NostrEventsScreen> {
|
|||||||
Map<String, bool> _connectionStatus = {};
|
Map<String, bool> _connectionStatus = {};
|
||||||
List<String> _events = [];
|
List<String> _events = [];
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
final TextEditingController _nsecController = TextEditingController();
|
||||||
|
final TextEditingController _npubController = TextEditingController();
|
||||||
|
final TextEditingController _hexPrivateKeyController = TextEditingController();
|
||||||
|
bool _showImportFields = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -34,6 +38,14 @@ class _NostrEventsScreenState extends State<NostrEventsScreen> {
|
|||||||
_generateKeyPair();
|
_generateKeyPair();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nsecController.dispose();
|
||||||
|
_npubController.dispose();
|
||||||
|
_hexPrivateKeyController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _loadRelays() {
|
void _loadRelays() {
|
||||||
if (widget.nostrService == null) return;
|
if (widget.nostrService == null) return;
|
||||||
|
|
||||||
@@ -50,9 +62,115 @@ class _NostrEventsScreenState extends State<NostrEventsScreen> {
|
|||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_keyPair = widget.nostrService!.generateKeyPair();
|
_keyPair = widget.nostrService!.generateKeyPair();
|
||||||
|
_nsecController.clear();
|
||||||
|
_npubController.clear();
|
||||||
|
_hexPrivateKeyController.clear();
|
||||||
|
_showImportFields = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _importFromNsec() {
|
||||||
|
final nsec = _nsecController.text.trim();
|
||||||
|
if (nsec.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Please enter an nsec key'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() {
|
||||||
|
_keyPair = NostrKeyPair.fromNsec(nsec);
|
||||||
|
_showImportFields = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Keypair imported from nsec'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to import nsec: ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _importFromNpub() {
|
||||||
|
final npub = _npubController.text.trim();
|
||||||
|
if (npub.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Please enter an npub key'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() {
|
||||||
|
_keyPair = NostrKeyPair.fromNpub(npub);
|
||||||
|
_showImportFields = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Public key imported from npub (note: cannot sign events without private key)'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to import npub: ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _importFromHexPrivateKey() {
|
||||||
|
final hexKey = _hexPrivateKeyController.text.trim();
|
||||||
|
if (hexKey.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Please enter a hex private key'),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setState(() {
|
||||||
|
_keyPair = NostrKeyPair.fromHexPrivateKey(hexKey);
|
||||||
|
_showImportFields = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Keypair imported from hex private key'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to import hex key: ${e.toString()}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _connectToRelay(String relayUrl) async {
|
Future<void> _connectToRelay(String relayUrl) async {
|
||||||
if (widget.nostrService == null) return;
|
if (widget.nostrService == null) return;
|
||||||
|
|
||||||
@@ -149,6 +267,21 @@ class _NostrEventsScreenState extends State<NostrEventsScreen> {
|
|||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (_keyPair!.privateKey.isEmpty) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Cannot publish: No private key available. Import nsec or hex private key.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a test event
|
// Create a test event
|
||||||
final event = NostrEvent.create(
|
final event = NostrEvent.create(
|
||||||
@@ -236,6 +369,9 @@ class _NostrEventsScreenState extends State<NostrEventsScreen> {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'Keypair',
|
'Keypair',
|
||||||
@@ -244,21 +380,102 @@ class _NostrEventsScreenState extends State<NostrEventsScreen> {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_showImportFields = !_showImportFields;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: Icon(_showImportFields ? Icons.visibility_off : Icons.import_export),
|
||||||
|
label: Text(_showImportFields ? 'Hide Import' : 'Import Key'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
if (_keyPair != null) ...[
|
if (_keyPair != null) ...[
|
||||||
Text(
|
const Text(
|
||||||
'Public Key: ${_keyPair!.publicKey.substring(0, 16)}...',
|
'Public Key:',
|
||||||
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
SelectableText(
|
||||||
Text(
|
_keyPair!.publicKey,
|
||||||
'Private Key: ${_keyPair!.privateKey.substring(0, 16)}...',
|
style: const TextStyle(fontSize: 11, fontFamily: 'monospace'),
|
||||||
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (_keyPair!.privateKey.isNotEmpty) ...[
|
||||||
|
const Text(
|
||||||
|
'Private Key:',
|
||||||
|
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
SelectableText(
|
||||||
|
_keyPair!.privateKey,
|
||||||
|
style: const TextStyle(fontSize: 11, fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
const Text(
|
||||||
|
'Private Key: (not available - imported from npub only)',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.orange),
|
||||||
|
),
|
||||||
|
],
|
||||||
] else ...[
|
] else ...[
|
||||||
const Text('No keypair generated'),
|
const Text('No keypair generated'),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
if (_showImportFields) ...[
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Import Key',
|
||||||
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextField(
|
||||||
|
controller: _nsecController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'nsec (private key)',
|
||||||
|
hintText: 'nsec1...',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
helperText: 'Enter your nsec private key',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _importFromNsec,
|
||||||
|
child: const Text('Import from nsec'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _npubController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'npub (public key)',
|
||||||
|
hintText: 'npub1...',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
helperText: 'Enter npub to view only (cannot sign events)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _importFromNpub,
|
||||||
|
child: const Text('Import from npub'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _hexPrivateKeyController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Hex Private Key',
|
||||||
|
hintText: '64 hex characters',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
helperText: 'Enter private key in hex format (64 characters)',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _importFromHexPrivateKey,
|
||||||
|
child: const Text('Import from Hex'),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: _generateKeyPair,
|
onPressed: _generateKeyPair,
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ class _SessionScreenState extends State<SessionScreen> {
|
|||||||
final TextEditingController _userIdController = TextEditingController();
|
final TextEditingController _userIdController = TextEditingController();
|
||||||
final TextEditingController _emailController = TextEditingController();
|
final TextEditingController _emailController = TextEditingController();
|
||||||
final TextEditingController _passwordController = TextEditingController();
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
|
final TextEditingController _nostrKeyController = TextEditingController();
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _useFirebaseAuth = false;
|
bool _useFirebaseAuth = false;
|
||||||
|
bool _useNostrLogin = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -41,6 +43,7 @@ class _SessionScreenState extends State<SessionScreen> {
|
|||||||
_userIdController.dispose();
|
_userIdController.dispose();
|
||||||
_emailController.dispose();
|
_emailController.dispose();
|
||||||
_passwordController.dispose();
|
_passwordController.dispose();
|
||||||
|
_nostrKeyController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +55,50 @@ class _SessionScreenState extends State<SessionScreen> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Handle Nostr login
|
||||||
|
if (_useNostrLogin) {
|
||||||
|
final nostrKey = _nostrKeyController.text.trim();
|
||||||
|
|
||||||
|
if (nostrKey.isEmpty) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Please enter nsec or npub key'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate format
|
||||||
|
if (!nostrKey.startsWith('nsec') && !nostrKey.startsWith('npub')) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Invalid key format. Expected nsec or npub.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login with Nostr
|
||||||
|
await widget.sessionService!.loginWithNostr(nostrKey);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Nostr login successful'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
widget.onSessionChanged?.call();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Firebase or regular login
|
||||||
if (_useFirebaseAuth && widget.firebaseService != null) {
|
if (_useFirebaseAuth && widget.firebaseService != null) {
|
||||||
// Use Firebase Auth for authentication
|
// Use Firebase Auth for authentication
|
||||||
final email = _emailController.text.trim();
|
final email = _emailController.text.trim();
|
||||||
@@ -246,7 +293,56 @@ class _SessionScreenState extends State<SessionScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text('User ID: ${currentUser.id}'),
|
// Display Nostr profile if available
|
||||||
|
if (currentUser.nostrProfile != null) ...[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (currentUser.nostrProfile!.picture != null)
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 30,
|
||||||
|
backgroundImage: NetworkImage(
|
||||||
|
currentUser.nostrProfile!.picture!,
|
||||||
|
),
|
||||||
|
onBackgroundImageError: (_, __) {},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const CircleAvatar(
|
||||||
|
radius: 30,
|
||||||
|
child: Icon(Icons.person),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
currentUser.nostrProfile!.name ??
|
||||||
|
currentUser.nostrProfile!.displayName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (currentUser.nostrProfile!.about != null)
|
||||||
|
Text(
|
||||||
|
currentUser.nostrProfile!.about!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
],
|
||||||
|
Text('User ID: ${currentUser.id.substring(0, currentUser.id.length > 32 ? 32 : currentUser.id.length)}${currentUser.id.length > 32 ? '...' : ''}'),
|
||||||
Text('Username: ${currentUser.username}'),
|
Text('Username: ${currentUser.username}'),
|
||||||
Text(
|
Text(
|
||||||
'Created: ${DateTime.fromMillisecondsSinceEpoch(currentUser.createdAt).toString().split('.')[0]}',
|
'Created: ${DateTime.fromMillisecondsSinceEpoch(currentUser.createdAt).toString().split('.')[0]}',
|
||||||
@@ -306,7 +402,39 @@ class _SessionScreenState extends State<SessionScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
if (_useFirebaseAuth) ...[
|
// Login method selector
|
||||||
|
SegmentedButton<bool>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment<bool>(
|
||||||
|
value: false,
|
||||||
|
label: Text('Regular'),
|
||||||
|
),
|
||||||
|
ButtonSegment<bool>(
|
||||||
|
value: true,
|
||||||
|
label: Text('Nostr'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {_useNostrLogin},
|
||||||
|
onSelectionChanged: (Set<bool> newSelection) {
|
||||||
|
setState(() {
|
||||||
|
_useNostrLogin = newSelection.first;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (_useNostrLogin) ...[
|
||||||
|
TextField(
|
||||||
|
controller: _nostrKeyController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Nostr Key (nsec or npub)',
|
||||||
|
hintText: 'Enter your nsec or npub key',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
helperText: 'Enter your Nostr private key (nsec) or public key (npub)',
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
minLines: 1,
|
||||||
|
),
|
||||||
|
] else if (_useFirebaseAuth) ...[
|
||||||
TextField(
|
TextField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
|
|||||||
+82
-2
@@ -41,6 +41,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.0"
|
||||||
|
base58check:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: base58check
|
||||||
|
sha256: "6c300dfc33e598d2fe26319e13f6243fea81eaf8204cb4c6b69ef20a625319a5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
bech32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bech32
|
||||||
|
sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.2"
|
||||||
|
bip32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bip32
|
||||||
|
sha256: "54787cd7a111e9d37394aabbf53d1fc5e2e0e0af2cd01c459147a97c0e3f8a97"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
|
bip340:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bip340
|
||||||
|
sha256: "2a92f6ed68959f75d67c9a304c17928b9c9449587d4f75ee68f34152f7f69e87"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
bip39:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bip39
|
||||||
|
sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.6"
|
||||||
bloc:
|
bloc:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -65,6 +105,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
|
bs58check:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bs58check
|
||||||
|
sha256: c4a164d42b25c2f6bc88a8beccb9fc7d01440f3c60ba23663a20a70faf484ea9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.2"
|
||||||
build:
|
build:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -218,7 +266,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.15.0"
|
version: "1.15.0"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
@@ -456,6 +504,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.2"
|
version: "2.3.2"
|
||||||
|
hex:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hex
|
||||||
|
sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
http:
|
http:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -504,6 +560,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.9.0"
|
version: "4.9.0"
|
||||||
|
kepler:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: kepler
|
||||||
|
sha256: "8cf9f7df525bd4e5b192d91e52f1c75832b1fefb27fb4f4a09b1412b0f4f23d0"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -592,6 +656,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
nostr_tools:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: nostr_tools
|
||||||
|
sha256: a4c9a4ed938a63bfac8d4e5207b1f1958a0d2775bc41fe90cf6d963ac1dcda90
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.9"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -672,6 +744,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
pointycastle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pointycastle
|
||||||
|
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.9.1"
|
||||||
pool:
|
pool:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -942,7 +1022,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||||
|
|||||||
+1
-2
@@ -14,8 +14,7 @@ dependencies:
|
|||||||
path: ^1.8.3
|
path: ^1.8.3
|
||||||
http: ^1.2.0
|
http: ^1.2.0
|
||||||
dio: ^5.4.0
|
dio: ^5.4.0
|
||||||
crypto: ^3.0.3
|
nostr_tools: ^1.0.9
|
||||||
web_socket_channel: ^2.4.0
|
|
||||||
flutter_dotenv: ^5.1.0
|
flutter_dotenv: ^5.1.0
|
||||||
# Firebase dependencies (optional - can be disabled if not needed)
|
# Firebase dependencies (optional - can be disabled if not needed)
|
||||||
firebase_core: ^3.0.0
|
firebase_core: ^3.0.0
|
||||||
|
|||||||
@@ -207,11 +207,28 @@ void main() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
final mockDio = _createMockDio(
|
final mockDio = _createMockDio(
|
||||||
onGet: (path) => Response(
|
onPost: (path, data) {
|
||||||
|
if (path == '/api/search/metadata') {
|
||||||
|
// Return the correct nested response structure
|
||||||
|
return Response(
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
data: mockAssets,
|
data: {
|
||||||
|
'assets': {
|
||||||
|
'items': mockAssets,
|
||||||
|
'total': mockAssets.length,
|
||||||
|
'count': mockAssets.length,
|
||||||
|
'nextPage': null,
|
||||||
|
}
|
||||||
|
},
|
||||||
requestOptions: RequestOptions(path: path),
|
requestOptions: RequestOptions(path: path),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
return Response(
|
||||||
|
statusCode: 200,
|
||||||
|
data: {},
|
||||||
|
requestOptions: RequestOptions(path: path),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final immichService = ImmichService(
|
final immichService = ImmichService(
|
||||||
@@ -238,19 +255,18 @@ void main() {
|
|||||||
test('fetchAssets - pagination', () async {
|
test('fetchAssets - pagination', () async {
|
||||||
// Arrange
|
// Arrange
|
||||||
final mockDio = _createMockDio(
|
final mockDio = _createMockDio(
|
||||||
onGet: (path) {
|
onPost: (path, data) {
|
||||||
// Extract query parameters from request options
|
if (path == '/api/search/metadata') {
|
||||||
// The path might be just the endpoint, so we need to check request options
|
// Extract limit and skip from request body (data), not query params
|
||||||
final uri = path.startsWith('http')
|
final requestData = data as Map<String, dynamic>;
|
||||||
? Uri.parse(path)
|
final limit = requestData['limit'] as int? ?? 100;
|
||||||
: Uri.parse('https://immich.example.com$path');
|
final skip = requestData['skip'] as int? ?? 0;
|
||||||
final queryParams = uri.queryParameters;
|
|
||||||
final skip = int.parse(queryParams['skip'] ?? '0');
|
|
||||||
final limit = int.parse(queryParams['limit'] ?? '100');
|
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
data: List.generate(
|
data: {
|
||||||
|
'assets': {
|
||||||
|
'items': List.generate(
|
||||||
limit,
|
limit,
|
||||||
(i) => {
|
(i) => {
|
||||||
'id': 'asset-${skip + i}',
|
'id': 'asset-${skip + i}',
|
||||||
@@ -260,6 +276,17 @@ void main() {
|
|||||||
'mimeType': 'image/jpeg',
|
'mimeType': 'image/jpeg',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
'total': 100, // Mock total
|
||||||
|
'count': limit,
|
||||||
|
'nextPage': null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestOptions: RequestOptions(path: path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Response(
|
||||||
|
statusCode: 200,
|
||||||
|
data: {},
|
||||||
requestOptions: RequestOptions(path: path),
|
requestOptions: RequestOptions(path: path),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -287,11 +314,20 @@ void main() {
|
|||||||
test('fetchAssets - server error', () async {
|
test('fetchAssets - server error', () async {
|
||||||
// Arrange
|
// Arrange
|
||||||
final mockDio = _createMockDio(
|
final mockDio = _createMockDio(
|
||||||
onGet: (path) => Response(
|
onPost: (path, data) {
|
||||||
|
if (path == '/api/search/metadata') {
|
||||||
|
return Response(
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
data: {'error': 'Internal server error'},
|
data: {'error': 'Internal server error', 'message': 'Internal server error'},
|
||||||
requestOptions: RequestOptions(path: path),
|
requestOptions: RequestOptions(path: path),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
return Response(
|
||||||
|
statusCode: 200,
|
||||||
|
data: {},
|
||||||
|
requestOptions: RequestOptions(path: path),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final immichService = ImmichService(
|
final immichService = ImmichService(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:mockito/mockito.dart';
|
|||||||
import 'package:app_boilerplate/ui/navigation/main_navigation_scaffold.dart';
|
import 'package:app_boilerplate/ui/navigation/main_navigation_scaffold.dart';
|
||||||
import 'package:app_boilerplate/data/local/local_storage_service.dart';
|
import 'package:app_boilerplate/data/local/local_storage_service.dart';
|
||||||
import 'package:app_boilerplate/data/nostr/nostr_service.dart';
|
import 'package:app_boilerplate/data/nostr/nostr_service.dart';
|
||||||
|
import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart';
|
||||||
import 'package:app_boilerplate/data/sync/sync_engine.dart';
|
import 'package:app_boilerplate/data/sync/sync_engine.dart';
|
||||||
import 'package:app_boilerplate/data/session/session_service.dart';
|
import 'package:app_boilerplate/data/session/session_service.dart';
|
||||||
import 'package:app_boilerplate/data/firebase/firebase_service.dart';
|
import 'package:app_boilerplate/data/firebase/firebase_service.dart';
|
||||||
@@ -36,6 +37,11 @@ void main() {
|
|||||||
when(mockSessionService.isLoggedIn).thenReturn(false);
|
when(mockSessionService.isLoggedIn).thenReturn(false);
|
||||||
when(mockSessionService.currentUser).thenReturn(null);
|
when(mockSessionService.currentUser).thenReturn(null);
|
||||||
when(mockFirebaseService.isEnabled).thenReturn(false);
|
when(mockFirebaseService.isEnabled).thenReturn(false);
|
||||||
|
when(mockNostrService.getRelays()).thenReturn([]);
|
||||||
|
|
||||||
|
// Stub NostrService methods that might be called by UI
|
||||||
|
final mockKeyPair = NostrKeyPair.generate();
|
||||||
|
when(mockNostrService.generateKeyPair()).thenReturn(mockKeyPair);
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget createTestWidget() {
|
Widget createTestWidget() {
|
||||||
@@ -68,7 +74,8 @@ void main() {
|
|||||||
await tester.pumpWidget(createTestWidget());
|
await tester.pumpWidget(createTestWidget());
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Home'), findsOneWidget); // AppBar title
|
// Home text might appear in AppBar and body, so check for at least one
|
||||||
|
expect(find.text('Home'), findsWidgets);
|
||||||
expect(find.byIcon(Icons.home), findsWidgets);
|
expect(find.byIcon(Icons.home), findsWidgets);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,8 +83,8 @@ void main() {
|
|||||||
await tester.pumpWidget(createTestWidget());
|
await tester.pumpWidget(createTestWidget());
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
// Verify Home is shown initially
|
// Verify Home is shown initially (might appear multiple times)
|
||||||
expect(find.text('Home'), findsOneWidget);
|
expect(find.text('Home'), findsWidgets);
|
||||||
|
|
||||||
// Verify navigation structure allows switching
|
// Verify navigation structure allows switching
|
||||||
expect(find.byType(BottomNavigationBar), findsOneWidget);
|
expect(find.byType(BottomNavigationBar), findsOneWidget);
|
||||||
@@ -116,7 +123,7 @@ void main() {
|
|||||||
await tester.pumpWidget(createTestWidget());
|
await tester.pumpWidget(createTestWidget());
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect(find.text('Home'), findsOneWidget); // AppBar title
|
expect(find.text('Home'), findsWidgets); // AppBar title and/or body
|
||||||
expect(find.byType(Scaffold), findsWidgets);
|
expect(find.byType(Scaffold), findsWidgets);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,7 +135,7 @@ void main() {
|
|||||||
expect(find.byType(BottomNavigationBar), findsOneWidget);
|
expect(find.byType(BottomNavigationBar), findsOneWidget);
|
||||||
expect(find.byType(Scaffold), findsWidgets);
|
expect(find.byType(Scaffold), findsWidgets);
|
||||||
// IndexedStack is internal - verify it indirectly by checking scaffold renders
|
// IndexedStack is internal - verify it indirectly by checking scaffold renders
|
||||||
expect(find.text('Home'), findsOneWidget); // Home screen should be visible
|
expect(find.text('Home'), findsWidgets); // Home screen should be visible
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,24 @@
|
|||||||
import 'dart:async' as _i9;
|
import 'dart:async' as _i9;
|
||||||
import 'dart:io' as _i2;
|
import 'dart:io' as _i2;
|
||||||
|
|
||||||
import 'package:app_boilerplate/data/firebase/firebase_service.dart' as _i18;
|
import 'package:app_boilerplate/data/firebase/firebase_service.dart' as _i19;
|
||||||
import 'package:app_boilerplate/data/firebase/models/firebase_config.dart'
|
import 'package:app_boilerplate/data/firebase/models/firebase_config.dart'
|
||||||
as _i6;
|
as _i6;
|
||||||
import 'package:app_boilerplate/data/local/local_storage_service.dart' as _i7;
|
import 'package:app_boilerplate/data/local/local_storage_service.dart' as _i7;
|
||||||
import 'package:app_boilerplate/data/local/models/item.dart' as _i10;
|
import 'package:app_boilerplate/data/local/models/item.dart' as _i10;
|
||||||
import 'package:app_boilerplate/data/nostr/models/nostr_event.dart' as _i4;
|
import 'package:app_boilerplate/data/nostr/models/nostr_event.dart' as _i4;
|
||||||
import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart' as _i3;
|
import 'package:app_boilerplate/data/nostr/models/nostr_keypair.dart' as _i3;
|
||||||
|
import 'package:app_boilerplate/data/nostr/models/nostr_profile.dart' as _i13;
|
||||||
import 'package:app_boilerplate/data/nostr/models/nostr_relay.dart' as _i12;
|
import 'package:app_boilerplate/data/nostr/models/nostr_relay.dart' as _i12;
|
||||||
import 'package:app_boilerplate/data/nostr/nostr_service.dart' as _i11;
|
import 'package:app_boilerplate/data/nostr/nostr_service.dart' as _i11;
|
||||||
import 'package:app_boilerplate/data/session/models/user.dart' as _i5;
|
import 'package:app_boilerplate/data/session/models/user.dart' as _i5;
|
||||||
import 'package:app_boilerplate/data/session/session_service.dart' as _i17;
|
import 'package:app_boilerplate/data/session/session_service.dart' as _i18;
|
||||||
import 'package:app_boilerplate/data/sync/models/sync_operation.dart' as _i14;
|
import 'package:app_boilerplate/data/sync/models/sync_operation.dart' as _i15;
|
||||||
import 'package:app_boilerplate/data/sync/models/sync_status.dart' as _i15;
|
import 'package:app_boilerplate/data/sync/models/sync_status.dart' as _i16;
|
||||||
import 'package:app_boilerplate/data/sync/sync_engine.dart' as _i13;
|
import 'package:app_boilerplate/data/sync/sync_engine.dart' as _i14;
|
||||||
import 'package:firebase_auth/firebase_auth.dart' as _i8;
|
import 'package:firebase_auth/firebase_auth.dart' as _i8;
|
||||||
import 'package:mockito/mockito.dart' as _i1;
|
import 'package:mockito/mockito.dart' as _i1;
|
||||||
import 'package:mockito/src/dummies.dart' as _i16;
|
import 'package:mockito/src/dummies.dart' as _i17;
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: avoid_redundant_argument_values
|
// ignore_for_file: avoid_redundant_argument_values
|
||||||
@@ -377,6 +378,20 @@ class MockNostrService extends _i1.Mock implements _i11.NostrService {
|
|||||||
)),
|
)),
|
||||||
) as _i9.Future<_i4.NostrEvent>);
|
) as _i9.Future<_i4.NostrEvent>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i9.Future<_i13.NostrProfile?> fetchProfile(
|
||||||
|
String? publicKey, {
|
||||||
|
Duration? timeout = const Duration(seconds: 10),
|
||||||
|
}) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(
|
||||||
|
#fetchProfile,
|
||||||
|
[publicKey],
|
||||||
|
{#timeout: timeout},
|
||||||
|
),
|
||||||
|
returnValue: _i9.Future<_i13.NostrProfile?>.value(),
|
||||||
|
) as _i9.Future<_i13.NostrProfile?>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() => super.noSuchMethod(
|
void dispose() => super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
@@ -390,7 +405,7 @@ class MockNostrService extends _i1.Mock implements _i11.NostrService {
|
|||||||
/// A class which mocks [SyncEngine].
|
/// A class which mocks [SyncEngine].
|
||||||
///
|
///
|
||||||
/// See the documentation for Mockito's code generation for more information.
|
/// See the documentation for Mockito's code generation for more information.
|
||||||
class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
class MockSyncEngine extends _i1.Mock implements _i14.SyncEngine {
|
||||||
MockSyncEngine() {
|
MockSyncEngine() {
|
||||||
_i1.throwOnMissingStub(this);
|
_i1.throwOnMissingStub(this);
|
||||||
}
|
}
|
||||||
@@ -402,10 +417,10 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
) as int);
|
) as int);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_i9.Stream<_i14.SyncOperation> get statusStream => (super.noSuchMethod(
|
_i9.Stream<_i15.SyncOperation> get statusStream => (super.noSuchMethod(
|
||||||
Invocation.getter(#statusStream),
|
Invocation.getter(#statusStream),
|
||||||
returnValue: _i9.Stream<_i14.SyncOperation>.empty(),
|
returnValue: _i9.Stream<_i15.SyncOperation>.empty(),
|
||||||
) as _i9.Stream<_i14.SyncOperation>);
|
) as _i9.Stream<_i15.SyncOperation>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void setNostrKeyPair(_i3.NostrKeyPair? keypair) => super.noSuchMethod(
|
void setNostrKeyPair(_i3.NostrKeyPair? keypair) => super.noSuchMethod(
|
||||||
@@ -417,7 +432,7 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void setConflictResolution(_i15.ConflictResolution? strategy) =>
|
void setConflictResolution(_i16.ConflictResolution? strategy) =>
|
||||||
super.noSuchMethod(
|
super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
#setConflictResolution,
|
#setConflictResolution,
|
||||||
@@ -427,25 +442,25 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<_i14.SyncOperation> getPendingOperations() => (super.noSuchMethod(
|
List<_i15.SyncOperation> getPendingOperations() => (super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
#getPendingOperations,
|
#getPendingOperations,
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
returnValue: <_i14.SyncOperation>[],
|
returnValue: <_i15.SyncOperation>[],
|
||||||
) as List<_i14.SyncOperation>);
|
) as List<_i15.SyncOperation>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<_i14.SyncOperation> getAllOperations() => (super.noSuchMethod(
|
List<_i15.SyncOperation> getAllOperations() => (super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
#getAllOperations,
|
#getAllOperations,
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
returnValue: <_i14.SyncOperation>[],
|
returnValue: <_i15.SyncOperation>[],
|
||||||
) as List<_i14.SyncOperation>);
|
) as List<_i15.SyncOperation>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void queueOperation(_i14.SyncOperation? operation) => super.noSuchMethod(
|
void queueOperation(_i15.SyncOperation? operation) => super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
#queueOperation,
|
#queueOperation,
|
||||||
[operation],
|
[operation],
|
||||||
@@ -456,7 +471,7 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
@override
|
@override
|
||||||
_i9.Future<String> syncToImmich(
|
_i9.Future<String> syncToImmich(
|
||||||
String? itemId, {
|
String? itemId, {
|
||||||
_i15.SyncPriority? priority = _i15.SyncPriority.normal,
|
_i16.SyncPriority? priority = _i16.SyncPriority.normal,
|
||||||
}) =>
|
}) =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
@@ -464,7 +479,7 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
[itemId],
|
[itemId],
|
||||||
{#priority: priority},
|
{#priority: priority},
|
||||||
),
|
),
|
||||||
returnValue: _i9.Future<String>.value(_i16.dummyValue<String>(
|
returnValue: _i9.Future<String>.value(_i17.dummyValue<String>(
|
||||||
this,
|
this,
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
#syncToImmich,
|
#syncToImmich,
|
||||||
@@ -477,7 +492,7 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
@override
|
@override
|
||||||
_i9.Future<String> syncFromImmich(
|
_i9.Future<String> syncFromImmich(
|
||||||
String? assetId, {
|
String? assetId, {
|
||||||
_i15.SyncPriority? priority = _i15.SyncPriority.normal,
|
_i16.SyncPriority? priority = _i16.SyncPriority.normal,
|
||||||
}) =>
|
}) =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
@@ -485,7 +500,7 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
[assetId],
|
[assetId],
|
||||||
{#priority: priority},
|
{#priority: priority},
|
||||||
),
|
),
|
||||||
returnValue: _i9.Future<String>.value(_i16.dummyValue<String>(
|
returnValue: _i9.Future<String>.value(_i17.dummyValue<String>(
|
||||||
this,
|
this,
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
#syncFromImmich,
|
#syncFromImmich,
|
||||||
@@ -498,7 +513,7 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
@override
|
@override
|
||||||
_i9.Future<String> syncToNostr(
|
_i9.Future<String> syncToNostr(
|
||||||
String? itemId, {
|
String? itemId, {
|
||||||
_i15.SyncPriority? priority = _i15.SyncPriority.normal,
|
_i16.SyncPriority? priority = _i16.SyncPriority.normal,
|
||||||
}) =>
|
}) =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
@@ -506,7 +521,7 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
[itemId],
|
[itemId],
|
||||||
{#priority: priority},
|
{#priority: priority},
|
||||||
),
|
),
|
||||||
returnValue: _i9.Future<String>.value(_i16.dummyValue<String>(
|
returnValue: _i9.Future<String>.value(_i17.dummyValue<String>(
|
||||||
this,
|
this,
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
#syncToNostr,
|
#syncToNostr,
|
||||||
@@ -518,7 +533,7 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
_i9.Future<List<String>> syncAll(
|
_i9.Future<List<String>> syncAll(
|
||||||
{_i15.SyncPriority? priority = _i15.SyncPriority.normal}) =>
|
{_i16.SyncPriority? priority = _i16.SyncPriority.normal}) =>
|
||||||
(super.noSuchMethod(
|
(super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
#syncAll,
|
#syncAll,
|
||||||
@@ -575,7 +590,7 @@ class MockSyncEngine extends _i1.Mock implements _i13.SyncEngine {
|
|||||||
/// A class which mocks [SessionService].
|
/// A class which mocks [SessionService].
|
||||||
///
|
///
|
||||||
/// See the documentation for Mockito's code generation for more information.
|
/// See the documentation for Mockito's code generation for more information.
|
||||||
class MockSessionService extends _i1.Mock implements _i17.SessionService {
|
class MockSessionService extends _i1.Mock implements _i18.SessionService {
|
||||||
MockSessionService() {
|
MockSessionService() {
|
||||||
_i1.throwOnMissingStub(this);
|
_i1.throwOnMissingStub(this);
|
||||||
}
|
}
|
||||||
@@ -616,6 +631,22 @@ class MockSessionService extends _i1.Mock implements _i17.SessionService {
|
|||||||
)),
|
)),
|
||||||
) as _i9.Future<_i5.User>);
|
) as _i9.Future<_i5.User>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_i9.Future<_i5.User> loginWithNostr(String? nsecOrNpub) =>
|
||||||
|
(super.noSuchMethod(
|
||||||
|
Invocation.method(
|
||||||
|
#loginWithNostr,
|
||||||
|
[nsecOrNpub],
|
||||||
|
),
|
||||||
|
returnValue: _i9.Future<_i5.User>.value(_FakeUser_3(
|
||||||
|
this,
|
||||||
|
Invocation.method(
|
||||||
|
#loginWithNostr,
|
||||||
|
[nsecOrNpub],
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
) as _i9.Future<_i5.User>);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_i9.Future<void> logout({bool? clearCache = true}) => (super.noSuchMethod(
|
_i9.Future<void> logout({bool? clearCache = true}) => (super.noSuchMethod(
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
@@ -664,7 +695,7 @@ class MockSessionService extends _i1.Mock implements _i17.SessionService {
|
|||||||
/// A class which mocks [FirebaseService].
|
/// A class which mocks [FirebaseService].
|
||||||
///
|
///
|
||||||
/// See the documentation for Mockito's code generation for more information.
|
/// See the documentation for Mockito's code generation for more information.
|
||||||
class MockFirebaseService extends _i1.Mock implements _i18.FirebaseService {
|
class MockFirebaseService extends _i1.Mock implements _i19.FirebaseService {
|
||||||
MockFirebaseService() {
|
MockFirebaseService() {
|
||||||
_i1.throwOnMissingStub(this);
|
_i1.throwOnMissingStub(this);
|
||||||
}
|
}
|
||||||
@@ -780,7 +811,7 @@ class MockFirebaseService extends _i1.Mock implements _i18.FirebaseService {
|
|||||||
path,
|
path,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
returnValue: _i9.Future<String>.value(_i16.dummyValue<String>(
|
returnValue: _i9.Future<String>.value(_i17.dummyValue<String>(
|
||||||
this,
|
this,
|
||||||
Invocation.method(
|
Invocation.method(
|
||||||
#uploadFile,
|
#uploadFile,
|
||||||
|
|||||||
Reference in New Issue
Block a user