From 5f115dd4c944d175d5f1179fb324cf2cdf973734 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Mon, 25 Nov 2024 16:57:58 -0600 Subject: [PATCH 1/5] Show token input field by default and adjust decorations Shows the token input field always instead of being in a collapsible section. Also, adds messaging when a valid token or invalid token is added, with an icon. --- .../ubuntupro/end_to_end/end_to_end_test.dart | 2 +- .../ubuntu_pro_for_wsl_test.dart | 4 +- gui/packages/ubuntupro/lib/constants.dart | 4 + gui/packages/ubuntupro/lib/l10n/app_en.arb | 5 +- .../subscribe_now/subscribe_now_widgets.dart | 124 ++++++++++++------ .../subscription_status_widgets.dart | 5 +- 6 files changed, 94 insertions(+), 50 deletions(-) diff --git a/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart b/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart index 4d6ee04ff..95749e548 100644 --- a/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart +++ b/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart @@ -82,7 +82,7 @@ Future testManualTokenInput(WidgetTester tester) async { await tester.pumpAndSettle(); // submits the input. - final button = find.text(l10n.confirm); + final button = find.text(l10n.attach); await tester.tap(button); await tester.pumpAndSettle(); diff --git a/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart b/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart index 2838615f4..f5b3ee956 100644 --- a/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart +++ b/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart @@ -129,7 +129,7 @@ void main() { await tester.pump(); // submits the input. - final button = find.text(l10n.confirm); + final button = find.text(l10n.attach); await tester.tap(button); await tester.pumpAndSettle(); @@ -177,7 +177,7 @@ void main() { await tester.pump(); // submits the input. - final button = find.text(l10n.confirm); + final button = find.text(l10n.attach); await tester.tap(button); await tester.pumpAndSettle(); diff --git a/gui/packages/ubuntupro/lib/constants.dart b/gui/packages/ubuntupro/lib/constants.dart index ae46f9372..361c70fdd 100644 --- a/gui/packages/ubuntupro/lib/constants.dart +++ b/gui/packages/ubuntupro/lib/constants.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + /// The name of the file where the Agent's drop its service connection information. const kAddrFileName = '.ubuntupro/.address'; @@ -12,3 +14,5 @@ const kVersion = String.fromEnvironment( 'UP4W_FULL_VERSION', defaultValue: 'Dev', ); + +const kConfirmColor = Color(0xFF0E8420); diff --git a/gui/packages/ubuntupro/lib/l10n/app_en.arb b/gui/packages/ubuntupro/lib/l10n/app_en.arb index 8f64e73e5..d8ac9a116 100644 --- a/gui/packages/ubuntupro/lib/l10n/app_en.arb +++ b/gui/packages/ubuntupro/lib/l10n/app_en.arb @@ -1,10 +1,11 @@ { "appTitle": "Ubuntu Pro for WSL", "tokenErrorEmpty": "Token cannot be empty", - "tokenErrorInvalid": "Token invalid", + "tokenErrorInvalid": "Invalid token", + "tokenValid": "Valid token", "tokenInputTitle": "Already have a token?", "tokenInputHint": "Paste your Ubuntu Pro token here", - "confirm": "Confirm", + "attach": "Attach", "applyProToken": "Apply Pro Token", "applyingProToken": "Applying token {token}", "@applyingProToken": { diff --git a/gui/packages/ubuntupro/lib/pages/subscribe_now/subscribe_now_widgets.dart b/gui/packages/ubuntupro/lib/pages/subscribe_now/subscribe_now_widgets.dart index d826ad76b..213024674 100644 --- a/gui/packages/ubuntupro/lib/pages/subscribe_now/subscribe_now_widgets.dart +++ b/gui/packages/ubuntupro/lib/pages/subscribe_now/subscribe_now_widgets.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:yaru/yaru.dart'; +import '../../constants.dart'; import '../../core/either_value_notifier.dart'; import '../../core/pro_token.dart'; @@ -61,58 +62,95 @@ class _ProTokenInputFieldState extends State { final lang = AppLocalizations.of(context); final theme = Theme.of(context); - return YaruExpandable( - header: Text( - lang.tokenInputTitle, - style: - theme.textTheme.bodyMedium!.copyWith(fontWeight: FontWeight.w100), - ), - expandIcon: Icon( - ProTokenInputField.expandIcon, - color: theme.textTheme.bodyMedium!.color, - ), - isExpanded: widget.isExpanded, - child: ValueListenableBuilder( - valueListenable: _token, - builder: (context, _, __) => Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextField( - inputFormatters: [ - // This ignores all sorts of (Unicode) whitespaces (not only at the ends). - FilteringTextInputFormatter.deny(RegExp(r'\s')), - ], - autofocus: false, - controller: _controller, - decoration: InputDecoration( - hintText: lang.tokenInputHint, - errorText: _token.errorOrNull?.localize(lang), - counterText: '', + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + lang.tokenInputTitle, + style: + theme.textTheme.bodyMedium!.copyWith(fontWeight: FontWeight.w100), + ), + const SizedBox( + height: 8, + ), + ValueListenableBuilder( + valueListenable: _token, + builder: (context, _, __) => Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + inputFormatters: [ + // This ignores all sorts of (Unicode) whitespaces (not only at the ends). + FilteringTextInputFormatter.deny(RegExp(r'\s')), + ], + autofocus: false, + controller: _controller, + decoration: InputDecoration( + hintText: lang.tokenInputHint, + error: _token.errorOrNull?.localize(lang) != null + ? Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + const Icon( + Icons.cancel, + color: Colors.red, + size: 16.0, + ), + const SizedBox(width: 4), + Text( + _token.errorOrNull!.localize(lang)!, + style: theme.textTheme.bodySmall! + .copyWith(color: Colors.redAccent), + ), + ], + ), + ) + : null, + helper: _token.valueOrNull != null + ? Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + const Icon( + Icons.check_circle, + color: kConfirmColor, + size: 16.0, + ), + const SizedBox(width: 4), + Text( + lang.tokenValid, + style: theme.textTheme.bodySmall! + .copyWith(color: Colors.green), + ), + ], + ), + ) + : null, + ), + onChanged: _token.update, + onSubmitted: _onSubmitted, ), - onChanged: _token.update, - onSubmitted: _onSubmitted, ), - ), - const SizedBox( - width: 8.0, - ), - ElevatedButton( - onPressed: canSubmit ? _handleApplyButton : null, - child: Text(lang.confirm), - ), - ], + const SizedBox( + width: 8.0, + ), + ElevatedButton( + onPressed: canSubmit ? _handleApplyButton : null, + child: Text(lang.attach), + ), + ], + ), ), - ), + ], ); } } /// A value-notifier for the [ProToken] with validation. -/// Since we don't want to start the UI with an error due the text field being -/// empty, this stores a nullable [ProToken] object class ProTokenValue extends EitherValueNotifier { - ProTokenValue() : super.ok(null); + ProTokenValue() : super.err(TokenError.empty); String? get token => valueOrNull?.value; diff --git a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart index 1f83d02d7..ef9b2637b 100644 --- a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart +++ b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart @@ -3,6 +3,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:yaru/yaru.dart'; +import '../../constants.dart'; import '../widgets/page_widgets.dart'; /// A page content widget built on top of the Dark styled landing page showing the current user active subscription @@ -47,7 +48,7 @@ class SubscriptionStatus extends StatelessWidget { Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - border: Border.all(color: const Color(0xFF0E8420), width: 1.0), + border: Border.all(color: kConfirmColor, width: 1.0), color: const Color(0xFFE6F8E8), borderRadius: const BorderRadius.all(Radius.circular(4.0)), ), @@ -57,7 +58,7 @@ class SubscriptionStatus extends StatelessWidget { children: [ const Icon( Icons.check_circle_outline_outlined, - color: Color(0xFF0E8420), + color: kConfirmColor, size: 24.0, ), const SizedBox(width: 8.0), From c94da67b43f2d1d7284226b0b2611016741fc7a7 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Mon, 25 Nov 2024 16:58:14 -0600 Subject: [PATCH 2/5] Adjust tests for always visible token input --- .../subscribe_now_widgets_test.dart | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/gui/packages/ubuntupro/test/pages/subcribe_now/subscribe_now_widgets_test.dart b/gui/packages/ubuntupro/test/pages/subcribe_now/subscribe_now_widgets_test.dart index 404ab9071..e022e0bd8 100644 --- a/gui/packages/ubuntupro/test/pages/subcribe_now/subscribe_now_widgets_test.dart +++ b/gui/packages/ubuntupro/test/pages/subcribe_now/subscribe_now_widgets_test.dart @@ -46,25 +46,6 @@ void main() { }); group('pro token input', () { - testWidgets('collapsed by default', (tester) async { - final app = MaterialApp( - home: Scaffold( - body: ProTokenInputField( - onApply: (_) {}, - ), - ), - localizationsDelegates: AppLocalizations.localizationsDelegates, - ); - - await tester.pumpWidget(app); - expect(find.byType(TextField).hitTestable(), findsNothing); - - final toggle = find.byType(IconButton); - await tester.tap(toggle); - await tester.pumpAndSettle(); - expect(find.byType(TextField).hitTestable(), findsOneWidget); - }); - group('basic flow', () { final theApp = buildApp(onApply: (_) {}, isExpanded: true); testWidgets('starts with no error', (tester) async { @@ -93,8 +74,11 @@ void main() { await tester.enterText(inputField, token); await tester.pump(); - final input = tester.firstWidget(inputField); - expect(input.decoration!.errorText, equals(lang.tokenErrorInvalid)); + final errorText = find.descendant( + of: inputField, + matching: find.text(lang.tokenErrorInvalid), + ); + expect(errorText, findsOne); final button = tester.firstWidget(find.byType(ElevatedButton)); @@ -112,8 +96,11 @@ void main() { await tester.enterText(inputField, tks.invalidTokens[0]); await tester.pump(); - var input = tester.firstWidget(inputField); - expect(input.decoration!.errorText, equals(lang.tokenErrorInvalid)); + final errorText = find.descendant( + of: inputField, + matching: find.text(lang.tokenErrorInvalid), + ); + expect(errorText, findsOne); final button = tester.firstWidget(find.byType(ElevatedButton)); @@ -122,20 +109,27 @@ void main() { // ...except when we delete the content we should have no more errors await tester.enterText(inputField, ''); await tester.pump(); - input = tester.firstWidget(inputField); - expect(input.decoration!.errorText, isNull); + final input = tester.firstWidget(inputField); + expect(input.decoration!.error, isNull); expect(button.enabled, isFalse); }); testWidgets('good token', (tester) async { await tester.pumpWidget(theApp); final inputField = find.byType(TextField); + final context = tester.element(inputField); + final lang = AppLocalizations.of(context); await tester.enterText(inputField, tks.good); await tester.pump(); final input = tester.firstWidget(inputField); - expect(input.decoration!.errorText, isNull); + expect(input.decoration!.error, isNull); + final validText = find.descendant( + of: inputField, + matching: find.text(lang.tokenValid), + ); + expect(validText, findsOne); final button = tester.firstWidget(find.byType(ElevatedButton)); From b65dea59f6e882eff45a47a8084b05e077463dc7 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Mon, 25 Nov 2024 18:08:41 -0600 Subject: [PATCH 3/5] Fix end to end and integration tests --- gui/packages/ubuntupro/end_to_end/end_to_end_test.dart | 6 ------ .../integration_test/ubuntu_pro_for_wsl_test.dart | 9 --------- 2 files changed, 15 deletions(-) diff --git a/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart b/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart index 95749e548..eb6c31690 100644 --- a/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart +++ b/gui/packages/ubuntupro/end_to_end/end_to_end_test.dart @@ -7,7 +7,6 @@ import 'package:stack_trace/stack_trace.dart' as stack_trace; import 'package:ubuntupro/core/environment.dart'; import 'package:ubuntupro/main.dart' as app; import 'package:ubuntupro/pages/subscribe_now/subscribe_now_page.dart'; -import 'package:ubuntupro/pages/subscribe_now/subscribe_now_widgets.dart'; import 'package:ubuntupro/pages/subscription_status/subscription_status_page.dart'; import '../test/utils/l10n_tester.dart'; @@ -63,11 +62,6 @@ Future testManualTokenInput(WidgetTester tester) async { // The "subscribe now page" is only shown if the GUI communicates with the background agent. var l10n = tester.l10n(); - // expands the collapsed input field group - final toggle = find.byIcon(ProTokenInputField.expandIcon); - await tester.tap(toggle); - await tester.pumpAndSettle(); - // finds the pro token from the environment final goodToken = Environment()[proTokenEnv]; expect( diff --git a/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart b/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart index f5b3ee956..817828e1f 100644 --- a/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart +++ b/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_wsl_test.dart @@ -15,7 +15,6 @@ import 'package:ubuntupro/main.dart' as app; import 'package:ubuntupro/pages/landscape/landscape_page.dart'; import 'package:ubuntupro/pages/startup/startup_page.dart'; import 'package:ubuntupro/pages/subscribe_now/subscribe_now_page.dart'; -import 'package:ubuntupro/pages/subscribe_now/subscribe_now_widgets.dart'; import 'package:ubuntupro/pages/subscription_status/subscription_status_page.dart'; import 'package:yaru/widgets.dart'; import 'package:yaru_test/yaru_test.dart'; @@ -118,10 +117,6 @@ void main() { // The "subscribe now page" is only shown if the GUI communicates with the background agent. var l10n = tester.l10n(); - // expands the collapsed input field group - final toggle = find.byIcon(ProTokenInputField.expandIcon); - await tester.tap(toggle); - await tester.pumpAndSettle(); // enters a good token value final inputField = find.byType(TextField); @@ -166,10 +161,6 @@ void main() { // The "subscribe now page" is only shown if the GUI communicates with the background agent. var l10n = tester.l10n(); - // expands the collapsed input field group - final toggle = find.byIcon(ProTokenInputField.expandIcon); - await tester.tap(toggle); - await tester.pumpAndSettle(); // enters a good token value final inputField = find.byType(TextField); From e4737a2c9e48b6be8fe2b948dcd3221641cee0f1 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Tue, 26 Nov 2024 13:58:39 -0600 Subject: [PATCH 4/5] Convert custom colors to Yaru colors --- gui/packages/ubuntupro/lib/constants.dart | 4 ---- .../subscribe_now/subscribe_now_widgets.dart | 19 ++++++++++--------- .../subscription_status_widgets.dart | 10 +++++----- 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/gui/packages/ubuntupro/lib/constants.dart b/gui/packages/ubuntupro/lib/constants.dart index 361c70fdd..ae46f9372 100644 --- a/gui/packages/ubuntupro/lib/constants.dart +++ b/gui/packages/ubuntupro/lib/constants.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - /// The name of the file where the Agent's drop its service connection information. const kAddrFileName = '.ubuntupro/.address'; @@ -14,5 +12,3 @@ const kVersion = String.fromEnvironment( 'UP4W_FULL_VERSION', defaultValue: 'Dev', ); - -const kConfirmColor = Color(0xFF0E8420); diff --git a/gui/packages/ubuntupro/lib/pages/subscribe_now/subscribe_now_widgets.dart b/gui/packages/ubuntupro/lib/pages/subscribe_now/subscribe_now_widgets.dart index 213024674..73aaa3293 100644 --- a/gui/packages/ubuntupro/lib/pages/subscribe_now/subscribe_now_widgets.dart +++ b/gui/packages/ubuntupro/lib/pages/subscribe_now/subscribe_now_widgets.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:yaru/yaru.dart'; -import '../../constants.dart'; import '../../core/either_value_notifier.dart'; import '../../core/pro_token.dart'; @@ -93,16 +92,17 @@ class _ProTokenInputFieldState extends State { padding: const EdgeInsets.only(top: 4), child: Row( children: [ - const Icon( + Icon( Icons.cancel, - color: Colors.red, + color: YaruColors.of(context).error, size: 16.0, ), const SizedBox(width: 4), Text( _token.errorOrNull!.localize(lang)!, - style: theme.textTheme.bodySmall! - .copyWith(color: Colors.redAccent), + style: theme.textTheme.bodySmall!.copyWith( + color: YaruColors.of(context).error, + ), ), ], ), @@ -113,16 +113,17 @@ class _ProTokenInputFieldState extends State { padding: const EdgeInsets.only(top: 4), child: Row( children: [ - const Icon( + Icon( Icons.check_circle, - color: kConfirmColor, + color: YaruColors.of(context).success, size: 16.0, ), const SizedBox(width: 4), Text( lang.tokenValid, - style: theme.textTheme.bodySmall! - .copyWith(color: Colors.green), + style: theme.textTheme.bodySmall!.copyWith( + color: YaruColors.of(context).success, + ), ), ], ), diff --git a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart index ef9b2637b..c622f4783 100644 --- a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart +++ b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart @@ -3,7 +3,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:yaru/yaru.dart'; -import '../../constants.dart'; import '../widgets/page_widgets.dart'; /// A page content widget built on top of the Dark styled landing page showing the current user active subscription @@ -48,17 +47,18 @@ class SubscriptionStatus extends StatelessWidget { Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - border: Border.all(color: kConfirmColor, width: 1.0), - color: const Color(0xFFE6F8E8), + border: Border.all(color: theme.colorScheme.success, width: 1.0), + color: theme.colorScheme.success + .copyWith(lightness: 0.94, saturation: 0.56), borderRadius: const BorderRadius.all(Radius.circular(4.0)), ), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon( + Icon( Icons.check_circle_outline_outlined, - color: kConfirmColor, + color: theme.colorScheme.success, size: 24.0, ), const SizedBox(width: 8.0), From a9c1fd870fce2aa777c84ef6df606fefcf837812 Mon Sep 17 00:00:00 2001 From: ashuntu Date: Tue, 26 Nov 2024 15:53:56 -0600 Subject: [PATCH 5/5] Use YaruColors instead of colorScheme --- .../subscription_status/subscription_status_widgets.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart index c622f4783..cbe908113 100644 --- a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart +++ b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_widgets.dart @@ -47,8 +47,10 @@ class SubscriptionStatus extends StatelessWidget { Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( - border: Border.all(color: theme.colorScheme.success, width: 1.0), - color: theme.colorScheme.success + border: + Border.all(color: YaruColors.of(context).success, width: 1.0), + color: YaruColors.of(context) + .success .copyWith(lightness: 0.94, saturation: 0.56), borderRadius: const BorderRadius.all(Radius.circular(4.0)), ), @@ -58,7 +60,7 @@ class SubscriptionStatus extends StatelessWidget { children: [ Icon( Icons.check_circle_outline_outlined, - color: theme.colorScheme.success, + color: YaruColors.of(context).success, size: 24.0, ), const SizedBox(width: 8.0),