From f2fd2401042145c450b90b7913a7caae37a287b2 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 16 Oct 2023 12:03:10 -0300 Subject: [PATCH 1/4] Disables the "Subscribe Now" CTA button by default For the private beta we've been advised to avoid the usage of MS Store purchase. The intended customers will use the scaled deployment. A greyed out button requires some explanation IMHO, Thus the tooltip. Only rendered in the disabled case. --- gui/packages/ubuntupro/lib/l10n/app_en.arb | 1 + .../subscribe_now_page.dart | 59 +++++++++++-------- .../subscription_status_model.dart | 10 ++++ 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/gui/packages/ubuntupro/lib/l10n/app_en.arb b/gui/packages/ubuntupro/lib/l10n/app_en.arb index 525e2209b..2dbd6441b 100644 --- a/gui/packages/ubuntupro/lib/l10n/app_en.arb +++ b/gui/packages/ubuntupro/lib/l10n/app_en.arb @@ -29,6 +29,7 @@ "proHeading": "The most comprehensive subscription\nfor open-source software security\nnow available on WSL", "subscribeNow": "Subscribe Now", + "subscribeNowTooltipDisabled": "Coming soon", "learnMore": "Learn More", "subscriptionIsActive": "Your subscription is active!", diff --git a/gui/packages/ubuntupro/lib/pages/subscription_status/subscribe_now_page.dart b/gui/packages/ubuntupro/lib/pages/subscription_status/subscribe_now_page.dart index efaf8475c..aa75c1fab 100644 --- a/gui/packages/ubuntupro/lib/pages/subscription_status/subscribe_now_page.dart +++ b/gui/packages/ubuntupro/lib/pages/subscription_status/subscribe_now_page.dart @@ -27,34 +27,41 @@ class SubscribeNowPage extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - ElevatedButton( - onPressed: () async { - final subs = await model.purchaseSubscription(); + Tooltip( + message: model.isPurchaseAllowed() + ? '' + : lang.subscribeNowTooltipDisabled, + child: ElevatedButton( + onPressed: model.isPurchaseAllowed() + ? () async { + final subs = await model.purchaseSubscription(); - // Using anything attached to the BuildContext after a suspension point might be tricky. - // Better check if it's still mounted in the widget tree. - if (!context.mounted) return; + // Using anything attached to the BuildContext after a suspension point might be tricky. + // Better check if it's still mounted in the widget tree. + if (!context.mounted) return; - subs.fold( - ifLeft: (status) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Center( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 2.0, - horizontal: 16.0, - ), - child: Text(status.localize(lang)), - ), - ), - ), - ); - }, - ifRight: onSubscriptionUpdate, - ); - }, - child: Text(lang.subscribeNow), + subs.fold( + ifLeft: (status) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Center( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 2.0, + horizontal: 16.0, + ), + child: Text(status.localize(lang)), + ), + ), + ), + ); + }, + ifRight: onSubscriptionUpdate, + ); + } + : null, + child: Text(lang.subscribeNow), + ), ), const Padding(padding: EdgeInsets.only(right: 8.0)), FilledButton.tonal( diff --git a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_model.dart b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_model.dart index 4cd6c86dc..2171412e5 100644 --- a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_model.dart +++ b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_model.dart @@ -6,6 +6,7 @@ import 'package:url_launcher/url_launcher.dart'; import '/core/agent_api_client.dart'; import '/core/pro_token.dart'; +import '../../core/environment.dart'; /// A base class for the view-models that may represent different types of subscriptions and the optional actions they allow. sealed class SubscriptionStatusModel { @@ -69,6 +70,7 @@ class OrgSubscriptionStatusModel extends SubscriptionStatusModel { class SubscribeNowModel extends SubscriptionStatusModel { final AgentApiClient client; + bool? _isPurchaseAllowed; SubscribeNowModel(this.client) : super._(); Future applyProToken(ProToken token) async { @@ -99,4 +101,12 @@ class SubscribeNowModel extends SubscriptionStatusModel { return PurchaseStatus.unknown.left(); } } + + /// Returns true if the environment variable 'UP4W_ALLOW_STORE_PURCHASE' has been set. + /// Since this reading won't change during the app lifetime, even if the user changes + /// it's value from outside, the value is cached so we don't check the environment more than once. + bool isPurchaseAllowed() { + return _isPurchaseAllowed ??= ['true', '1', 'on'] + .contains(Environment()['UP4W_ALLOW_STORE_PURCHASE']?.toLowerCase()); + } } From c1eb3c8254d300aa08219afff4ca43d18205a899 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 16 Oct 2023 12:51:54 -0300 Subject: [PATCH 2/4] Updates page & model tests --- .../subscribe_now_page_test.dart | 38 +++++++++++++++++++ .../subscribe_now_page_test.mocks.dart | 8 ++++ .../subscription_status_model_test.dart | 5 +++ 3 files changed, 51 insertions(+) diff --git a/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.dart b/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.dart index 71e4bb3e0..ad7b0c792 100644 --- a/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.dart +++ b/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.dart @@ -17,6 +17,7 @@ import 'token_samples.dart' as tks; void main() { testWidgets('launch web page', (tester) async { final model = MockSubscribeNowModel(); + when(model.isPurchaseAllowed()).thenReturn(true); var called = false; when(model.launchProWebPage()).thenAnswer((_) async { called = true; @@ -32,9 +33,44 @@ void main() { await tester.pump(); expect(called, isTrue); }); + group('purchase button enabled by model', () { + testWidgets('disabled', (tester) async { + final model = MockSubscribeNowModel(); + when(model.isPurchaseAllowed()).thenReturn(false); + final app = buildApp(model, (_) {}); + await tester.pumpWidget(app); + final context = tester.element(find.byType(SubscribeNowPage)); + final lang = AppLocalizations.of(context); + + final button = find.byType(ElevatedButton); + // check that's the right button + expect( + find.descendant(of: button, matching: find.text(lang.subscribeNow)), + findsOneWidget, + ); + expect(tester.widget(button).enabled, isFalse); + }); + testWidgets('enabled', (tester) async { + final model = MockSubscribeNowModel(); + when(model.isPurchaseAllowed()).thenReturn(true); + final app = buildApp(model, (_) {}); + await tester.pumpWidget(app); + final context = tester.element(find.byType(SubscribeNowPage)); + final lang = AppLocalizations.of(context); + + final button = find.byType(ElevatedButton); + // check that's the right button + expect( + find.descendant(of: button, matching: find.text(lang.subscribeNow)), + findsOneWidget, + ); + expect(tester.widget(button).enabled, isTrue); + }); + }); group('subscribe', () { testWidgets('calls back on success', (tester) async { final model = MockSubscribeNowModel(); + when(model.isPurchaseAllowed()).thenReturn(true); var called = false; when(model.purchaseSubscription()).thenAnswer((_) async { final info = SubscriptionInfo()..ensureMicrosoftStore(); @@ -57,6 +93,7 @@ void main() { testWidgets('feedback on error', (tester) async { const purchaseError = PurchaseStatus.networkError; final model = MockSubscribeNowModel(); + when(model.isPurchaseAllowed()).thenReturn(true); var called = false; when(model.purchaseSubscription()).thenAnswer((_) async { return purchaseError.left(); @@ -79,6 +116,7 @@ void main() { }); testWidgets('feedback when applying token', (tester) async { final model = MockSubscribeNowModel(); + when(model.isPurchaseAllowed()).thenReturn(true); when(model.applyProToken(any)).thenAnswer((_) async { return SubscriptionInfo()..ensureUser(); }); diff --git a/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.mocks.dart b/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.mocks.dart index 42718b383..573f84bdd 100644 --- a/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.mocks.dart +++ b/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.mocks.dart @@ -115,4 +115,12 @@ class MockSubscribeNowModel extends _i1.Mock implements _i5.SubscribeNowModel { )), ) as _i6 .Future<_i4.Either<_i8.PurchaseStatus, _i3.SubscriptionInfo>>); + @override + bool isPurchaseAllowed() => (super.noSuchMethod( + Invocation.method( + #isPurchaseAllowed, + [], + ), + returnValue: false, + ) as bool); } diff --git a/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_model_test.dart b/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_model_test.dart index eb7790d85..71aafac2d 100644 --- a/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_model_test.dart +++ b/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_model_test.dart @@ -97,6 +97,11 @@ void main() { final client = MockAgentApiClient(); + test('disabled by default', () { + final model = SubscribeNowModel(client); + expect(model.isPurchaseAllowed(), isFalse); + }); + test('expected failure', () async { const expectedError = Left(PurchaseStatus.userGaveUp); pluginMessenger.setMockMethodCallHandler(pluginChannel, (_) async { From ad0f13f4020ba9451fcd75653607ce95ba949034 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 16 Oct 2023 15:59:47 -0300 Subject: [PATCH 3/4] Updates the integration test But not yet e2e. We don't have one with the purchase scenario just yet. --- .../integration_test/ubuntu_pro_for_windows_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_windows_test.dart b/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_windows_test.dart index 605d6dc62..1352094e7 100644 --- a/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_windows_test.dart +++ b/gui/packages/ubuntupro/integration_test/ubuntu_pro_for_windows_test.dart @@ -35,7 +35,10 @@ void main() { // Use a random place inside the build tree as the `LOCALAPPDATA` env variable for all test cases below. tmp = await msixRootDir().createTemp('test-'); Environment( - overrides: {'LOCALAPPDATA': tmp!.path}, + overrides: { + 'LOCALAPPDATA': tmp!.path, + 'UP4W_ALLOW_STORE_PURCHASE': '1', + }, ); }); From b52f3ab73625df8a9cf569525fc671a872c2e8e3 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 17 Oct 2023 10:13:59 -0300 Subject: [PATCH 4/4] isPurchaseAllowed should be an answer ... instead of a question. --- .../subscription_status/subscribe_now_page.dart | 4 ++-- .../subscription_status_model.dart | 2 +- .../subscription_status/subscribe_now_page_test.dart | 12 ++++++------ .../subscribe_now_page_test.mocks.dart | 4 ++-- .../subscription_status_model_test.dart | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/gui/packages/ubuntupro/lib/pages/subscription_status/subscribe_now_page.dart b/gui/packages/ubuntupro/lib/pages/subscription_status/subscribe_now_page.dart index aa75c1fab..559290e0f 100644 --- a/gui/packages/ubuntupro/lib/pages/subscription_status/subscribe_now_page.dart +++ b/gui/packages/ubuntupro/lib/pages/subscription_status/subscribe_now_page.dart @@ -28,11 +28,11 @@ class SubscribeNowPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, children: [ Tooltip( - message: model.isPurchaseAllowed() + message: model.purchaseAllowed() ? '' : lang.subscribeNowTooltipDisabled, child: ElevatedButton( - onPressed: model.isPurchaseAllowed() + onPressed: model.purchaseAllowed() ? () async { final subs = await model.purchaseSubscription(); diff --git a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_model.dart b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_model.dart index 2171412e5..fea939ac5 100644 --- a/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_model.dart +++ b/gui/packages/ubuntupro/lib/pages/subscription_status/subscription_status_model.dart @@ -105,7 +105,7 @@ class SubscribeNowModel extends SubscriptionStatusModel { /// Returns true if the environment variable 'UP4W_ALLOW_STORE_PURCHASE' has been set. /// Since this reading won't change during the app lifetime, even if the user changes /// it's value from outside, the value is cached so we don't check the environment more than once. - bool isPurchaseAllowed() { + bool purchaseAllowed() { return _isPurchaseAllowed ??= ['true', '1', 'on'] .contains(Environment()['UP4W_ALLOW_STORE_PURCHASE']?.toLowerCase()); } diff --git a/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.dart b/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.dart index ad7b0c792..c24d8c3df 100644 --- a/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.dart +++ b/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.dart @@ -17,7 +17,7 @@ import 'token_samples.dart' as tks; void main() { testWidgets('launch web page', (tester) async { final model = MockSubscribeNowModel(); - when(model.isPurchaseAllowed()).thenReturn(true); + when(model.purchaseAllowed()).thenReturn(true); var called = false; when(model.launchProWebPage()).thenAnswer((_) async { called = true; @@ -36,7 +36,7 @@ void main() { group('purchase button enabled by model', () { testWidgets('disabled', (tester) async { final model = MockSubscribeNowModel(); - when(model.isPurchaseAllowed()).thenReturn(false); + when(model.purchaseAllowed()).thenReturn(false); final app = buildApp(model, (_) {}); await tester.pumpWidget(app); final context = tester.element(find.byType(SubscribeNowPage)); @@ -52,7 +52,7 @@ void main() { }); testWidgets('enabled', (tester) async { final model = MockSubscribeNowModel(); - when(model.isPurchaseAllowed()).thenReturn(true); + when(model.purchaseAllowed()).thenReturn(true); final app = buildApp(model, (_) {}); await tester.pumpWidget(app); final context = tester.element(find.byType(SubscribeNowPage)); @@ -70,7 +70,7 @@ void main() { group('subscribe', () { testWidgets('calls back on success', (tester) async { final model = MockSubscribeNowModel(); - when(model.isPurchaseAllowed()).thenReturn(true); + when(model.purchaseAllowed()).thenReturn(true); var called = false; when(model.purchaseSubscription()).thenAnswer((_) async { final info = SubscriptionInfo()..ensureMicrosoftStore(); @@ -93,7 +93,7 @@ void main() { testWidgets('feedback on error', (tester) async { const purchaseError = PurchaseStatus.networkError; final model = MockSubscribeNowModel(); - when(model.isPurchaseAllowed()).thenReturn(true); + when(model.purchaseAllowed()).thenReturn(true); var called = false; when(model.purchaseSubscription()).thenAnswer((_) async { return purchaseError.left(); @@ -116,7 +116,7 @@ void main() { }); testWidgets('feedback when applying token', (tester) async { final model = MockSubscribeNowModel(); - when(model.isPurchaseAllowed()).thenReturn(true); + when(model.purchaseAllowed()).thenReturn(true); when(model.applyProToken(any)).thenAnswer((_) async { return SubscriptionInfo()..ensureUser(); }); diff --git a/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.mocks.dart b/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.mocks.dart index 573f84bdd..491307ba5 100644 --- a/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.mocks.dart +++ b/gui/packages/ubuntupro/test/pages/subscription_status/subscribe_now_page_test.mocks.dart @@ -116,9 +116,9 @@ class MockSubscribeNowModel extends _i1.Mock implements _i5.SubscribeNowModel { ) as _i6 .Future<_i4.Either<_i8.PurchaseStatus, _i3.SubscriptionInfo>>); @override - bool isPurchaseAllowed() => (super.noSuchMethod( + bool purchaseAllowed() => (super.noSuchMethod( Invocation.method( - #isPurchaseAllowed, + #purchaseAllowed, [], ), returnValue: false, diff --git a/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_model_test.dart b/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_model_test.dart index 71aafac2d..af164fa7c 100644 --- a/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_model_test.dart +++ b/gui/packages/ubuntupro/test/pages/subscription_status/subscription_status_model_test.dart @@ -99,7 +99,7 @@ void main() { test('disabled by default', () { final model = SubscribeNowModel(client); - expect(model.isPurchaseAllowed(), isFalse); + expect(model.purchaseAllowed(), isFalse); }); test('expected failure', () async {