From a1c7eda5ad5cf8b1cdb70e8d8bfc18ab9748e425 Mon Sep 17 00:00:00 2001 From: Ethan Lee <125412902+ethan-tbd@users.noreply.github.com> Date: Mon, 12 Feb 2024 13:04:28 -0800 Subject: [PATCH] chore: update text fields (#61) * add `InputDecorationTheme` * use theme's `InputDecoration` * add `useFocusNode()` in `SearchPaymentMethodsPage` * add `didPrefix` localization * add `invalidDid` localization * refactor `SendDidPage` with `useFocusNode()` * update test after refactoring `SendDidPage` --- .../payments/search_payment_methods_page.dart | 9 +- lib/features/send/send_did_page.dart | 156 ++++++++++-------- lib/l10n/app_en.arb | 6 +- lib/l10n/app_localizations.dart | 14 +- lib/l10n/app_localizations_en.dart | 8 +- lib/shared/json_schema_form.dart | 40 +++-- lib/shared/theme/theme.dart | 16 ++ test/features/send/send_did_page_test.dart | 23 +-- 8 files changed, 160 insertions(+), 112 deletions(-) diff --git a/lib/features/payments/search_payment_methods_page.dart b/lib/features/payments/search_payment_methods_page.dart index 28ee348e..66fb0384 100644 --- a/lib/features/payments/search_payment_methods_page.dart +++ b/lib/features/payments/search_payment_methods_page.dart @@ -18,6 +18,7 @@ class SearchPaymentMethodsPage extends HookWidget { @override Widget build(BuildContext context) { final searchText = useState(''); + final focusNode = useFocusNode(); return Scaffold( appBar: AppBar(scrolledUnderElevation: 0), @@ -33,9 +34,15 @@ class SearchPaymentMethodsPage extends HookWidget { mainAxisSize: MainAxisSize.min, children: [ TextFormField( + focusNode: focusNode, + onTapOutside: (_) => focusNode.unfocus(), + enableSuggestions: false, + autocorrect: false, decoration: InputDecoration( labelText: Loc.of(context).search, - prefixIcon: const Icon(Icons.search), + prefixIcon: const Padding( + padding: EdgeInsets.only(top: Grid.xs), + child: Icon(Icons.search)), ), onChanged: (value) => searchText.value = value, ), diff --git a/lib/features/send/send_did_page.dart b/lib/features/send/send_did_page.dart index 6fe7cdf4..8c492473 100644 --- a/lib/features/send/send_did_page.dart +++ b/lib/features/send/send_did_page.dart @@ -5,94 +5,48 @@ import 'package:didpay/shared/theme/grid.dart'; import 'package:didpay/shared/success_page.dart'; class SendDidPage extends HookWidget { + final _formKey = GlobalKey(); + final String sendAmount; - const SendDidPage({super.key, required this.sendAmount}); + SendDidPage({super.key, required this.sendAmount}); @override Widget build(BuildContext context) { - final sendDid = useState(''); + final focusNode = useFocusNode(); + final controller = useTextEditingController(); return Scaffold( - appBar: AppBar( - title: Column( - children: [ - Text( - '\$$sendAmount', - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center, - ), - Text( - Loc.of(context).accountBalance, - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ) - ], - )), + appBar: AppBar(), body: SafeArea( child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: Grid.side), - child: Column( - children: [ - Column(children: [ - const SizedBox(height: Grid.lg), - GestureDetector( - child: Row( - children: [ - const Icon(Icons.qr_code_scanner, - size: Grid.sm), - const SizedBox(width: Grid.xs), - Expanded( - child: Text( - Loc.of(context).scanQrCode, - softWrap: true, - ), - ) - ], - ), - ), - ]), - const SizedBox(height: Grid.lg), - Row( - children: [ - Text(Loc.of(context).to, - style: Theme.of(context).textTheme.titleMedium), - const SizedBox(width: Grid.xs), - Expanded( - child: TextField( - decoration: InputDecoration( - border: InputBorder.none, - floatingLabelBehavior: - FloatingLabelBehavior.never, - labelText: Loc.of(context).didTag), - enableSuggestions: false, - autocorrect: false, - onChanged: (value) => sendDid.value = value), - ) - ], - ) - ], - ), - ), - ], + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: Grid.xxs), + _buildScanQrTile(context, controller), + const SizedBox(height: Grid.xxs), + _buildForm(context, focusNode, controller), + ], + ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: Grid.side), child: FilledButton( onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => SuccessPage( - text: Loc.of(context).yourPaymentWasSent, + if (_formKey.currentState?.validate() ?? false) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SuccessPage( + text: Loc.of(context).yourPaymentWasSent), ), - ), - ); + ); + } }, child: Text('${Loc.of(context).pay} \$$sendAmount'), ), @@ -100,4 +54,64 @@ class SendDidPage extends HookWidget { ]), )); } + + Widget _buildForm(BuildContext context, FocusNode focusNode, + TextEditingController controller) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: Grid.side), + child: Form( + key: _formKey, + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Padding( + padding: const EdgeInsets.only(right: Grid.xs), + child: Text( + Loc.of(context).to, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(color: Theme.of(context).colorScheme.primary), + ), + ), + Expanded( + child: TextFormField( + focusNode: focusNode, + controller: controller, + onTapOutside: (event) => focusNode.unfocus(), + maxLines: null, + enableSuggestions: false, + autocorrect: false, + decoration: InputDecoration( + labelText: Loc.of(context).didPrefix, + ), + validator: (value) { + if (value == null || + !value.startsWith(Loc.of(context).didPrefix)) { + return Loc.of(context).invalidDid; + } + return null; + }), + ), + ], + )), + ); + } + + Widget _buildScanQrTile( + BuildContext context, TextEditingController controller) { + return ListTile( + leading: const Icon(Icons.qr_code), + title: Text( + Loc.of(context).scanQrCode, + style: Theme.of(context).textTheme.bodyMedium, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: Grid.side), + trailing: const Icon(Icons.chevron_right), + onTap: () => { + //TODO: complete in #55 using TextEditingController + }, + ); + } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index da141ed9..8b1f54e3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -8,7 +8,7 @@ "done": "Done", "deposit": "Deposit", "send": "Send", - "to": "To", + "to": "To:", "pay": "Pay", "scanQrCode": "Don't know the recipient's DID tag? Scan their QR code", "didTag": "DID tag", @@ -64,5 +64,7 @@ }, "search": "Search", "serviceFeesMayApply": "Service fees may apply", - "selectPaymentMethod": "Select a payment method" + "selectPaymentMethod": "Select a payment method", + "didPrefix": "did:jwk:", + "invalidDid": "Invalid DID" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 20022595..d4e7b13b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -148,7 +148,7 @@ abstract class Loc { /// No description provided for @to. /// /// In en, this message translates to: - /// **'To'** + /// **'To:'** String get to; /// No description provided for @pay. @@ -336,6 +336,18 @@ abstract class Loc { /// In en, this message translates to: /// **'Select a payment method'** String get selectPaymentMethod; + + /// No description provided for @didPrefix. + /// + /// In en, this message translates to: + /// **'did:jwk:'** + String get didPrefix; + + /// No description provided for @invalidDid. + /// + /// In en, this message translates to: + /// **'Invalid DID'** + String get invalidDid; } class _LocDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 4a5fe2b4..2c5eecd2 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -32,7 +32,7 @@ class LocEn extends Loc { String get send => 'Send'; @override - String get to => 'To'; + String get to => 'To:'; @override String get pay => 'Pay'; @@ -132,4 +132,10 @@ class LocEn extends Loc { @override String get selectPaymentMethod => 'Select a payment method'; + + @override + String get didPrefix => 'did:jwk:'; + + @override + String get invalidDid => 'Invalid DID'; } diff --git a/lib/shared/json_schema_form.dart b/lib/shared/json_schema_form.dart index 251d2271..b7a68458 100644 --- a/lib/shared/json_schema_form.dart +++ b/lib/shared/json_schema_form.dart @@ -17,20 +17,28 @@ class JsonSchemaForm extends HookWidget { @override Widget build(BuildContext context) { final jsonSchema = json.decode(schema); + List formFields = []; - jsonSchema['properties']?.forEach((key, value) { - formFields.add(TextFormField( - decoration: InputDecoration( - labelText: value['title'] ?? key, - labelStyle: TextStyle( - color: Theme.of(context).colorScheme.outlineVariant, + jsonSchema['properties']?.forEach( + (key, value) { + final focusNode = useFocusNode(); + + formFields.add( + TextFormField( + focusNode: focusNode, + onTapOutside: (_) => focusNode.unfocus(), + enableSuggestions: false, + autocorrect: false, + decoration: InputDecoration( + labelText: value['title'] ?? key, + ), + textInputAction: TextInputAction.next, + validator: (value) => _validateField(key, value, jsonSchema), + onSaved: (value) => formData[key] = value ?? '', ), - border: InputBorder.none, - ), - validator: (value) => _validateField(key, value, jsonSchema), - onSaved: (value) => formData[key] = value ?? '', - )); - }); + ); + }, + ); return Form( key: _formKey, @@ -41,12 +49,8 @@ class JsonSchemaForm extends HookWidget { child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Grid.side, - ), - child: Column( - children: formFields, - ), + padding: const EdgeInsets.symmetric(horizontal: Grid.side), + child: Column(children: formFields), ), ), ), diff --git a/lib/shared/theme/theme.dart b/lib/shared/theme/theme.dart index 6061e140..7aee5972 100644 --- a/lib/shared/theme/theme.dart +++ b/lib/shared/theme/theme.dart @@ -33,6 +33,14 @@ ThemeData lightTheme(BuildContext context) => ThemeData( ), ), ), + inputDecorationTheme: InputDecorationTheme( + border: InputBorder.none, + floatingLabelBehavior: FloatingLabelBehavior.never, + alignLabelWithHint: true, + labelStyle: TextStyle( + color: lightColorScheme.outlineVariant, + ), + ), ); ThemeData darkTheme(BuildContext context) => ThemeData( @@ -65,4 +73,12 @@ ThemeData darkTheme(BuildContext context) => ThemeData( ), ), ), + inputDecorationTheme: InputDecorationTheme( + border: InputBorder.none, + floatingLabelBehavior: FloatingLabelBehavior.never, + alignLabelWithHint: true, + labelStyle: TextStyle( + color: darkColorScheme.outlineVariant, + ), + ), ); diff --git a/test/features/send/send_did_page_test.dart b/test/features/send/send_did_page_test.dart index f5b6d8dd..739dfeb7 100644 --- a/test/features/send/send_did_page_test.dart +++ b/test/features/send/send_did_page_test.dart @@ -8,26 +8,15 @@ void main() { group('SendDidPage', () { testWidgets('should show amount to send', (tester) async { await tester.pumpWidget( - WidgetHelpers.testableWidget( - child: const SendDidPage(sendAmount: '25')), + WidgetHelpers.testableWidget(child: SendDidPage(sendAmount: '25')), ); - expect(find.textContaining('\$25'), findsNWidgets(2)); - }); - - testWidgets('should show Account Balance', (tester) async { - await tester.pumpWidget( - WidgetHelpers.testableWidget( - child: const SendDidPage(sendAmount: '25')), - ); - - expect(find.text('Account balance'), findsOneWidget); + expect(find.textContaining('\$25'), findsOneWidget); }); testWidgets('should show QR Code CTA', (tester) async { await tester.pumpWidget( - WidgetHelpers.testableWidget( - child: const SendDidPage(sendAmount: '25')), + WidgetHelpers.testableWidget(child: SendDidPage(sendAmount: '25')), ); expect(find.textContaining('Scan their QR code'), findsOneWidget); @@ -35,8 +24,7 @@ void main() { testWidgets('should show input field', (tester) async { await tester.pumpWidget( - WidgetHelpers.testableWidget( - child: const SendDidPage(sendAmount: '25')), + WidgetHelpers.testableWidget(child: SendDidPage(sendAmount: '25')), ); expect(find.byType(TextField), findsOneWidget); @@ -44,8 +32,7 @@ void main() { testWidgets('should show pay button', (tester) async { await tester.pumpWidget( - WidgetHelpers.testableWidget( - child: const SendDidPage(sendAmount: '25')), + WidgetHelpers.testableWidget(child: SendDidPage(sendAmount: '25')), ); expect(find.widgetWithText(FilledButton, 'Pay \$25'), findsOneWidget);