Skip to content

Commit

Permalink
Address Send Payment UI feedback (#252)
Browse files Browse the repository at this point in the history
* Add a placeholder image on LNURL payments

* Disable "Next" & "Send" buttons if there are errors

* Add a placeholder image on LN payments

Remove center alignment to be consistent with LNURL payment confirmation page UI.

* Add currency display name on error messages.

* fix: Make SingleButtonBottomBar's enabled by default

* Display Currency Converter on a bottom sheet

- Remove Currency Converter Dialog
- Align Currency Converter icon to amount text
- Allow users to exclude currency symbol from formatted fiat value.
- Allow users to include display name to formatted fiat value.

* Apply font size, padding & layout changes on LNURL payment UI

- Allow editing label & error text style of AmountFormField
- Allow formatFiat options on format
- Move LnUrlPaymentLimits below error message
- Add bottom padding to LnUrlPaymentLimits
- Remove bottom padding from LnPaymentDescription
- Reduce LNURLMetadataText max height to 120

* Apply font size, padding & layout changes on Receive payment UIs

- Display ScrollableErrorMessageWidget if there are any errors creating invoices
- Allow customizing contentPadding, titleStyle & errorTextStyle of ScrollableErrorMessageWidget.
- Make the error message scrollable.
- Fix how LnurlWithdrawDialog looks on light theme

- Make amount + description labels white

* Make fiat currency chips scrollable

* Update CurrencyConverterBottomSheet

* Do not disable currency converter button, pop the sheet instead

* Address UI Feedback
  • Loading branch information
erdemyerebasmaz authored Dec 3, 2024
1 parent 7ab710e commit b66498c
Show file tree
Hide file tree
Showing 50 changed files with 2,055 additions and 1,201 deletions.
91 changes: 54 additions & 37 deletions lib/routes/enter_payment_info/enter_payment_info_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,48 +50,65 @@ class _EnterPaymentInfoPageState extends State<EnterPaymentInfoPage> {
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
controller: _paymentInfoController,
decoration: InputDecoration(
labelText: texts.enter_payment_info_page_label,
suffixIcon: IconButton(
padding: const EdgeInsets.only(top: 21.0),
alignment: Alignment.bottomRight,
icon: Image(
image: const AssetImage('assets/icons/qr_scan.png'),
color: themeData.iconTheme.color,
width: 24.0,
height: 24.0,
child: Container(
decoration: const ShapeDecoration(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
color: Color.fromRGBO(40, 59, 74, 0.5),
),
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
controller: _paymentInfoController,
decoration: InputDecoration(
border: const OutlineInputBorder(),
prefixIconConstraints: BoxConstraints.tight(
const Size(16, 56),
),
prefixIcon: const SizedBox.shrink(),
contentPadding: EdgeInsets.zero,
labelText: texts.enter_payment_info_page_label,
suffixIcon: IconButton(
padding: const EdgeInsets.only(bottom: 12.0, right: 12.0),
alignment: Alignment.bottomRight,
icon: Image(
image: const AssetImage('assets/icons/qr_scan.png'),
color: themeData.iconTheme.color,
width: 24.0,
height: 24.0,
),
tooltip: texts.enter_payment_info_page_scan_tooltip,
onPressed: () => _scanBarcode(),
),
tooltip: texts.enter_payment_info_page_scan_tooltip,
onPressed: () => _scanBarcode(),
),
style: FieldTextStyle.textStyle,
validator: (String? value) => errorMessage.isNotEmpty ? errorMessage : null,
onFieldSubmitted: (String input) async {
if (input.isNotEmpty) {
setState(() {
_paymentInfoController.text = input;
});
await _validateInput();
}
},
),
style: FieldTextStyle.textStyle,
validator: (String? value) => errorMessage.isNotEmpty ? errorMessage : null,
onFieldSubmitted: (String input) async {
if (input.isNotEmpty) {
setState(() {
_paymentInfoController.text = input;
});
await _validateInput();
}
},
),
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
texts.enter_payment_info_page_label_expanded,
style: FieldTextStyle.labelStyle.copyWith(
fontSize: 13.0,
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
texts.enter_payment_info_page_label_expanded,
style: FieldTextStyle.labelStyle.copyWith(
fontSize: 13.0,
),
),
),
),
],
],
),
),
),
),
Expand Down
40 changes: 40 additions & 0 deletions lib/routes/lnurl/widgets/lnurl_metadata_image.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter/material.dart';

