From 4d405170badf19ad366af72c8de91abd047eccfd Mon Sep 17 00:00:00 2001
From: ashuntu <ashton.nelson@canonical.com>
Date: Wed, 4 Dec 2024 16:50:10 -0600
Subject: [PATCH] Add Landscape skip page

This is quite a large commit since there are a lot of moving pieces. The existing Landscape page needs its skip functionality removed, the skip page uses an updated version of copy, the skip page uses an entirely new layout, etc.

I plan on expanding the use of reusable navigation and other layout widgets since the new designs will be much more consistent across pages.
---
 gui/packages/ubuntupro/lib/app.dart           |  3 +
 gui/packages/ubuntupro/lib/l10n/app_en.arb    |  7 +-
 .../lib/pages/landscape/landscape_model.dart  |  2 +-
 .../lib/pages/landscape/landscape_page.dart   | 26 +-----
 .../landscape_skip/landscape_skip_page.dart   | 76 ++++++++++++++++
 .../lib/pages/widgets/navigation_row.dart     | 27 ++++++
 .../lib/pages/widgets/page_widgets.dart       | 89 +++++++++++++++++++
 .../lib/pages/widgets/radio_tile.dart         | 37 ++++++++
 gui/packages/ubuntupro/lib/routes.dart        |  1 +
 .../pages/landscape/landscape_page_test.dart  |  1 -
 10 files changed, 243 insertions(+), 26 deletions(-)
 create mode 100644 gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart
 create mode 100644 gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart
 create mode 100644 gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart

diff --git a/gui/packages/ubuntupro/lib/app.dart b/gui/packages/ubuntupro/lib/app.dart
index adaf848a2..749656acf 100644
--- a/gui/packages/ubuntupro/lib/app.dart
+++ b/gui/packages/ubuntupro/lib/app.dart
@@ -11,6 +11,7 @@ import 'core/agent_connection.dart';
 import 'core/agent_monitor.dart';
 import 'core/settings.dart';
 import 'pages/landscape/landscape_page.dart';
+import 'pages/landscape_skip/landscape_skip_page.dart';
 import 'pages/startup/startup_page.dart';
 import 'pages/subscribe_now/subscribe_now_page.dart';
 import 'pages/subscription_status/subscription_status_page.dart';
