Skip to content

Commit

Permalink
feat: Always show Pro token input (#980)
Browse files Browse the repository at this point in the history
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.

A small detail I missed when adjusting the copy for errors previously:
invalid tokens now show the string "Invalid token" instead of "Token
invalid".

<details>
<summary>Screenshot of the default look</summary>


![image](https://github.com/user-attachments/assets/4ec8ff4f-67da-4a1e-9eb3-337048e1afd5)

</details>

<details>
<summary>Screenshot when an invalid token is entered</summary>


![image](https://github.com/user-attachments/assets/bdacf88c-3f4c-41e1-acd8-78267c5fed66)

</details>

<details>
<summary>Screenshot when a valid token is entered</summary>


![image](https://github.com/user-attachments/assets/0d9e8f65-3643-402c-8cfc-8e10d21d5f04)

</details>

Our designs in Figma include functionality that we do not have, so I
took some creative liberties in adapting it to the current layout. Let
me know if it needs any adjustments.

---

UDENG-5284
  • Loading branch information
ashuntu authored Nov 27, 2024
2 parents bbb22e0 + a9c1fd8 commit c7baeea
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 93 deletions.
8 changes: 1 addition & 7 deletions gui/packages/ubuntupro/end_to_end/end_to_end_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -63,11 +62,6 @@ Future<void> testManualTokenInput(WidgetTester tester) async {
// The "subscribe now page" is only shown if the GUI communicates with the background agent.
var l10n = tester.l10n<SubscribeNowPage>();

// 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(
Expand All @@ -82,7 +76,7 @@ Future<void> 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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -118,18 +117,14 @@ void main() {

// The "subscribe now page" is only shown if the GUI communicates with the background agent.
var l10n = tester.l10n<SubscribeNowPage>();
// 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);
await tester.enterText(inputField, 'CJd8MMN8wXSWsv7wJT8c8dDK');
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();

Expand Down Expand Up @@ -166,18 +161,14 @@ void main() {

// The "subscribe now page" is only shown if the GUI communicates with the background agent.
var l10n = tester.l10n<SubscribeNowPage>();
// 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);
await tester.enterText(inputField, 'CJd8MMN8wXSWsv7wJT8c8dDK');
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();

Expand Down
5 changes: 3 additions & 2 deletions gui/packages/ubuntupro/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,58 +61,97 @@ class _ProTokenInputFieldState extends State<ProTokenInputField> {
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: [
Icon(
Icons.cancel,
color: YaruColors.of(context).error,
size: 16.0,
),
const SizedBox(width: 4),
Text(
_token.errorOrNull!.localize(lang)!,
style: theme.textTheme.bodySmall!.copyWith(
color: YaruColors.of(context).error,
),
),
],
),
)
: null,
helper: _token.valueOrNull != null
? Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
children: [
Icon(
Icons.check_circle,
color: YaruColors.of(context).success,
size: 16.0,
),
const SizedBox(width: 4),
Text(
lang.tokenValid,
style: theme.textTheme.bodySmall!.copyWith(
color: YaruColors.of(context).success,
),
),
],
),
)
: 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<TokenError, ProToken?> {
ProTokenValue() : super.ok(null);
ProTokenValue() : super.err(TokenError.empty);

String? get token => valueOrNull?.value;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,20 @@ class SubscriptionStatus extends StatelessWidget {
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFF0E8420), width: 1.0),
color: const Color(0xFFE6F8E8),
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)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(
Icon(
Icons.check_circle_outline_outlined,
color: Color(0xFF0E8420),
color: YaruColors.of(context).success,
size: 24.0,
),
const SizedBox(width: 8.0),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -93,8 +74,11 @@ void main() {
await tester.enterText(inputField, token);
await tester.pump();

final input = tester.firstWidget<TextField>(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<ElevatedButton>(find.byType(ElevatedButton));
Expand All @@ -112,8 +96,11 @@ void main() {
await tester.enterText(inputField, tks.invalidTokens[0]);
await tester.pump();

var input = tester.firstWidget<TextField>(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<ElevatedButton>(find.byType(ElevatedButton));
Expand All @@ -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<TextField>(inputField);
expect(input.decoration!.errorText, isNull);
final input = tester.firstWidget<TextField>(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<TextField>(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<ElevatedButton>(find.byType(ElevatedButton));
Expand Down

0 comments on commit c7baeea

Please sign in to comment.