feat: custom amount

chore: improve zap dialog ux
closes #25
This commit is contained in:
2025-05-16 11:04:18 +01:00
parent 4ca9460a6c
commit d85c93b7ed
3 changed files with 131 additions and 37 deletions

View File

@ -138,7 +138,9 @@ class _StreamPage extends State<StreamPage> {
context: context, context: context,
constraints: BoxConstraints.expand(), constraints: BoxConstraints.expand(),
builder: (ctx) { builder: (ctx) {
return ZapWidget( return SingleChildScrollView(
primary: false,
child: ZapWidget(
pubkey: stream.info.host, pubkey: stream.info.host,
target: stream.event, target: stream.event,
zapTags: zapTags:
@ -148,6 +150,7 @@ class _StreamPage extends State<StreamPage> {
["e", stream.info.goal!], ["e", stream.info.goal!],
] ]
: null, : null,
),
); );
}, },
); );

View File

@ -7,6 +7,7 @@ class BasicButton extends StatelessWidget {
final EdgeInsetsGeometry? padding; final EdgeInsetsGeometry? padding;
final EdgeInsetsGeometry? margin; final EdgeInsetsGeometry? margin;
final void Function()? onTap; final void Function()? onTap;
final bool? disabled;
const BasicButton( const BasicButton(
this.child, { this.child, {
@ -15,6 +16,7 @@ class BasicButton extends StatelessWidget {
this.padding, this.padding,
this.margin, this.margin,
this.onTap, this.onTap,
this.disabled,
}); });
static text( static text(
@ -24,6 +26,7 @@ class BasicButton extends StatelessWidget {
EdgeInsetsGeometry? margin, EdgeInsetsGeometry? margin,
void Function()? onTap, void Function()? onTap,
double? fontSize, double? fontSize,
bool? disabled,
}) { }) {
return BasicButton( return BasicButton(
Text( Text(
@ -34,6 +37,7 @@ class BasicButton extends StatelessWidget {
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
disabled: disabled,
decoration: decoration, decoration: decoration,
padding: padding ?? EdgeInsets.symmetric(vertical: 4, horizontal: 12), padding: padding ?? EdgeInsets.symmetric(vertical: 4, horizontal: 12),
margin: margin, margin: margin,
@ -44,16 +48,20 @@ class BasicButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final defaultBr = BorderRadius.all(Radius.circular(100)); final defaultBr = BorderRadius.all(Radius.circular(100));
return GestureDetector( final inner = Container(
onTap: onTap,
child: Container(
padding: padding, padding: padding,
margin: margin, margin: margin,
decoration: decoration:
decoration ?? decoration ?? BoxDecoration(color: LAYER_2, borderRadius: defaultBr),
BoxDecoration(color: LAYER_2, borderRadius: defaultBr),
child: Center(child: child), child: Center(child: child),
), );
return GestureDetector(
onTap: () {
if (!(disabled ?? false) && onTap != null) {
onTap!();
}
},
child: (disabled ?? false) ? Opacity(opacity: 0.3, child: inner) : inner,
); );
} }
} }

View File

@ -1,6 +1,7 @@
import 'package:clipboard/clipboard.dart'; import 'package:clipboard/clipboard.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ndk/domain_layer/usecases/lnurl/lnurl.dart'; import 'package:ndk/domain_layer/usecases/lnurl/lnurl.dart';
import 'package:ndk/ndk.dart'; import 'package:ndk/ndk.dart';
import 'package:qr_flutter/qr_flutter.dart'; import 'package:qr_flutter/qr_flutter.dart';
@ -31,6 +32,9 @@ class ZapWidget extends StatefulWidget {
class _ZapWidget extends State<ZapWidget> { class _ZapWidget extends State<ZapWidget> {
final TextEditingController _comment = TextEditingController(); final TextEditingController _comment = TextEditingController();
final TextEditingController _customAmount = TextEditingController();
final FocusNode _customAmountFocus = FocusNode();
bool _loading = false;
String? _error; String? _error;
String? _pr; String? _pr;
int? _amount; int? _amount;
@ -67,8 +71,9 @@ class _ZapWidget extends State<ZapWidget> {
), ),
], ],
), ),
if (_pr == null) ..._inputs(), if (_pr == null && !_loading) ..._inputs(),
if (_pr != null) ..._invoice(), if (_pr != null) ..._invoice(),
if (_loading) CircularProgressIndicator(),
], ],
), ),
); );
@ -83,32 +88,78 @@ class _ZapWidget extends State<ZapWidget> {
crossAxisCount: 5, crossAxisCount: 5,
mainAxisSpacing: 5, mainAxisSpacing: 5,
crossAxisSpacing: 5, crossAxisSpacing: 5,
childAspectRatio: 1.5, childAspectRatio: 1.9,
), ),
itemBuilder: (ctx, idx) => _zapAmount(_zapAmounts[idx]), itemBuilder: (ctx, idx) => _zapAmount(_zapAmounts[idx]),
), ),
Row(
spacing: 8,
children: [
Expanded(
child: TextFormField(
controller: _customAmount,
focusNode: _customAmountFocus,
keyboardType: TextInputType.number,
decoration: InputDecoration(labelText: "Custom Amount"),
),
),
BasicButton.text(
"Confirm",
onTap: () {
final newAmount = int.tryParse(_customAmount.text);
if (newAmount != null) {
setState(() {
_error = null;
_amount = newAmount;
_customAmountFocus.unfocus();
});
} else {
setState(() {
_error = "Invalid custom amount";
_amount = null;
});
}
},
),
],
),
TextFormField( TextFormField(
controller: _comment, controller: _comment,
decoration: InputDecoration(labelText: "Comment"), decoration: InputDecoration(labelText: "Comment"),
), ),
BasicButton.text( BasicButton.text(
"Zap", _amount != null ? "Zap ${formatSats(_amount!)} sats" : "Zap",
disabled: _amount == null,
decoration: BoxDecoration(color: LAYER_3, borderRadius: DEFAULT_BR), decoration: BoxDecoration(color: LAYER_3, borderRadius: DEFAULT_BR),
onTap: () { onTap: () async {
try { try {
_loadZap(); setState(() {
_error = null;
_loading = true;
});
await _loadZap();
} catch (e) { } catch (e) {
setState(() { setState(() {
_error = e.toString(); _error = e.toString();
}); });
} finally {
setState(() {
_loading = false;
});
} }
}, },
), ),
if (_error != null) Text(_error!), if (_error != null)
Text(
_error!,
style: TextStyle(color: WARNING, fontWeight: FontWeight.bold),
),
]; ];
} }
List<Widget> _invoice() { List<Widget> _invoice() {
final prLink = "lightning:${_pr!}";
return [ return [
QrImageView( QrImageView(
data: _pr!, data: _pr!,
@ -124,21 +175,50 @@ class _ZapWidget extends State<ZapWidget> {
onTap: () async { onTap: () async {
await FlutterClipboard.copy(_pr!); await FlutterClipboard.copy(_pr!);
}, },
child: Text(_pr!, overflow: TextOverflow.ellipsis), child: Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(borderRadius: DEFAULT_BR, color: LAYER_2),
child: Row(
spacing: 4,
children: [
Icon(Icons.copy, size: 16),
Expanded(child: Text(_pr!, overflow: TextOverflow.ellipsis)),
],
), ),
BasicButton.text( ),
),
FutureBuilder(
future: canLaunchUrlString(prLink),
builder: (context, v) {
if (!(v.data ?? false)) return SizedBox();
return BasicButton.text(
"Open in Wallet", "Open in Wallet",
onTap: () async { onTap: () async {
try { try {
await launchUrlString("lightning:${_pr!}"); await launchUrlString(prLink);
} catch (e) { } catch (e) {
if (e is PlatformException) {
if (e.code == "ACTIVITY_NOT_FOUND") {
setState(() {
_error = "No lightning wallet installed";
});
return;
}
}
setState(() { setState(() {
_error = e is String ? e : e.toString(); _error = e is String ? e : e.toString();
}); });
} }
}, },
);
},
),
if (_error != null)
Text(
_error!,
style: TextStyle(color: WARNING, fontWeight: FontWeight.bold),
), ),
if (_error != null) Text(_error!),
]; ];
} }
@ -204,6 +284,9 @@ class _ZapWidget extends State<ZapWidget> {
return GestureDetector( return GestureDetector(
onTap: onTap:
() => setState(() { () => setState(() {
_error = null;
_customAmount.clear();
_customAmountFocus.unfocus();
_amount = n; _amount = n;
}), }),
child: Container( child: Container(