@@ -76,6 +77,8 @@ class Pro4WSLApp extends StatelessWidget {
                     },
                   ),
                   if (settings.isLandscapeConfigurationEnabled) ...{
+                    Routes.skipLandscape:
+                        WizardRoute(builder: (_) => const LandscapeSkipPage()),
                     Routes.configureLandscape:
                         const WizardRoute(builder: LandscapePage.create),
                     Routes.subscriptionStatus: WizardRoute(
diff --git a/gui/packages/ubuntupro/lib/l10n/app_en.arb b/gui/packages/ubuntupro/lib/l10n/app_en.arb
index 1cbdf00a9..af2b8692b 100644
--- a/gui/packages/ubuntupro/lib/l10n/app_en.arb
+++ b/gui/packages/ubuntupro/lib/l10n/app_en.arb
@@ -57,7 +57,12 @@
     "purchaseStatusServer":"Something went wrong with Microsoft Store. Please try again later.",
     "purchaseStatusUnknown": "Unknown error when trying to purchase the subscription. Consider restarting this app.",
 
-    "landscapeHeading": "Configure the connection to {landscapeLink} to manage your Ubuntu WSL instances remotely.",
+    "landscapeSkip": "Skip for now",
+    "landscapeSkipDescription": "You can always configure Landscape later",
+    "landscapeSkipRegister": "Register with Landscape",
+
+    "landscapeTitle": "Landscape",
+    "landscapeHeading": "Configure the connection to Landscape to manage your Ubuntu WSL instances remotely. {landscapeLink}",
     "@landscapeHeading": {
         "placeholders": {
             "landscapeLink": {
diff --git a/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart b/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart
index a3f6426d5..0e3cc6145 100644
--- a/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart
+++ b/gui/packages/ubuntupro/lib/pages/landscape/landscape_model.dart
@@ -19,7 +19,7 @@ class LandscapeModel extends ChangeNotifier {
   LandscapeModel(this.client);
 
   /// The URL to be shown in the UI.
-  final landscapeURI = Uri.https('ubuntu.com', '/landscape');
+  static final landscapeURI = Uri.https('ubuntu.com', '/landscape');
 
   /// Whether the current form is complete (ready to be submitted).
   bool get isComplete => _active.isComplete;
diff --git a/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart b/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart
index c2df76073..84bf06b30 100644
--- a/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart
+++ b/gui/packages/ubuntupro/lib/pages/landscape/landscape_page.dart
@@ -25,7 +25,6 @@ class LandscapePage extends StatelessWidget {
     super.key,
     required this.onApplyConfig,
     required this.onBack,
-    required this.onSkip,
   });
 
   /// Callable invoked when this page successfully applies the configuration.
@@ -34,9 +33,6 @@ class LandscapePage extends StatelessWidget {
   /// Callable invoked when the user navigates back.
   final void Function() onBack;
 
-  /// Callable invoked when the user skips this page.
-  final void Function() onSkip;
-
   @override
   Widget build(BuildContext context) {
     final lang = AppLocalizations.of(context);
@@ -49,21 +45,17 @@ class LandscapePage extends StatelessWidget {
           ),
         ),
       ),
-    ).copyWith(
-      a: const TextStyle(
-        decoration: TextDecoration.underline,
-      ),
     );
 
     return LandingPage(
       svgAsset: 'assets/Landscape-tag.svg',
-      title: 'Landscape',
+      title: lang.landscapeTitle,
       children: [
         // Only rebuilds if the value of model.landscapeURI changes (never in production)
         Selector<LandscapeModel, Uri>(
-          selector: (_, model) => model.landscapeURI,
+          selector: (_, model) => LandscapeModel.landscapeURI,
           builder: (context, uri, _) => MarkdownBody(
-            data: lang.landscapeHeading('[Landscape]($uri)'),
+            data: lang.landscapeHeading('[${lang.learnMore}]($uri)'),
             onTapLink: (_, href, __) => launchUrl(uri),
             styleSheet: linkStyle,
           ),
@@ -79,7 +71,6 @@ class LandscapePage extends StatelessWidget {
           selector: (_, model) => model.isComplete,
           builder: (context, isComplete, _) => _NavigationButtonRow(
             onBack: onBack,
-            onSkip: onSkip,
             onNext: isComplete ? () => _tryApplyConfig(context) : null,
           ),
         ),
@@ -114,13 +105,11 @@ class LandscapePage extends StatelessWidget {
       landscapePage = LandscapePage(
         onApplyConfig: Wizard.of(context).back,
         onBack: Wizard.of(context).back,
-        onSkip: Wizard.of(context).back,
       );
     } else {
       landscapePage = LandscapePage(
         onApplyConfig: Wizard.of(context).next,
         onBack: Wizard.of(context).back,
-        onSkip: Wizard.of(context).next,
       );
     }
 
@@ -242,12 +231,10 @@ class LandscapeConfigForm extends StatelessWidget {
 class _NavigationButtonRow extends StatelessWidget {
   const _NavigationButtonRow({
     this.onBack,
-    this.onSkip,
     this.onNext,
   });
 
   final void Function()? onBack;
-  final void Function()? onSkip;
   final void Function()? onNext;
 
   @override
@@ -261,13 +248,6 @@ class _NavigationButtonRow extends StatelessWidget {
           child: Text(lang.buttonBack),
         ),
         const Spacer(),
-        FilledButton(
-          onPressed: onSkip,
-          child: Text(lang.buttonSkip),
-        ),
-        const SizedBox(
-          width: 16.0,
-        ),
         ElevatedButton(
           onPressed: onNext,
           child: Text(lang.buttonNext),
diff --git a/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart b/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart
new file mode 100644
index 000000000..42811dfc1
--- /dev/null
+++ b/gui/packages/ubuntupro/lib/pages/landscape_skip/landscape_skip_page.dart
@@ -0,0 +1,76 @@
+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.dart';
+import 'package:wizard_router/wizard_router.dart';
+
+import '../../routes.dart';
+import '../landscape/landscape_model.dart';
+import '../widgets/navigation_row.dart';
+import '../widgets/page_widgets.dart';
+import '../widgets/radio_tile.dart';
+
+enum _SkipEnum {
+  skip,
+  register,
+}
+
+class LandscapeSkipPage extends StatefulWidget {
+  const LandscapeSkipPage({super.key});
+
+  @override
+  State<LandscapeSkipPage> createState() => _LandscapeSkipPageState();
+}
+
+class _LandscapeSkipPageState extends State<LandscapeSkipPage> {
+  _SkipEnum groupValue = _SkipEnum.skip;
+
+  @override
+  Widget build(BuildContext context) {
+    final lang = AppLocalizations.of(context);
+    final wizard = Wizard.of(context);
+
+    return ColumnPage(
+      svgAsset: 'assets/Landscape-tag.svg',
+      title: lang.landscapeTitle,
+      left: [
+        MarkdownBody(
+          data: lang.landscapeHeading(
+            '[${lang.learnMore}](${LandscapeModel.landscapeURI})',
+          ),
+          onTapLink: (_, href, __) => launchUrl(LandscapeModel.landscapeURI),
+        ),
+      ],
+      right: [
+        RadioTile(
+          value: _SkipEnum.skip,
+          title: lang.landscapeSkip,
+          subtitle: lang.landscapeSkipDescription,
+          groupValue: groupValue,
+          onChanged: (v) => setState(() {
+            groupValue = v!;
+          }),
+        ),
+        RadioTile(
+          value: _SkipEnum.register,
+          title: lang.landscapeSkipRegister,
+          groupValue: groupValue,
+          onChanged: (v) => setState(() {
+            groupValue = v!;
+          }),
+        ),
+      ],
+      navigationRow: NavigationRow(
+        onBack: wizard.back,
+        onNext: () {
+          switch (groupValue) {
+            case _SkipEnum.skip:
+              wizard.jump(Routes.subscriptionStatus);
+            case _SkipEnum.register:
+              wizard.next();
+          }
+        },
+      ),
+    );
+  }
+}
diff --git a/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart b/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart
new file mode 100644
index 000000000..f9269c875
--- /dev/null
+++ b/gui/packages/ubuntupro/lib/pages/widgets/navigation_row.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+
+class NavigationRow extends StatelessWidget {
+  const NavigationRow({
+    required this.onBack,
+    required this.onNext,
+    this.backText,
+    this.nextText,
+    super.key,
+  });
+
+  final void Function()? onBack;
+  final String? backText;
+  final void Function()? onNext;
+  final String? nextText;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+      children: [
+        OutlinedButton(onPressed: onBack, child: Text(backText ?? 'Back')),
+        FilledButton(onPressed: onNext, child: Text(nextText ?? 'Next')),
+      ],
+    );
+  }
+}
diff --git a/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart b/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart
index a27003310..432313c5e 100644
--- a/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart
+++ b/gui/packages/ubuntupro/lib/pages/widgets/page_widgets.dart
@@ -27,6 +27,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
 import 'package:flutter_svg/flutter_svg.dart';
 import 'package:yaru/yaru.dart';
 
+import 'navigation_row.dart';
 import 'status_bar.dart';
 
 /// The simplest material page that covers most of the use cases in this app,
@@ -168,3 +169,91 @@ class _PageContent extends StatelessWidget {
     );
   }
 }
+
+/// Two-column, vertically centered page. The left column always contains the
+/// svg image and title, with the left children below it. Both columns are equal
+/// in width. Optionally, a [NavigationRow] may be provided that will span the
+/// width below both columns.
+class ColumnPage extends StatelessWidget {
+  const ColumnPage({
+    required this.left,
+    required this.right,
+    this.svgAsset = 'assets/Ubuntu-tag.svg',
+    this.title = 'Ubuntu Pro',
+    this.navigationRow,
+    super.key,
+  });
+
+  final List<Widget> left;
+  final List<Widget> right;
+  final String svgAsset;
+  final String title;
+  final NavigationRow? navigationRow;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+
+    return Pro4WSLPage(
+      body: Padding(
+        padding: const EdgeInsets.fromLTRB(32.0, 32.0, 32.0, 32.0),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Expanded(
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: [
+                  // Left column
+                  Expanded(
+                    child: Column(
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      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: 24),
+                        ...left,
+                      ],
+                    ),
+                  ),
+                  // Spacer
+                  const SizedBox(width: 32),
+                  // Right column
+                  Expanded(
+                    child: Column(
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: right,
+                    ),
+                  ),
+                ],
+              ),
+            ),
+            if (navigationRow != null) navigationRow!,
+          ],
+        ),
+      ),
+    );
+  }
+}
diff --git a/gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart b/gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart
new file mode 100644
index 000000000..6220ca6a3
--- /dev/null
+++ b/gui/packages/ubuntupro/lib/pages/widgets/radio_tile.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/material.dart';
+import 'package:yaru/yaru.dart';
+
+class RadioTile<T> extends StatelessWidget {
+  const RadioTile({
+    required this.value,
+    required this.title,
+    required this.groupValue,
+    required this.onChanged,
+    this.subtitle,
+    super.key,
+  });
+
+  final String title;
+  final String? subtitle;
+  final T value, groupValue;
+  final Function(T?)? onChanged;
+
+  @override
+  Widget build(BuildContext context) {
+    // Adds a nice visual clue that the tile is selected.
+    return YaruSelectableContainer(
+      selected: groupValue == value,
+      selectionColor: Theme.of(context).colorScheme.tertiaryContainer,
+      child: YaruRadioListTile(
+        contentPadding: EdgeInsets.zero,
+        visualDensity: VisualDensity.comfortable,
+        dense: true,
+        title: Text(title),
+        subtitle: subtitle != null ? Text(subtitle!) : null,
+        value: value,
+        groupValue: groupValue,
+        onChanged: onChanged,
+      ),
+    );
+  }
+}
diff --git a/gui/packages/ubuntupro/lib/routes.dart b/gui/packages/ubuntupro/lib/routes.dart
index 28a9c61cd..248b69226 100644
--- a/gui/packages/ubuntupro/lib/routes.dart
+++ b/gui/packages/ubuntupro/lib/routes.dart
@@ -1,6 +1,7 @@
 abstract class Routes {
   static const startup = '/startup';
   static const subscribeNow = '/subscribe-now';
+  static const skipLandscape = '/skip-landscape';
   static const configureLandscape = '/configure-landscape';
   static const subscriptionStatus = '/subscription-status';
   static const configureLandscapeLate = '/configure-landscape-late';
diff --git a/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart b/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart
index 65e34df52..087d4a208 100644
--- a/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart
+++ b/gui/packages/ubuntupro/test/pages/landscape/landscape_page_test.dart
@@ -403,7 +403,6 @@ Widget buildApp(
   return buildSingleRouteMultiProviderApp(
     child: LandscapePage(
       onApplyConfig: () {},
-      onSkip: () {},
       onBack: () {},
     ),
     providers: [ChangeNotifierProvider<LandscapeModel>.value(value: model)],