class LNURLMetadataImage extends StatelessWidget {
final String? base64String;

const LNURLMetadataImage({
super.key,
this.base64String,
});

@override
Widget build(BuildContext context) {
const double imageSize = 128.0;

final Uint8List? imageBytes = base64String?.isNotEmpty == true ? base64Decode(base64String!) : null;

return ConstrainedBox(
constraints: const BoxConstraints(
minHeight: imageSize,
minWidth: imageSize,
maxHeight: imageSize,
maxWidth: imageSize,
),
child: imageBytes != null && imageBytes.isNotEmpty
? Image.memory(
imageBytes,
width: imageSize,
fit: BoxFit.cover,
)
: Image.asset(
'assets/icons/app_icon.png',
width: imageSize,
fit: BoxFit.cover,
),
);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:l_breez/theme/theme.dart';
import 'package:l_breez/utils/min_font_size.dart';

class LNURLMetadataText extends StatefulWidget {
Expand All @@ -20,9 +16,11 @@ class _LNURLMetadataTextState extends State<LNURLMetadataText> {

@override
Widget build(BuildContext context) {
final ThemeData themeData = Theme.of(context);

return Container(
constraints: const BoxConstraints(
maxHeight: 200,
maxHeight: 120,
minWidth: double.infinity,
),
child: Scrollbar(
Expand All @@ -33,41 +31,16 @@ class _LNURLMetadataTextState extends State<LNURLMetadataText> {
controller: _scrollController,
child: AutoSizeText(
widget.metadataText,
style: Theme.of(context).paymentItemSubtitleTextStyle.copyWith(color: Colors.white70),
style: themeData.primaryTextTheme.displaySmall!.copyWith(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: Colors.white,
height: 1.156,
),
minFontSize: MinFontSize(context).minFontSize,
),
),
),
);
}
}

class LNURLMetadataImage extends StatelessWidget {
final String? base64String;

const LNURLMetadataImage({super.key, this.base64String});

@override
Widget build(BuildContext context) {
if (base64String != null) {
final Uint8List bytes = base64Decode(base64String!);
if (bytes.isNotEmpty) {
const double imageSize = 128.0;
return ConstrainedBox(
constraints: const BoxConstraints(
minHeight: imageSize,
minWidth: imageSize,
maxWidth: imageSize,
maxHeight: imageSize,
),
child: Image.memory(
bytes,
width: imageSize,
fit: BoxFit.fitWidth,
),
);
}
}
return const SizedBox.shrink();
}
}
3 changes: 2 additions & 1 deletion lib/routes/lnurl/widgets/widgets.dart
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export 'lnurl_metadata.dart';
export 'lnurl_metadata_image.dart';
export 'lnurl_metadata_text.dart';
135 changes: 91 additions & 44 deletions lib/routes/receive_payment/lightning/receive_lightning_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class ReceiveLightningPaymentPageState extends State<ReceiveLightningPaymentPage
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

final TextEditingController _descriptionController = TextEditingController();
final FocusNode _descriptionFocusNode = FocusNode();
final TextEditingController _amountController = TextEditingController();
final FocusNode _amountFocusNode = FocusNode();
KeyboardDoneAction _doneAction = KeyboardDoneAction();
Expand Down Expand Up @@ -67,6 +68,7 @@ class ReceiveLightningPaymentPageState extends State<ReceiveLightningPaymentPage
builder: (BuildContext context, PaymentLimitsState snapshot) {
if (snapshot.hasError) {
return ScrollableErrorMessageWidget(
showIcon: true,
title: texts.payment_limits_generic_error_title,
message: texts.payment_limits_generic_error_message(snapshot.errorMessage),
);
Expand All @@ -82,7 +84,7 @@ class ReceiveLightningPaymentPageState extends State<ReceiveLightningPaymentPage

return prepareResponseFuture == null
? Padding(
padding: const EdgeInsets.only(bottom: 40.0),
padding: const EdgeInsets.only(top: 32, bottom: 40.0),
child: SingleChildScrollView(
child: _buildForm(lightningPaymentLimits),
),
Expand Down Expand Up @@ -149,51 +151,85 @@ class ReceiveLightningPaymentPageState extends State<ReceiveLightningPaymentPage

Widget _buildForm(LightningPaymentLimitsResponse lightningPaymentLimits) {
final BreezTranslations texts = context.texts();
final ThemeData themeData = Theme.of(context);

return BlocBuilder<CurrencyCubit, CurrencyState>(
builder: (BuildContext context, CurrencyState currencyState) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
controller: _descriptionController,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.done,
maxLines: null,
maxLength: 90,
maxLengthEnforcement: MaxLengthEnforcement.enforced,
decoration: InputDecoration(
labelText: texts.invoice_description_label,
),
style: FieldTextStyle.textStyle,
return Container(
decoration: const ShapeDecoration(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
AmountFormField(
context: context,
texts: texts,
bitcoinCurrency: currencyState.bitcoinCurrency,
focusNode: _amountFocusNode,
autofocus: true,
readOnly: false,
controller: _amountController,
validatorFn: (int v) => validatePayment(v, lightningPaymentLimits),
style: FieldTextStyle.textStyle,
),
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: AutoSizeText(
texts.invoice_min_payment_limit(
currencyState.bitcoinCurrency.format(
lightningPaymentLimits.receive.minSat.toInt(),
),
color: Color.fromRGBO(40, 59, 74, 0.5),
),
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
focusNode: _descriptionFocusNode,
controller: _descriptionController,
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.done,
maxLines: null,
maxLength: 90,
maxLengthEnforcement: MaxLengthEnforcement.enforced,
decoration: InputDecoration(
prefixIconConstraints: BoxConstraints.tight(
const Size(16, 56),
),
prefixIcon: const SizedBox.shrink(),
contentPadding: const EdgeInsets.only(left: 16, top: 16, bottom: 16),
border: const OutlineInputBorder(),
labelText: texts.invoice_description_label,
counterStyle: _descriptionFocusNode.hasFocus ? focusedCounterTextStyle : counterTextStyle,
),
style: textStyle,
maxLines: 1,
minFontSize: MinFontSize(context).minFontSize,
style: FieldTextStyle.textStyle,
),
),
],
const SizedBox(height: 8.0),
AmountFormField(
context: context,
texts: texts,
bitcoinCurrency: currencyState.bitcoinCurrency,
focusNode: _amountFocusNode,
autofocus: true,
readOnly: false,
controller: _amountController,
validatorFn: (int v) => validatePayment(v, lightningPaymentLimits),
style: FieldTextStyle.textStyle,
errorStyle: FieldTextStyle.labelStyle.copyWith(
fontSize: 18.0,
color: themeData.colorScheme.error,
),
),
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: AutoSizeText(
texts.invoice_min_payment_limit(
currencyState.bitcoinCurrency.format(
lightningPaymentLimits.receive.minSat.toInt(),
),
),
style: paymentLimitInformationTextStyle,
maxLines: 1,
minFontSize: MinFontSize(context).minFontSize,
),
),
].expand((Widget widget) sync* {
yield widget;
yield const Divider(
height: 32.0,
color: Color.fromRGBO(40, 59, 74, 1),
indent: 0.0,
endIndent: 0.0,
);
}).toList()
..removeLast(),
),
),
);
},
Expand All @@ -210,11 +246,22 @@ class ReceiveLightningPaymentPageState extends State<ReceiveLightningPaymentPage
return FutureBuilder<ReceivePaymentResponse>(
future: receivePaymentResponseFuture,
builder: (BuildContext context, AsyncSnapshot<ReceivePaymentResponse> receiveSnapshot) {
return DestinationWidget(
snapshot: receiveSnapshot,
title: context.texts().receive_payment_method_lightning_invoice,
infoWidget: PaymentFeesMessageBox(
feesSat: prepareSnapshot.data!.feesSat.toInt(),
return Container(
decoration: const ShapeDecoration(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
color: Color.fromRGBO(40, 59, 74, 0.5),
),
padding: const EdgeInsets.symmetric(vertical: 32, horizontal: 24),
child: DestinationWidget(
snapshot: receiveSnapshot,
paymentMethod: context.texts().receive_payment_method_lightning_invoice,
infoWidget: PaymentFeesMessageBox(
feesSat: prepareSnapshot.data!.feesSat.toInt(),
),
),
);
},
Expand Down
Loading

0 comments on commit b66498c

Please sign in to comment.