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', + }, ); }); 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..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 @@ -27,34 +27,41 @@ class SubscribeNowPage extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - ElevatedButton( - onPressed: () async { - final subs = await model.purchaseSubscription(); + Tooltip( + message: model.purchaseAllowed() + ? '' + : lang.subscribeNowTooltipDisabled, + child: ElevatedButton( + onPressed: model.purchaseAllowed() + ? () 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..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 @@ -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 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 71e4bb3e0..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,6 +17,7 @@ import 'token_samples.dart' as tks; void main() { testWidgets('launch web page', (tester) async { final model = MockSubscribeNowModel(); + when(model.purchaseAllowed()).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.purchaseAllowed()).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.purchaseAllowed()).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.purchaseAllowed()).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.purchaseAllowed()).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.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 42718b383..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 @@ -115,4 +115,12 @@ class MockSubscribeNowModel extends _i1.Mock implements _i5.SubscribeNowModel { )), ) as _i6 .Future<_i4.Either<_i8.PurchaseStatus, _i3.SubscriptionInfo>>); + @override + bool purchaseAllowed() => (super.noSuchMethod( + Invocation.method( + #purchaseAllowed, + [], + ), + 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..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 @@ -97,6 +97,11 @@ void main() { final client = MockAgentApiClient(); + test('disabled by default', () { + final model = SubscribeNowModel(client); + expect(model.purchaseAllowed(), isFalse); + }); + test('expected failure', () async { const expectedError = Left(PurchaseStatus.userGaveUp); pluginMessenger.setMockMethodCallHandler(pluginChannel, (_) async {