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 100c01d13..96c8933b1 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 @@ -52,7 +52,7 @@ Future testOrganizationProvidedToken(WidgetTester tester) async { // asserts that we transitioned to the organization-managed status page. final l10n = tester.l10n(); - expect(find.text(l10n.orgManaged), findsOneWidget); + expect(find.text(l10n.manageUbuntuPro), findsNothing); } Future testManualTokenInput(WidgetTester tester) async { @@ -99,5 +99,5 @@ Future testPurchase(WidgetTester tester) async { // asserts that we transitioned to the store-managed status page. l10n = tester.l10n(); - expect(find.text(l10n.storeManaged), findsOneWidget); + expect(find.text(l10n.manageUbuntuPro), findsOneWidget); } diff --git a/gui/packages/ubuntupro/lib/l10n/app_en.arb b/gui/packages/ubuntupro/lib/l10n/app_en.arb index 2487aad19..d5ca62339 100644 --- a/gui/packages/ubuntupro/lib/l10n/app_en.arb +++ b/gui/packages/ubuntupro/lib/l10n/app_en.arb @@ -43,18 +43,9 @@ "getUbuntuPro": "Get Ubuntu Pro", "learnMore": "Learn more", - "subscriptionIsActive": "Your subscription is active!", - "storeManaged": "This subscription is managed through Microsoft Store.", - "manageSubscription": "Manage your subscription", - "orgManaged": "This subscription is owned and managed by your organization.", - "manuallyManaged": "Visit {proDashboardLink} to manage your subscription.", - "@manuallyManaged": { - "placeholders": { - "proDashboardLink": { - "type": "String" - } - } - }, + "ubuntuProEnabled": "Ubuntu Pro is enabled on this machine.", + "ubuntuProEnabledInfo": "All Ubuntu WSL instances have access to Ubuntu Pro security features.", + "manageUbuntuPro": "Manage Ubuntu Pro subscription", "detachPro": "Detach Ubuntu Pro", "updatingSubscriptionInfo": "Updating the subscription information.", diff --git a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_page.dart b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_page.dart index f017bb136..e53c87201 100644 --- a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_page.dart +++ b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_page.dart @@ -1,8 +1,10 @@ import 'package:agentapi/agentapi.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:provider/provider.dart'; import 'package:ubuntu_service/ubuntu_service.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:wizard_router/wizard_router.dart'; import 'package:yaru/yaru.dart'; @@ -24,19 +26,17 @@ class SubscriptionStatusPage extends StatelessWidget { duration: const Duration(milliseconds: 700), child: switch (model) { StoreSubscriptionStatusModel() => SubscriptionStatus( - caption: lang.storeManaged, actionButtons: [ if (model.canConfigureLandscape) _landscapeButton(context), - OutlinedButton( - onPressed: model.launchManagementWebPage, - child: Text(lang.manageSubscription), + ], + footerLinks: [ + MarkdownBody( + data: '[${lang.manageUbuntuPro}]()', + onTapLink: (_, href, __) => model.launchManagementWebPage(), ), ], ), UserSubscriptionStatusModel() => SubscriptionStatus( - caption: lang.manuallyManaged( - '[ubuntu.com/pro/dashboard](https://ubuntu.com/pro/dashboard)', - ), actionButtons: [ if (model.canConfigureLandscape) _landscapeButton(context), ElevatedButton( @@ -59,9 +59,15 @@ class SubscriptionStatusPage extends StatelessWidget { child: Text(lang.detachPro), ), ], + footerLinks: [ + MarkdownBody( + data: '[${lang.manageUbuntuPro}]()', + onTapLink: (_, href, __) => + launchUrlString('https://ubuntu.com/pro/dashboard'), + ), + ], ), OrgSubscriptionStatusModel() => SubscriptionStatus( - caption: lang.orgManaged, actionButtons: model.canConfigureLandscape ? [_landscapeButton(context)] : null, 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 90024acf0..ba7ca553e 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 @@ -1,56 +1,38 @@ import 'package:flutter/material.dart'; 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 '../widgets/page_widgets.dart'; +import '/pages/widgets/page_widgets.dart'; /// A page content widget built on top of the Dark styled landing page showing the current user active subscription /// feedback and an optional action button in a column layout. class SubscriptionStatus extends StatelessWidget { const SubscriptionStatus({ super.key, - required this.caption, this.actionButtons, + this.footerLinks, }); - /// The caption to render below the active subscription subtitle. - final String caption; - /// The optional action button matching the capabilities of the current subscription type. final List? actionButtons; + final List? footerLinks; + @override Widget build(BuildContext context) { final lang = AppLocalizations.of(context); - final theme = Theme.of(context); - final linkStyle = MarkdownStyleSheet.fromTheme( - theme.copyWith( - textTheme: theme.textTheme.copyWith( - bodyMedium: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w100, - ), - ), - ), - ).copyWith( - a: TextStyle( - decoration: TextDecoration.underline, - color: theme.colorScheme.onSurface, - ), - ); - - return LandingPage( - centered: true, + return CenteredPage( + footer: footerLinks != null + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: footerLinks!, + ) + : null, children: [ const SizedBox(height: 16.0), YaruInfoBox( - title: Text(lang.subscriptionIsActive), - subtitle: MarkdownBody( - data: caption, - onTapLink: (_, href, __) => launchUrlString(href!), - styleSheet: linkStyle, - ), + title: Text(lang.ubuntuProEnabled), + subtitle: Text(lang.ubuntuProEnabledInfo), yaruInfoType: YaruInfoType.success, ), if (actionButtons != null) diff --git a/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart b/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart index 9d917b6b6..698b98045 100644 --- a/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart +++ b/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart @@ -69,21 +69,19 @@ class Pro4WSLPage extends StatelessWidget { } } -// A more stylized page that mimics the design of the https://ubuntu.com/pro -// landing page, with a dark background and an [svgAsset] logo followed by -// a title with some opacity, rendering the [children] in a column layout. -class LandingPage extends StatelessWidget { - const LandingPage({ +class CenteredPage extends StatelessWidget { + const CenteredPage({ super.key, required this.children, this.svgAsset = 'assets/Ubuntu-tag.svg', this.title = 'Ubuntu Pro', - this.centered = false, + this.footer, }); + final List children; + final Widget? footer; final String svgAsset; final String title; - final bool centered; @override Widget build(BuildContext context) { @@ -91,81 +89,49 @@ class LandingPage extends StatelessWidget { return Pro4WSLPage( body: Padding( - padding: const EdgeInsets.fromLTRB(32.0, 32.0, 32.0, 8.0), - child: centered - ? Center( + padding: const EdgeInsets.fromLTRB(32.0, 24.0, 32.0, 32.0), + child: Column( + children: [ + Expanded( + child: Center( child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 480.0), - child: _PageContent( - svgAsset: svgAsset, - title: title, - data: theme, - centered: true, - children: children, + constraints: const BoxConstraints(maxWidth: 540.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + RichText( + text: TextSpan( + children: [ + WidgetSpan( + child: SvgPicture.asset( + svgAsset, + height: 70, + ), + ), + const WidgetSpan( + child: SizedBox( + width: 8, + ), + ), + TextSpan( + text: title, + style: theme.textTheme.displaySmall + ?.copyWith(fontWeight: FontWeight.w100), + ), + ], + ), + ), + const SizedBox(height: 16), + ...children, + ], ), ), - ) - : _PageContent( - svgAsset: svgAsset, - title: title, - data: theme, - children: children, ), - ), - ); - } -} - -class _PageContent extends StatelessWidget { - const _PageContent({ - required this.svgAsset, - required this.title, - required ThemeData data, - required this.children, - this.centered = false, - }) : _data = data; - - final String svgAsset; - final String title; - final ThemeData _data; - final List children; - final bool centered; - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: - centered ? CrossAxisAlignment.center : CrossAxisAlignment.start, - mainAxisAlignment: - centered ? MainAxisAlignment.center : MainAxisAlignment.start, - children: [ - RichText( - text: TextSpan( - children: [ - WidgetSpan( - child: SvgPicture.asset( - svgAsset, - height: 70, - ), - ), - const WidgetSpan( - child: SizedBox( - width: 8, - ), - ), - TextSpan( - text: title, - style: _data.textTheme.displaySmall - ?.copyWith(fontWeight: FontWeight.w100), - ), - ], - ), - ), - const SizedBox( - height: 12, + ), + if (footer != null) footer!, + ], ), - ...children, - ], + ), ); } } diff --git a/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_page_test.dart b/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_page_test.dart index e2dacc7e6..1548143bf 100644 --- a/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_page_test.dart +++ b/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_page_test.dart @@ -7,15 +7,59 @@ import 'package:ubuntu_service/ubuntu_service.dart'; import 'package:ubuntupro/core/agent_api_client.dart'; import 'package:ubuntupro/pages/subscription_status/subscription_status_model.dart'; import 'package:ubuntupro/pages/subscription_status/subscription_status_page.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; import 'package:wizard_router/wizard_router.dart'; import '../../utils/build_multiprovider_app.dart'; +import '../../utils/url_launcher_mock.dart'; void main() { group('subscription info:', () { final client = FakeAgentApiClient(); registerServiceInstance(client); final info = SubscriptionInfo(); + + group('footer links:', () { + final landscape = LandscapeSource()..ensureOrganization(); + testWidgets('user', (tester) async { + final launcher = FakeUrlLauncher(); + UrlLauncherPlatform.instance = launcher; + + info.ensureUser(); + final app = buildApp(info, landscape, client); + + await tester.pumpWidget(app); + + final context = tester.element(find.byType(SubscriptionStatusPage)); + final lang = AppLocalizations.of(context); + + expect(launcher.launched, isFalse); + await tester + .tapOnText(find.textRange.ofSubstring(lang.manageUbuntuPro)); + await tester.pump(); + expect(launcher.launched, isTrue); + }); + + testWidgets('store', (tester) async { + final launcher = FakeUrlLauncher(); + UrlLauncherPlatform.instance = launcher; + + info.ensureMicrosoftStore(); + final app = buildApp(info, landscape, client); + + await tester.pumpWidget(app); + + final context = tester.element(find.byType(SubscriptionStatusPage)); + final lang = AppLocalizations.of(context); + + expect(launcher.launched, isFalse); + await tester + .tapOnText(find.textRange.ofSubstring(lang.manageUbuntuPro)); + await tester.pump(); + expect(launcher.launched, isTrue); + }); + }); + group('org landscape:', () { final landscape = LandscapeSource()..ensureOrganization(); testWidgets('user', (tester) async { @@ -27,6 +71,7 @@ void main() { final context = tester.element(find.byType(SubscriptionStatusPage)); final lang = AppLocalizations.of(context); + expect(find.text(lang.manageUbuntuPro), findsOneWidget); expect(find.text(lang.detachPro), findsOneWidget); expect(find.text(lang.landscapeConfigureButton), findsNothing); }); @@ -40,7 +85,7 @@ void main() { final context = tester.element(find.byType(SubscriptionStatusPage)); final lang = AppLocalizations.of(context); - expect(find.text(lang.manageSubscription), findsOneWidget); + expect(find.text(lang.manageUbuntuPro), findsOneWidget); expect(find.text(lang.landscapeConfigureButton), findsNothing); }); @@ -53,10 +98,11 @@ void main() { final context = tester.element(find.byType(SubscriptionStatusPage)); final lang = AppLocalizations.of(context); - expect(find.text(lang.orgManaged), findsOneWidget); + expect(find.text(lang.manageUbuntuPro), findsNothing); expect(find.text(lang.landscapeConfigureButton), findsNothing); }); }); + group('landscape:', () { testWidgets('user', (tester) async { final landscape = LandscapeSource()..ensureNone(); @@ -68,6 +114,7 @@ void main() { final context = tester.element(find.byType(SubscriptionStatusPage)); final lang = AppLocalizations.of(context); + expect(find.text(lang.manageUbuntuPro), findsOneWidget); expect(find.text(lang.detachPro), findsOneWidget); expect(find.text(lang.landscapeConfigureButton), findsOneWidget); }); @@ -82,7 +129,7 @@ void main() { final context = tester.element(find.byType(SubscriptionStatusPage)); final lang = AppLocalizations.of(context); - expect(find.text(lang.manageSubscription), findsOneWidget); + expect(find.text(lang.manageUbuntuPro), findsOneWidget); expect(find.text(lang.landscapeConfigureButton), findsOneWidget); }); @@ -96,7 +143,7 @@ void main() { final context = tester.element(find.byType(SubscriptionStatusPage)); final lang = AppLocalizations.of(context); - expect(find.text(lang.orgManaged), findsOneWidget); + expect(find.text(lang.manageUbuntuPro), findsNothing); expect(find.text(lang.landscapeConfigureButton), findsOneWidget); }); }); @@ -117,6 +164,7 @@ void main() { final context = tester.element(find.byType(SubscriptionStatusPage)); final lang = AppLocalizations.of(context); + expect(find.text(lang.manageUbuntuPro), findsOneWidget); expect(find.text(lang.detachPro), findsOneWidget); expect(find.text(lang.landscapeConfigureButton), findsNothing); }); @@ -136,7 +184,7 @@ void main() { final context = tester.element(find.byType(SubscriptionStatusPage)); final lang = AppLocalizations.of(context); - expect(find.text(lang.manageSubscription), findsOneWidget); + expect(find.text(lang.manageUbuntuPro), findsOneWidget); expect(find.text(lang.landscapeConfigureButton), findsNothing); }); @@ -155,7 +203,7 @@ void main() { final context = tester.element(find.byType(SubscriptionStatusPage)); final lang = AppLocalizations.of(context); - expect(find.text(lang.orgManaged), findsOneWidget); + expect(find.text(lang.manageUbuntuPro), findsNothing); expect(find.text(lang.landscapeConfigureButton), findsNothing); }); }); diff --git a/gui/packages/ubuntupro/test/pages/subscription_status/susbcription_status_widgets_test.dart b/gui/packages/ubuntupro/test/pages/subscription_status/susbcription_status_widgets_test.dart index 2813f4ecc..613697120 100644 --- a/gui/packages/ubuntupro/test/pages/subscription_status/susbcription_status_widgets_test.dart +++ b/gui/packages/ubuntupro/test/pages/subscription_status/susbcription_status_widgets_test.dart @@ -1,22 +1,37 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ubuntupro/pages/subscription_status/subscription_status_widgets.dart'; +import 'package:yaru_test/yaru_test.dart'; import '../../utils/build_multiprovider_app.dart'; void main() { group('subscription status', () { - const caption = 'my caption'; - const buttonName = 'my button'; + const footerText = 'my footer'; + const buttonText = 'my button'; - testWidgets('caption', (tester) async { + testWidgets('footer', (tester) async { + var clicked = false; await tester.pumpWidget( buildSingleRouteMultiProviderApp( - child: const SubscriptionStatus(caption: caption), + child: SubscriptionStatus( + footerLinks: [ + TextButton( + onPressed: () => clicked = true, + child: const Text(footerText), + ), + ], + ), ), ); - expect(find.text(caption), findsOneWidget); + final button = find.button(footerText); + expect(button, findsOneWidget); + + expect(clicked, isFalse); + await tester.tap(button); + await tester.pumpAndSettle(); + expect(clicked, isTrue); }); testWidgets('action button', (tester) async { @@ -24,11 +39,10 @@ void main() { await tester.pumpWidget( buildSingleRouteMultiProviderApp( child: SubscriptionStatus( - caption: caption, actionButtons: [ TextButton( onPressed: () => clicked = true, - child: const Text(buttonName), + child: const Text(buttonText), ), ], ),