mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48:24 +00:00
Fix NWC secret management, zap anonymously
This commit is contained in:
@@ -16,14 +16,6 @@ class FileMetadata extends base.FileMetadata with DataModelMixin<FileMetadata> {
|
||||
@override
|
||||
Object? get id => event.id;
|
||||
|
||||
// FileMetadata(
|
||||
// {super.createdAt,
|
||||
// super.content,
|
||||
// super.tags,
|
||||
// required this.author,
|
||||
// required this.release,
|
||||
// required this.signer});
|
||||
|
||||
FileMetadata.fromJson(super.map)
|
||||
: author = belongsTo(map['author']),
|
||||
release = belongsTo(map['release']),
|
||||
|
||||
@@ -19,7 +19,7 @@ class User extends base.User with DataModelMixin<User> {
|
||||
Object? get id => event.id;
|
||||
|
||||
Future<void> zap(int amountInSats,
|
||||
{required base.Event event, String? comment}) async {
|
||||
{required base.Event event, String? comment, base.Signer? signer}) async {
|
||||
final adapter = DataModel.adapterFor(this) as NostrAdapter;
|
||||
|
||||
// First ensure connection is available
|
||||
@@ -44,8 +44,8 @@ class User extends base.User with DataModelMixin<User> {
|
||||
..amount = amountInMillisats
|
||||
..comment = comment;
|
||||
|
||||
final zapRequest =
|
||||
await partialZapRequest.signWith(amberSigner, withPubkey: pubkey);
|
||||
final zapRequest = await partialZapRequest.signWith(signer ?? amberSigner,
|
||||
withPubkey: pubkey);
|
||||
|
||||
// Now we fetch the invoice
|
||||
final lnResponse = await author.fetchLightningAddress();
|
||||
|
||||
@@ -6,12 +6,14 @@ import 'package:zapstore/main.dart';
|
||||
import 'package:zapstore/main.data.dart';
|
||||
import 'package:zapstore/models/app.dart';
|
||||
import 'package:zapstore/models/local_app.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
import 'package:zapstore/navigation/router.dart';
|
||||
import 'package:zapstore/utils/signers.dart';
|
||||
import 'package:zapstore/widgets/app_curation_container.dart';
|
||||
|
||||
AppLifecycleListener? _lifecycleListener;
|
||||
SharedPreferences? sharedPreferences;
|
||||
User? anonUser;
|
||||
final amberSigner = AmberSigner();
|
||||
|
||||
final appInitializer = FutureProvider<void>((ref) async {
|
||||
@@ -47,6 +49,18 @@ final appInitializer = FutureProvider<void>((ref) async {
|
||||
// Initialize signer
|
||||
await amberSigner.initialize();
|
||||
|
||||
// Set up anon user (pubkey derives from pkSigner secret key)
|
||||
final anonPubkey =
|
||||
'c86eda2daae768374526bc54903f388d9a866c00740ec8db418d7ef2dca77b5b';
|
||||
anonUser ??= User.fromJson({
|
||||
'id': anonPubkey,
|
||||
'kind': 0,
|
||||
'pubkey': anonPubkey,
|
||||
'created_at': DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
'content': '{}',
|
||||
'tags': [],
|
||||
}).init().saveLocal();
|
||||
|
||||
// App-wide listeners
|
||||
|
||||
// Register app lifecycle listener
|
||||
|
||||
@@ -156,12 +156,10 @@ class AppDetailScreen extends HookConsumerWidget {
|
||||
padding: const EdgeInsets.only(right: 14),
|
||||
child: SignerAndDeveloperRow(app: app),
|
||||
),
|
||||
Gap(20),
|
||||
if (app.latestMetadata != null)
|
||||
ZapReceipts(fileMetadata: app.latestMetadata!),
|
||||
Gap(20),
|
||||
Gap(10),
|
||||
ZapButton(app: app),
|
||||
Gap(20),
|
||||
ZapReceipts(app: app),
|
||||
Gap(10),
|
||||
Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:ndk/domain_layer/usecases/nwc/nwc_connection.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:purplebase/purplebase.dart';
|
||||
import 'package:zapstore/main.data.dart';
|
||||
@@ -70,8 +69,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
user.settings.value!.signInMethod == SignInMethod.nip55 &&
|
||||
amberSigner.isAvailable)
|
||||
AsyncButtonBuilder(
|
||||
loadingWidget: SizedBox(
|
||||
width: 14, height: 14, child: CircularProgressIndicator()),
|
||||
loadingWidget: SmallCircularProgressIndicator(),
|
||||
onPressed: () async {
|
||||
if (feedbackController.text.trim().isNotEmpty) {
|
||||
final signedDirectMessage = await amberSigner.sign(
|
||||
@@ -110,13 +108,19 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
},
|
||||
child: Text('Send as ${user.nameOrNpub}'),
|
||||
),
|
||||
Gap(40),
|
||||
Gap(20),
|
||||
Divider(),
|
||||
Gap(40),
|
||||
if (user != null) NostrWalletConnectContainer(),
|
||||
Gap(40),
|
||||
Gap(20),
|
||||
Text(
|
||||
'Wallet',
|
||||
style: context.theme.textTheme.headlineLarge!
|
||||
.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Gap(20),
|
||||
NwcContainer(),
|
||||
Gap(20),
|
||||
Divider(),
|
||||
Gap(40),
|
||||
Gap(20),
|
||||
Text(
|
||||
'Tools',
|
||||
style: context.theme.textTheme.headlineLarge!
|
||||
@@ -218,9 +222,20 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class NostrWalletConnectContainer extends HookConsumerWidget {
|
||||
class SmallCircularProgressIndicator extends StatelessWidget {
|
||||
const SmallCircularProgressIndicator({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(width: 14, height: 14, child: CircularProgressIndicator());
|
||||
}
|
||||
}
|
||||
|
||||
class NwcContainer extends HookConsumerWidget {
|
||||
final bool dialogMode;
|
||||
const NostrWalletConnectContainer({
|
||||
const NwcContainer({
|
||||
super.key,
|
||||
this.dialogMode = false,
|
||||
});
|
||||
@@ -231,14 +246,25 @@ class NostrWalletConnectContainer extends HookConsumerWidget {
|
||||
final controller = useTextEditingController(text: '');
|
||||
final isTextFieldEmpty = useState(true);
|
||||
|
||||
if (nwcConnection.isLoading) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: SmallCircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Gap(20),
|
||||
Text(
|
||||
"Nostr Wallet Connect ${nwcConnection.value != null ? 'connected' : 'URI'}"),
|
||||
if (nwcConnection.value != null && !dialogMode)
|
||||
if (!nwcConnection.hasError)
|
||||
Text(
|
||||
"Nostr Wallet Connect ${nwcConnection.hasValue ? 'connected' : 'URI'}"),
|
||||
if (nwcConnection.hasError)
|
||||
Text('Error connecting: ${nwcConnection.error}'),
|
||||
if (nwcConnection.hasValue &&
|
||||
nwcConnection.value != null &&
|
||||
!dialogMode)
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await ref.read(nwcConnectionProvider.notifier).disconnectWallet();
|
||||
@@ -248,9 +274,11 @@ class NostrWalletConnectContainer extends HookConsumerWidget {
|
||||
backgroundColor: Colors.transparent),
|
||||
child: Text('Disconnect'),
|
||||
),
|
||||
if (nwcConnection.value == null)
|
||||
if ((nwcConnection.hasValue && nwcConnection.value == null) ||
|
||||
nwcConnection.hasError)
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.none,
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
isTextFieldEmpty.value = value.isEmpty;
|
||||
@@ -259,11 +287,7 @@ class NostrWalletConnectContainer extends HookConsumerWidget {
|
||||
hintText: 'nostr+walletconnect://...',
|
||||
suffixIcon: AsyncButtonBuilder(
|
||||
disabled: isTextFieldEmpty.value,
|
||||
loadingWidget: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
loadingWidget: SmallCircularProgressIndicator(),
|
||||
onPressed: () async {
|
||||
final nwcUri = controller.text.trim();
|
||||
await ref
|
||||
|
||||
@@ -135,4 +135,10 @@ extension StringWidget on String {
|
||||
}
|
||||
}
|
||||
|
||||
extension TextExt on Text {
|
||||
Widget get bold {
|
||||
return Text(data!, style: TextStyle(fontWeight: FontWeight.bold));
|
||||
}
|
||||
}
|
||||
|
||||
const kZapstoreAppIdentifier = 'dev.zapstore.app';
|
||||
|
||||
+12
-12
@@ -44,18 +44,18 @@ class NwcConnectionNotifier extends StateNotifier<AsyncValue<NwcConnection?>> {
|
||||
// Write secret to storage and connect
|
||||
await _storage.write(key: kNwcSecretKey, value: nwcSecret);
|
||||
|
||||
final connection = await ndk.nwc.connect(
|
||||
nwcSecret,
|
||||
doGetInfoMethod: false,
|
||||
onError: (error) {
|
||||
state =
|
||||
AsyncValue.error(error ?? 'Could not connect', StackTrace.current);
|
||||
},
|
||||
);
|
||||
if (connection.permissions.contains(NwcMethod.PAY_INVOICE.name)) {
|
||||
state = AsyncValue.data(connection);
|
||||
} else {
|
||||
state = AsyncError('No permission to zap', StackTrace.current);
|
||||
try {
|
||||
final connection = await ndk.nwc.connect(
|
||||
nwcSecret,
|
||||
doGetInfoMethod: false,
|
||||
);
|
||||
if (connection.permissions.contains(NwcMethod.PAY_INVOICE.name)) {
|
||||
state = AsyncValue.data(connection);
|
||||
} else {
|
||||
state = AsyncError('No permission to zap', StackTrace.current);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
state = AsyncValue.error(e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:zapstore/main.data.dart';
|
||||
import 'package:zapstore/models/app.dart';
|
||||
import 'package:zapstore/screens/settings_screen.dart';
|
||||
import 'package:zapstore/utils/notifier.dart';
|
||||
import 'package:zapstore/widgets/app_card.dart';
|
||||
|
||||
@@ -51,11 +52,7 @@ class LatestReleasesContainer extends HookConsumerWidget {
|
||||
for (final app in state.value!) AppCard(model: app),
|
||||
if (state.hasValue)
|
||||
AsyncButtonBuilder(
|
||||
loadingWidget: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
loadingWidget: SmallCircularProgressIndicator(),
|
||||
onPressed: () async {
|
||||
return ref
|
||||
.read(latestReleasesAppProvider.notifier)
|
||||
|
||||
@@ -9,13 +9,21 @@ import 'package:zapstore/main.data.dart';
|
||||
import 'package:zapstore/models/settings.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
import 'package:zapstore/navigation/app_initializer.dart';
|
||||
import 'package:zapstore/screens/settings_screen.dart';
|
||||
import 'package:zapstore/utils/extensions.dart';
|
||||
import 'package:zapstore/widgets/rounded_image.dart';
|
||||
|
||||
class SignInButton extends ConsumerWidget {
|
||||
final bool minimal;
|
||||
final String label;
|
||||
SignInButton({super.key, this.minimal = false, this.label = 'Sign in'});
|
||||
final bool publicKeyAllowed;
|
||||
final String? signedOutText;
|
||||
SignInButton(
|
||||
{super.key,
|
||||
this.minimal = false,
|
||||
this.label = 'Sign in',
|
||||
this.publicKeyAllowed = true,
|
||||
this.signedOutText});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -28,6 +36,7 @@ class SignInButton extends ConsumerWidget {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (user == null && signedOutText != null) Text(signedOutText!),
|
||||
Row(
|
||||
children: [
|
||||
if (!minimal) RoundedImage(url: user?.avatarUrl, size: 46),
|
||||
@@ -67,7 +76,9 @@ class SignInButton extends ConsumerWidget {
|
||||
label,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: SignInDialogBox(),
|
||||
content: SignInDialogBox(
|
||||
publicKeyAllowed: publicKeyAllowed,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -102,11 +113,7 @@ class SignInDialogBox extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AsyncButtonBuilder(
|
||||
loadingWidget: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
loadingWidget: SmallCircularProgressIndicator(),
|
||||
builder: (context, child, callback, buttonState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
@@ -183,11 +190,7 @@ class SignInDialogBox extends HookConsumerWidget {
|
||||
Gap(10),
|
||||
AsyncButtonBuilder(
|
||||
disabled: isTextFieldEmpty.value,
|
||||
loadingWidget: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
loadingWidget: SmallCircularProgressIndicator(),
|
||||
onPressed: () async {
|
||||
try {
|
||||
final input = controller.text.trim();
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:purplebase/purplebase.dart' as base;
|
||||
import 'package:zapstore/main.data.dart';
|
||||
import 'package:zapstore/models/settings.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
import 'package:zapstore/screens/settings_screen.dart';
|
||||
import 'package:zapstore/widgets/users_rich_text.dart';
|
||||
|
||||
class WebOfTrustContainer extends HookConsumerWidget {
|
||||
@@ -55,11 +56,7 @@ class WebOfTrustContainer extends HookConsumerWidget {
|
||||
children: [
|
||||
Text('Loading web of trust...'),
|
||||
Gap(10),
|
||||
SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
SmallCircularProgressIndicator(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
+45
-29
@@ -4,8 +4,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:zapstore/main.data.dart';
|
||||
import 'package:zapstore/models/app.dart';
|
||||
import 'package:zapstore/models/settings.dart';
|
||||
import 'package:zapstore/navigation/app_initializer.dart';
|
||||
import 'package:zapstore/screens/settings_screen.dart';
|
||||
import 'package:zapstore/utils/extensions.dart';
|
||||
import 'package:zapstore/utils/signers.dart';
|
||||
import 'package:zapstore/widgets/sign_in_container.dart';
|
||||
|
||||
import '../utils/nwc.dart';
|
||||
@@ -32,32 +34,29 @@ class ZapButton extends HookConsumerWidget {
|
||||
return app.developer.value?.lud16 != null
|
||||
? ElevatedButton(
|
||||
onPressed: () async {
|
||||
final loggedInUser = user ??
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(
|
||||
'Sign in to zap',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: SignInDialogBox(publicKeyAllowed: false),
|
||||
),
|
||||
);
|
||||
// final loggedInUser = user ??
|
||||
// await showDialog(
|
||||
// context: context,
|
||||
// builder: (_) => AlertDialog(
|
||||
// title: Text(
|
||||
// 'Sign in to zap',
|
||||
// style: TextStyle(fontWeight: FontWeight.bold),
|
||||
// ),
|
||||
// content: SignInDialogBox(publicKeyAllowed: false),
|
||||
// ),
|
||||
// );
|
||||
|
||||
if (loggedInUser == null) {
|
||||
return;
|
||||
}
|
||||
// if (loggedInUser == null) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (nwcConnection.value == null) {
|
||||
await showDialog(
|
||||
// ignore: use_build_context_synchronously
|
||||
context: context,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(
|
||||
'Connect your wallet to zap',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: NostrWalletConnectContainer(dialogMode: true),
|
||||
title: Text('Connect your wallet').bold,
|
||||
content: NwcContainer(dialogMode: true),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -71,10 +70,17 @@ class ZapButton extends HookConsumerWidget {
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text('Choose zap amount'),
|
||||
title: Text('Choose zap amount').bold,
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (user == null)
|
||||
SignInButton(
|
||||
publicKeyAllowed: false,
|
||||
minimal: true,
|
||||
signedOutText:
|
||||
'⚠️ If you do not sign in, you will be zapping anonymously',
|
||||
),
|
||||
TextField(
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
@@ -83,24 +89,22 @@ class ZapButton extends HookConsumerWidget {
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(21),
|
||||
child: Text('⚡ 21'),
|
||||
child: Text('⚡️ 21'),
|
||||
),
|
||||
Gap(10),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(210),
|
||||
child: Text('⚡ 210'),
|
||||
child: Text('⚡️ 210'),
|
||||
),
|
||||
Gap(10),
|
||||
ElevatedButton(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(2100),
|
||||
child: Text('⚡ 2100'),
|
||||
child: Text('⚡️ 2100'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -115,7 +119,7 @@ class ZapButton extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: Text('Zap'),
|
||||
child: Text('Zap').bold,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(
|
||||
int.tryParse(amountController.text) ?? 0);
|
||||
@@ -132,7 +136,12 @@ class ZapButton extends HookConsumerWidget {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.showInfo('Zap sent!',
|
||||
description: '$amount sats on their way');
|
||||
await loggedInUser.zap(amount, event: app.latestMetadata!);
|
||||
if (user != null) {
|
||||
await user.zap(amount, event: app.latestMetadata!);
|
||||
} else {
|
||||
await anonUser!.zap(amount,
|
||||
event: app.latestMetadata!, signer: pkSigner);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.showError(
|
||||
@@ -140,7 +149,14 @@ class ZapButton extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text('Zap!'),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey[800]),
|
||||
child: Text(
|
||||
'⚡️ Zap this release',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15),
|
||||
),
|
||||
)
|
||||
: Container();
|
||||
}
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_data/flutter_data.dart';
|
||||
import 'package:ndk/domain_layer/entities/filter.dart';
|
||||
import 'package:ndk/domain_layer/entities/nip_01_event.dart';
|
||||
import 'package:zapstore/main.data.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:zapstore/models/app.dart';
|
||||
import 'package:zapstore/models/file_metadata.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
import 'package:zapstore/models/zap_receipt.dart';
|
||||
import 'package:zapstore/utils/extensions.dart';
|
||||
import 'package:zapstore/widgets/users_rich_text.dart';
|
||||
|
||||
class ZapReceipts extends HookConsumerWidget {
|
||||
ZapReceipts({
|
||||
super.key,
|
||||
required this.fileMetadata,
|
||||
required this.app,
|
||||
});
|
||||
|
||||
final FileMetadata fileMetadata;
|
||||
final App app;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final fileMetadata = app.latestMetadata;
|
||||
if (fileMetadata == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final zapReceipts = ref.watch(zapReceiptsNotifier(fileMetadata));
|
||||
|
||||
if (zapReceipts.isLoading) {
|
||||
@@ -38,33 +47,63 @@ class ZapReceipts extends HookConsumerWidget {
|
||||
final senderIds = receipts.map((r) => r.senderPubkey).toSet();
|
||||
final senders = ref.users.findManyLocalByIds(senderIds);
|
||||
|
||||
return UsersRichText(
|
||||
preSpan: TextSpan(text: '⚡ $totalAmountInSats sats by'),
|
||||
trailingText: ' and others',
|
||||
users: senders,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 10, bottom: 10),
|
||||
child: UsersRichText(
|
||||
preSpan: TextSpan(
|
||||
text: '⚡ $totalAmountInSats sats (${receipts.length} zaps) by'),
|
||||
trailingText: ' and others',
|
||||
users: senders,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ZapReceiptsNotifier extends StateNotifier<AsyncValue<Set<ZapReceipt>>> {
|
||||
Ref ref;
|
||||
FileMetadata fileMetadata;
|
||||
final FileMetadata fileMetadata;
|
||||
StreamSubscription<Nip01Event>? sub;
|
||||
|
||||
ZapReceiptsNotifier(this.ref, this.fileMetadata)
|
||||
: super(AsyncData(ref.zapReceipts.zapReceiptAdapter
|
||||
.findByRecipient(
|
||||
pubkey: fileMetadata.author.id!.toString(),
|
||||
eventId: fileMetadata.id!.toString())
|
||||
.toSet())) {
|
||||
ZapReceiptsNotifier(this.ref, this.fileMetadata) : super(AsyncLoading()) {
|
||||
final adapter = ref.users.nostrAdapter;
|
||||
|
||||
final recipient =
|
||||
final developerPubkey =
|
||||
fileMetadata.event.getTag('zap') ?? fileMetadata.event.pubkey;
|
||||
print('querying for $recipient and id ${fileMetadata.event.id}');
|
||||
final receiptsResponse = adapter.socialRelays.ndk!.zaps
|
||||
.subscribeToZapReceipts(
|
||||
pubKey: recipient, eventId: fileMetadata.event.id);
|
||||
|
||||
// If it is a Zapstore-signed release from the indexer, return
|
||||
if (developerPubkey == kZapstorePubkey &&
|
||||
fileMetadata.release.value!.app.value!.identifier !=
|
||||
kZapstoreAppIdentifier) {
|
||||
state = AsyncData({});
|
||||
return;
|
||||
}
|
||||
|
||||
final localReceipts = ref.zapReceipts.zapReceiptAdapter
|
||||
.findByRecipient(
|
||||
pubkey: developerPubkey, eventId: fileMetadata.id!.toString())
|
||||
.toSet();
|
||||
state = AsyncData(localReceipts);
|
||||
|
||||
print('querying for $developerPubkey and id ${fileMetadata.event.id}');
|
||||
|
||||
final latestReceiptTimestamp = localReceipts
|
||||
.sortedBy((z) => z.event.createdAt)
|
||||
.lastOrNull
|
||||
?.event
|
||||
.createdAt
|
||||
.millisecondsSinceEpoch;
|
||||
|
||||
final receiptsResponse = adapter.socialRelays.ndk!.requests.subscription(
|
||||
filters: [
|
||||
Filter(
|
||||
kinds: [9735],
|
||||
since: latestReceiptTimestamp != null
|
||||
? latestReceiptTimestamp ~/ 1000 + 1
|
||||
: null,
|
||||
eTags: [fileMetadata.event.id],
|
||||
pTags: [developerPubkey],
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
sub = receiptsResponse.stream.listen((data) {
|
||||
final model = ZapReceipt.fromJson(data.toJson()).init().saveLocal();
|
||||
|
||||
Reference in New Issue
Block a user