mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Zap UX improvements
This commit is contained in:
+22
-24
@@ -22,13 +22,17 @@ class User extends base.User with DataModelMixin<User> {
|
||||
{required base.Event event, String? comment}) async {
|
||||
final adapter = DataModel.adapterFor(this) as NostrAdapter;
|
||||
|
||||
// First ensure connection is available
|
||||
final nwcConnection = adapter.ref.read(nwcConnectionProvider).asData?.value;
|
||||
if (nwcConnection == null) {
|
||||
throw 'No NWC connection';
|
||||
}
|
||||
|
||||
final authorPubkey = event.event.pubkey;
|
||||
print(
|
||||
'bro wants to zap $amountInSats to $authorPubkey for ${event.event.id}');
|
||||
final author = adapter.ref.users.findOneLocalById(authorPubkey);
|
||||
|
||||
if (author?.lud16 == null) {
|
||||
throw 'cant zap';
|
||||
throw 'Recipient has no Lightning address';
|
||||
}
|
||||
|
||||
final amountInMillisats = amountInSats * 1000;
|
||||
@@ -44,27 +48,27 @@ class User extends base.User with DataModelMixin<User> {
|
||||
await partialZapRequest.signWith(amberSigner, withPubkey: pubkey);
|
||||
|
||||
// Now we fetch the invoice
|
||||
final lnurlResponse = await author.fetchLnUrl();
|
||||
final lnResponse = await author.fetchLightningAddress();
|
||||
|
||||
final recipientAllowsNostr = lnurlResponse['allowsNostr'];
|
||||
final recipientPubkey = lnurlResponse['nostrPubkey'];
|
||||
final recipientAllowsNostr = lnResponse['allowsNostr'];
|
||||
final recipientPubkey = lnResponse['nostrPubkey'];
|
||||
// All amounts are in millisats
|
||||
final recipientMin = lnurlResponse['minSendable'];
|
||||
final recipientMax = lnurlResponse['maxSendable'];
|
||||
final recipientMin = lnResponse['minSendable'];
|
||||
final recipientMax = lnResponse['maxSendable'];
|
||||
|
||||
if (recipientAllowsNostr != true && recipientPubkey != null) {
|
||||
throw 'cant zap, recipient does not allow nostr';
|
||||
throw 'Recipient does not allow nostr';
|
||||
}
|
||||
if ((recipientMin != null && amountInMillisats < recipientMin) ||
|
||||
(recipientMax != null && amountInMillisats > recipientMax)) {
|
||||
throw 'cant zap, amount not between min and max sendable';
|
||||
throw 'Amount not between min and max sendable';
|
||||
}
|
||||
|
||||
final commentLength = lnurlResponse['commentAllowed'];
|
||||
final commentLength = lnResponse['commentAllowed'];
|
||||
comment =
|
||||
commentLength != null ? comment?.substringMax(commentLength) : comment;
|
||||
|
||||
var callbackUri = Uri.parse(lnurlResponse['callback']!);
|
||||
var callbackUri = Uri.parse(lnResponse['callback']!);
|
||||
callbackUri = callbackUri.replace(
|
||||
queryParameters: {
|
||||
...callbackUri.queryParameters,
|
||||
@@ -74,26 +78,20 @@ class User extends base.User with DataModelMixin<User> {
|
||||
},
|
||||
);
|
||||
|
||||
final response = await http.get(callbackUri);
|
||||
final invoiceMap = jsonDecode(response.body);
|
||||
final invoiceResponse = await http.get(callbackUri);
|
||||
final invoiceMap = jsonDecode(invoiceResponse.body);
|
||||
|
||||
final invoice = invoiceMap['pr'];
|
||||
if (invoice == null) {
|
||||
throw 'cant zap, no invoice';
|
||||
throw 'No invoice';
|
||||
}
|
||||
|
||||
final nwcConnection = adapter.ref.read(nwcConnectionProvider).asData?.value;
|
||||
if (nwcConnection == null) {
|
||||
throw 'cant zap, no nwc connection';
|
||||
}
|
||||
|
||||
// Pay the invoice via NWC
|
||||
final rw = await adapter.socialRelays.ndk!.nwc.payInvoice(nwcConnection,
|
||||
// Pay the invoice via NWC (errors are thrown)
|
||||
await adapter.socialRelays.ndk!.nwc.payInvoice(nwcConnection,
|
||||
invoice: invoice, timeout: Duration(seconds: 20));
|
||||
print(rw);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> fetchLnUrl() async {
|
||||
Future<Map<String, dynamic>> fetchLightningAddress() async {
|
||||
final [userName, domainName] = lud16!.split('@');
|
||||
final lnurl = 'https://$domainName/.well-known/lnurlp/$userName';
|
||||
final response = await http.get(Uri.parse(lnurl));
|
||||
|
||||
@@ -16,8 +16,8 @@ import 'package:zapstore/widgets/install_button.dart';
|
||||
import 'package:zapstore/widgets/release_card.dart';
|
||||
import 'package:zapstore/widgets/signer_and_developer_row.dart';
|
||||
import 'package:zapstore/widgets/spinning_logo.dart';
|
||||
import 'package:zapstore/widgets/users_rich_text.dart';
|
||||
import 'package:zapstore/widgets/versioned_app_header.dart';
|
||||
import 'package:zapstore/widgets/wot_container.dart';
|
||||
import 'package:zapstore/widgets/zap_button.dart';
|
||||
import 'package:zapstore/widgets/zap_receipts.dart';
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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';
|
||||
@@ -29,15 +30,11 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
final systemInfoState = ref.watch(systemInfoNotifierProvider);
|
||||
|
||||
final feedbackController = useTextEditingController();
|
||||
final nwcController = useTextEditingController();
|
||||
final user = ref.settings
|
||||
.watchOne('_', alsoWatch: (_) => {_.user})
|
||||
.model!
|
||||
.user
|
||||
.value;
|
||||
final isTextFieldEmpty = useState(true);
|
||||
|
||||
final nwcConnection = ref.watch(nwcConnectionProvider);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
@@ -116,67 +113,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
Gap(40),
|
||||
Divider(),
|
||||
Gap(40),
|
||||
if (user != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Gap(20),
|
||||
Text(
|
||||
"Nostr Wallet Connect ${nwcConnection.value != null ? 'connected' : 'URI'}"),
|
||||
if (nwcConnection.value != null)
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(nwcConnectionProvider.notifier)
|
||||
.disconnectWallet();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
disabledBackgroundColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent),
|
||||
child: Text('Disconnect'),
|
||||
),
|
||||
if (nwcConnection.value == null)
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
controller: nwcController,
|
||||
onChanged: (value) {
|
||||
isTextFieldEmpty.value = value.isEmpty;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'nostr+walletconnect://...',
|
||||
suffixIcon: AsyncButtonBuilder(
|
||||
disabled: isTextFieldEmpty.value,
|
||||
loadingWidget: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
onPressed: () async {
|
||||
final nwcUri = nwcController.text.trim();
|
||||
await ref
|
||||
.read(nwcConnectionProvider.notifier)
|
||||
.connectWallet(nwcUri);
|
||||
},
|
||||
builder: (context, child, callback, buttonState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: callback,
|
||||
style: ElevatedButton.styleFrom(
|
||||
disabledBackgroundColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text('Connect'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (user != null) NostrWalletConnectContainer(),
|
||||
Gap(40),
|
||||
Divider(),
|
||||
Gap(40),
|
||||
@@ -280,3 +217,81 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NostrWalletConnectContainer extends HookConsumerWidget {
|
||||
final bool dialogMode;
|
||||
const NostrWalletConnectContainer({
|
||||
super.key,
|
||||
this.dialogMode = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final nwcConnection = ref.watch(nwcConnectionProvider);
|
||||
final controller = useTextEditingController(text: '');
|
||||
final isTextFieldEmpty = useState(true);
|
||||
|
||||
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)
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await ref.read(nwcConnectionProvider.notifier).disconnectWallet();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
disabledBackgroundColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent),
|
||||
child: Text('Disconnect'),
|
||||
),
|
||||
if (nwcConnection.value == null)
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
isTextFieldEmpty.value = value.isEmpty;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'nostr+walletconnect://...',
|
||||
suffixIcon: AsyncButtonBuilder(
|
||||
disabled: isTextFieldEmpty.value,
|
||||
loadingWidget: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
onPressed: () async {
|
||||
final nwcUri = controller.text.trim();
|
||||
await ref
|
||||
.read(nwcConnectionProvider.notifier)
|
||||
.connectWallet(nwcUri);
|
||||
if (dialogMode && context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
builder: (context, child, callback, buttonState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: callback,
|
||||
style: ElevatedButton.styleFrom(
|
||||
disabledBackgroundColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text('Connect'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-12
@@ -2,15 +2,21 @@ import 'package:flutter_data/flutter_data.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:ndk/domain_layer/usecases/nwc/consts/nwc_method.dart';
|
||||
import 'package:ndk/ndk.dart';
|
||||
import 'package:zapstore/main.data.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
|
||||
const kNwcSecretKey = 'nwc_secret';
|
||||
|
||||
class NwcConnectionNotifier extends StateNotifier<AsyncValue<NwcConnection?>> {
|
||||
Ref ref;
|
||||
late Ndk ndk;
|
||||
|
||||
final _storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
|
||||
NwcConnectionNotifier() : super(AsyncData(null)) {
|
||||
NwcConnectionNotifier(this.ref) : super(AsyncLoading()) {
|
||||
ndk = ref.users.nostrAdapter.socialRelays.ndk!;
|
||||
// Attempt to load from local storage on initialization
|
||||
_storage
|
||||
.read(key: kNwcSecretKey)
|
||||
@@ -32,13 +38,13 @@ class NwcConnectionNotifier extends StateNotifier<AsyncValue<NwcConnection?>> {
|
||||
// If secret is supplied and a connection is still active,
|
||||
// it means it is a new connection string so disconnect NWC
|
||||
if (state.value != null) {
|
||||
await ndkForNwc.nwc.disconnect(state.value!);
|
||||
await ndk.nwc.disconnect(state.value!);
|
||||
}
|
||||
|
||||
// Write secret to storage and connect
|
||||
await _storage.write(key: kNwcSecretKey, value: nwcSecret);
|
||||
|
||||
final connection = await ndkForNwc.nwc.connect(
|
||||
final connection = await ndk.nwc.connect(
|
||||
nwcSecret,
|
||||
doGetInfoMethod: false,
|
||||
onError: (error) {
|
||||
@@ -61,12 +67,4 @@ class NwcConnectionNotifier extends StateNotifier<AsyncValue<NwcConnection?>> {
|
||||
|
||||
final nwcConnectionProvider =
|
||||
StateNotifierProvider<NwcConnectionNotifier, AsyncValue<NwcConnection?>>(
|
||||
(ref) => NwcConnectionNotifier());
|
||||
|
||||
final ndkForNwc = Ndk(
|
||||
NdkConfig(
|
||||
cache: MemCacheManager(),
|
||||
eventVerifier: Bip340EventVerifier(),
|
||||
bootstrapRelays: [],
|
||||
logLevel: LogLevels().info),
|
||||
);
|
||||
(ref) => NwcConnectionNotifier(ref));
|
||||
|
||||
@@ -67,7 +67,7 @@ class SignInButton extends ConsumerWidget {
|
||||
label,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: SignInContainer(),
|
||||
content: SignInDialogBox(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
@@ -81,8 +81,9 @@ class SignInButton extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class SignInContainer extends HookConsumerWidget {
|
||||
const SignInContainer({super.key});
|
||||
class SignInDialogBox extends HookConsumerWidget {
|
||||
final bool publicKeyAllowed;
|
||||
const SignInDialogBox({super.key, this.publicKeyAllowed = true});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -155,73 +156,81 @@ class SignInContainer extends HookConsumerWidget {
|
||||
settings.saveLocal();
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop(user);
|
||||
}
|
||||
},
|
||||
child: Text('Sign in with Amber'),
|
||||
),
|
||||
Gap(10),
|
||||
Text('or'),
|
||||
Gap(10),
|
||||
],
|
||||
),
|
||||
Text('Input an npub or nostr address\n(read-only):'),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
isTextFieldEmpty.value = value.isEmpty;
|
||||
},
|
||||
),
|
||||
Gap(10),
|
||||
AsyncButtonBuilder(
|
||||
disabled: isTextFieldEmpty.value,
|
||||
loadingWidget: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(),
|
||||
if (amberSigner.isAvailable && publicKeyAllowed)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text('or'),
|
||||
),
|
||||
onPressed: () async {
|
||||
try {
|
||||
final input = controller.text.trim();
|
||||
if (input.startsWith('nsec')) {
|
||||
controller.clear();
|
||||
throw Exception('Never give away your nsec!');
|
||||
}
|
||||
final user = await ref.users.findOne(input);
|
||||
|
||||
final settings = ref.settings.findOneLocalById('_')!;
|
||||
settings.signInMethod = SignInMethod.pubkey;
|
||||
settings.user.value = user;
|
||||
settings.saveLocal();
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} on Exception catch (e, stack) {
|
||||
if (context.mounted) {
|
||||
context.showError(
|
||||
title: e.toString(),
|
||||
description: stack.toString().substringMax(200));
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, child, callback, buttonState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: callback,
|
||||
style: ElevatedButton.styleFrom(
|
||||
disabledBackgroundColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent),
|
||||
child: child,
|
||||
),
|
||||
if (publicKeyAllowed)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Input an npub or nostr address\n(read-only):'),
|
||||
TextField(
|
||||
autocorrect: false,
|
||||
controller: controller,
|
||||
onChanged: (value) {
|
||||
isTextFieldEmpty.value = value.isEmpty;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text('Sign in with public key'),
|
||||
)
|
||||
Gap(10),
|
||||
AsyncButtonBuilder(
|
||||
disabled: isTextFieldEmpty.value,
|
||||
loadingWidget: SizedBox(
|
||||
width: 14,
|
||||
height: 14,
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
onPressed: () async {
|
||||
try {
|
||||
final input = controller.text.trim();
|
||||
if (input.startsWith('nsec')) {
|
||||
controller.clear();
|
||||
throw Exception('Never give away your nsec!');
|
||||
}
|
||||
final user = await ref.users.findOne(input);
|
||||
|
||||
final settings = ref.settings.findOneLocalById('_')!;
|
||||
settings.signInMethod = SignInMethod.pubkey;
|
||||
settings.user.value = user;
|
||||
settings.saveLocal();
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} on Exception catch (e, stack) {
|
||||
if (context.mounted) {
|
||||
context.showError(
|
||||
title: e.toString(),
|
||||
description: stack.toString().substringMax(200));
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, child, callback, buttonState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: SizedBox(
|
||||
child: ElevatedButton(
|
||||
onPressed: callback,
|
||||
style: ElevatedButton.styleFrom(
|
||||
disabledBackgroundColor: Colors.transparent,
|
||||
backgroundColor: Colors.transparent),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text('Sign in with public key'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
import 'package:zapstore/widgets/rounded_image.dart';
|
||||
|
||||
class UsersRichText extends StatelessWidget {
|
||||
const UsersRichText({
|
||||
super.key,
|
||||
this.preSpan,
|
||||
this.trailingText,
|
||||
required this.users,
|
||||
});
|
||||
|
||||
final TextSpan? preSpan;
|
||||
final String? trailingText;
|
||||
final List<User> users;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
if (preSpan != null) preSpan!,
|
||||
for (final user in users)
|
||||
TextSpan(
|
||||
style: TextStyle(height: 1.6),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
RoundedImage(url: user.avatarUrl, size: 20),
|
||||
Text(
|
||||
' ${user.nameOrNpub}',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(users.indexOf(user) == users.length - 1
|
||||
? ''
|
||||
: (users.indexOf(user) == users.length - 2
|
||||
? ' and '
|
||||
: ', ')),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (users.indexOf(user) == users.length - 1)
|
||||
TextSpan(text: trailingText, style: TextStyle(fontSize: 15))
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +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/widgets/rounded_image.dart';
|
||||
import 'package:zapstore/widgets/users_rich_text.dart';
|
||||
|
||||
class WebOfTrustContainer extends HookConsumerWidget {
|
||||
const WebOfTrustContainer({
|
||||
@@ -68,56 +68,6 @@ class WebOfTrustContainer extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class UsersRichText extends StatelessWidget {
|
||||
const UsersRichText({
|
||||
super.key,
|
||||
this.preSpan,
|
||||
this.trailingText,
|
||||
required this.users,
|
||||
});
|
||||
|
||||
final TextSpan? preSpan;
|
||||
final String? trailingText;
|
||||
final List<User> users;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
if (preSpan != null) preSpan!,
|
||||
for (final user in users)
|
||||
TextSpan(
|
||||
style: TextStyle(height: 1.6),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
RoundedImage(url: user.avatarUrl, size: 20),
|
||||
Text(
|
||||
' ${user.nameOrNpub}',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(users.indexOf(user) == users.length - 1
|
||||
? ''
|
||||
: (users.indexOf(user) == users.length - 2
|
||||
? ' and '
|
||||
: ', ')),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (users.indexOf(user) == users.length - 1)
|
||||
TextSpan(text: trailingText, style: TextStyle(fontSize: 15))
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final followsWhoFollowProvider = FutureProvider.autoDispose
|
||||
.family<List<User>, ({String from, String to})>((ref, arg) async {
|
||||
final _ =
|
||||
|
||||
+40
-62
@@ -1,12 +1,12 @@
|
||||
import 'package:async_button_builder/async_button_builder.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
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/models/settings.dart';
|
||||
import 'package:zapstore/navigation/router.dart';
|
||||
import 'package:zapstore/screens/settings_screen.dart';
|
||||
import 'package:zapstore/utils/extensions.dart';
|
||||
import 'package:zapstore/widgets/sign_in_container.dart';
|
||||
|
||||
import '../utils/nwc.dart';
|
||||
|
||||
@@ -21,7 +21,6 @@ class ZapButton extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final nwcConnection = ref.watch(nwcConnectionProvider);
|
||||
// final zapStatus = ref.watch(zapProvider);
|
||||
final user = ref.settings
|
||||
.watchOne('_', alsoWatch: (_) => {_.user})
|
||||
.model!
|
||||
@@ -30,61 +29,45 @@ class ZapButton extends HookConsumerWidget {
|
||||
|
||||
final amountController = TextEditingController();
|
||||
|
||||
// We use value as default text, for progress only interested in loading/error substates
|
||||
// final zapButtonText = switch (zapStatus) {
|
||||
// AsyncLoading() => 'Zapping... ⚡⚡⚡',
|
||||
// AsyncError() => 'Error zapping!',
|
||||
// AsyncValue() => 'Zap the dev ⚡',
|
||||
// };
|
||||
|
||||
// Show toast if could not zap
|
||||
// useMemoized(() {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// if (zapStatus.value != null) {
|
||||
// // final receipt = await zapStatus.value!.zapReceipt;
|
||||
// // TODO: zapStatus should include the amount of sats without needing async
|
||||
// // I could read zapReceiptsNotifier's state, but how I know it's this one?
|
||||
// context.showInfo('Zapped!',
|
||||
// description: 'You just sent the dev some sats');
|
||||
// }
|
||||
// if (zapStatus.hasError) {
|
||||
// context.showError(
|
||||
// title: 'Could not zap',
|
||||
// description: 'Error sending zap: ${zapStatus.error}');
|
||||
// }
|
||||
// });
|
||||
// }, [zapStatus.value, zapStatus.error]);
|
||||
|
||||
return app.developer.value != null
|
||||
? AsyncButtonBuilder(
|
||||
loadingWidget: SizedBox(
|
||||
width: 14, height: 14, child: CircularProgressIndicator()),
|
||||
return app.developer.value?.lud16 != null
|
||||
? ElevatedButton(
|
||||
onPressed: () async {
|
||||
if (user == null) {
|
||||
context.showError(
|
||||
title: 'Not signed in',
|
||||
description: 'Please sign in to zap');
|
||||
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 (nwcConnection.value == null) {
|
||||
context.showError(
|
||||
title: 'No wallet connected',
|
||||
description: 'Please connect your NWC wallet');
|
||||
appRouter.go('/settings');
|
||||
return;
|
||||
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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final developer = app.developer.value!;
|
||||
if (developer.lud16 == null) {
|
||||
context.showError(
|
||||
title: 'Unable to zap',
|
||||
description:
|
||||
'The developer (${developer.nameOrNpub}) does not have a Lightning address');
|
||||
if (ref.read(nwcConnectionProvider).value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final amount = await showDialog<int>(
|
||||
// ignore: use_build_context_synchronously
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
@@ -145,23 +128,18 @@ class ZapButton extends HookConsumerWidget {
|
||||
0;
|
||||
|
||||
if (amount > 0) {
|
||||
// User should be able to sign any event
|
||||
// Proposed API:
|
||||
await user.zap(amount,
|
||||
event: app.latestMetadata!, comment: 'From zapstore!');
|
||||
|
||||
// final lnurl = developer.lud16!;
|
||||
// await ref.read(zapProvider.notifier).zap(
|
||||
// user: user,
|
||||
// lnurl: lnurl,
|
||||
// eventId: app.latestMetadata!.id.toString(),
|
||||
// amount: amount,
|
||||
// pubKey: developer.pubkey);
|
||||
try {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.showInfo('Zap sent!',
|
||||
description: '$amount sats on their way');
|
||||
await loggedInUser.zap(amount, event: app.latestMetadata!);
|
||||
} catch (e) {
|
||||
// ignore: use_build_context_synchronously
|
||||
context.showError(
|
||||
title: 'Unable to zap', description: e.toString());
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, child, callback, state) {
|
||||
return ElevatedButton(onPressed: callback, child: child);
|
||||
},
|
||||
child: Text('Zap!'),
|
||||
)
|
||||
: Container();
|
||||
|
||||
@@ -5,12 +5,11 @@ import 'package:ndk/domain_layer/entities/nip_01_event.dart';
|
||||
import 'package:zapstore/main.data.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:zapstore/models/file_metadata.dart';
|
||||
import 'package:zapstore/models/user.dart';
|
||||
import 'package:zapstore/models/zap_receipt.dart';
|
||||
import 'package:zapstore/widgets/rounded_image.dart';
|
||||
import 'package:zapstore/widgets/users_rich_text.dart';
|
||||
|
||||
class ZapReceipts extends HookConsumerWidget {
|
||||
ZapReceipts({
|
||||
@@ -39,32 +38,10 @@ class ZapReceipts extends HookConsumerWidget {
|
||||
final senderIds = receipts.map((r) => r.senderPubkey).toSet();
|
||||
final senders = ref.users.findManyLocalByIds(senderIds);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Text("⚡ $totalAmountInSats sats (${receipts.length} zaps)"),
|
||||
Gap(5),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
for (final user in senders)
|
||||
TextSpan(
|
||||
style: TextStyle(height: 1.6),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
RoundedImage(url: user.avatarUrl, size: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
return UsersRichText(
|
||||
preSpan: TextSpan(text: '⚡ $totalAmountInSats sats by'),
|
||||
trailingText: ' and others',
|
||||
users: senders,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -810,10 +810,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ndk
|
||||
sha256: e17cb3aad2936c62978891e0d3db4dc37f3ded7efbdf89e571c3927158da9135
|
||||
sha256: "07c284a14e7800513743a84c911a546dcfcdc0c0ce2def4895c4e4c2286f159e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
version: "0.2.4"
|
||||
octo_image:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ dependencies:
|
||||
mutex: ^3.1.0
|
||||
easy_image_viewer: ^1.5.1
|
||||
flutter_secure_storage: ^9.2.2
|
||||
ndk: ^0.2.2
|
||||
ndk: ^0.2.4
|
||||
|
||||
dependency_overrides:
|
||||
purplebase:
|
||||
|
||||
Reference in New Issue
Block a user