Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gui): Conditionally hide the Landscape Config page based on registry #835

Merged
merged 7 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ void main() {
overrides: {
'USERPROFILE': tmpHome!.path,
'LOCALAPPDATA': tmpLocalAppData!.path,
'UP4W_ALLOW_STORE_PURCHASE': '1',
'UP4W_INTEGRATION_TESTING': 'true',
},
);
});
Expand Down
39 changes: 26 additions & 13 deletions gui/packages/ubuntupro/lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import 'package:yaru/yaru.dart';
import 'core/agent_api_client.dart';
import 'core/agent_connection.dart';
import 'core/agent_monitor.dart';
import 'core/settings.dart';
import 'pages/landscape/landscape_page.dart';
import 'pages/startup/startup_page.dart';
import 'pages/subscribe_now/subscribe_now_page.dart';
import 'pages/subscription_status/subscription_status_page.dart';
import 'routes.dart';

class Pro4WSLApp extends StatelessWidget {
const Pro4WSLApp(this.agentMonitor, {super.key});
const Pro4WSLApp(this.agentMonitor, this.settings, {super.key});
final AgentStartupMonitor agentMonitor;
final Settings settings;

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -62,6 +64,7 @@ class Pro4WSLApp extends StatelessWidget {
),
Routes.subscribeNow: WizardRoute(
builder: SubscribeNowPage.create,
userData: settings.isStorePurchaseAllowed,
onNext: (_) {
final src = context.read<ValueNotifier<ConfigSources>>();
final landscape = src.value.landscapeSource;
Expand All @@ -72,19 +75,29 @@ class Pro4WSLApp extends StatelessWidget {
return null;
},
),
Routes.configureLandscape:
const WizardRoute(builder: LandscapePage.create),
Routes.subscriptionStatus: WizardRoute(
builder: SubscriptionStatusPage.create,
onReplace: (_) => Routes.subscribeNow,
onBack: (_) => Routes.subscribeNow,
),
Routes.configureLandscapeLate: WizardRoute(
builder: (context) => LandscapePage.create(
context,
isLate: true,
if (settings.isLandscapeConfigurationEnabled) ...{
Routes.configureLandscape:
const WizardRoute(builder: LandscapePage.create),
Routes.subscriptionStatus: WizardRoute(
builder: SubscriptionStatusPage.create,
onReplace: (_) => Routes.subscribeNow,
onBack: (_) => Routes.subscribeNow,
userData: true,
),
),
Routes.configureLandscapeLate: WizardRoute(
builder: (context) => LandscapePage.create(
context,
isLate: true,
),
),
} else ...{
Routes.subscriptionStatus: WizardRoute(
builder: SubscriptionStatusPage.create,
onReplace: (_) => Routes.subscribeNow,
onBack: (_) => Routes.subscribeNow,
userData: false,
),
},
},
);
},
Expand Down
82 changes: 82 additions & 0 deletions gui/packages/ubuntupro/lib/core/settings.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'package:flutter/foundation.dart';
import 'package:win32/win32.dart';
import 'package:win32_registry/win32_registry.dart';

/// Manages the settings for the user interface.
class Settings {
/// Creates a new instance of [Settings] initialized with options read from [repository],
/// which is loaded, read from and closed.
Settings(SettingsRepository repository) {
if (!repository.load()) return;

final purchase = repository.readInt(kAllowStorePurchase) == 1
? Options.withStorePurchase
: Options.none;
final landscape = repository.readInt(kShowLandscapeConfig) == 1
? Options.withLandscapeConfiguration
: Options.none;

repository.close();

_options = purchase | landscape;
}

/// Creates a new instance of [Settings] with the specified [options], thus no need to read from the repository.
/// Useful for integration testing.
Settings.withOptions(this._options);

Options _options = Options.none;

bool get isLandscapeConfigurationEnabled =>
_options & Options.withLandscapeConfiguration;
bool get isStorePurchaseAllowed => _options & Options.withStorePurchase;

// constants for the key names only exposed for testing.
@visibleForTesting
static const kAllowStorePurchase = 'AllowStorePurchase';
@visibleForTesting
static const kShowLandscapeConfig = 'ShowLandscapeConfig';
}

/// Settings options modelled as an enum with bitwise operations, i.e. flags.
enum Options {
none(0x00),
withLandscapeConfiguration(0x01),
withStorePurchase(0x02),
// all optionss above or'ed.
withAll(0x03);

final int options;
const Options(this.options);
factory Options._fromInt(int options) =>
Options.values.firstWhere((e) => e.options == options);

bool operator &(Options other) => options & other.options != 0;
Options operator |(Options other) =>
Options._fromInt(options | other.options);
}

// "Abstracts" reading the settings storage (a.k.a the Windows registry).
class SettingsRepository {
RegistryKey? _key;

void close() => _key?.close();
int? readInt(String name) {
if (_key == null) return null;
return _key!.getValueAsInt(name);

Check warning on line 66 in gui/packages/ubuntupro/lib/core/settings.dart

View check run for this annotation

Codecov / codecov/patch

gui/packages/ubuntupro/lib/core/settings.dart#L66

Added line #L66 was not covered by tests
}

bool load() {

Check warning on line 69 in gui/packages/ubuntupro/lib/core/settings.dart

View check run for this annotation

Codecov / codecov/patch

gui/packages/ubuntupro/lib/core/settings.dart#L69

Added line #L69 was not covered by tests
try {
_key = Registry.openPath(RegistryHive.currentUser, path: _keyPath);

Check warning on line 71 in gui/packages/ubuntupro/lib/core/settings.dart

View check run for this annotation

Codecov / codecov/patch

gui/packages/ubuntupro/lib/core/settings.dart#L71

Added line #L71 was not covered by tests
return true;
} on WindowsException {

Check warning on line 73 in gui/packages/ubuntupro/lib/core/settings.dart

View check run for this annotation

Codecov / codecov/patch

gui/packages/ubuntupro/lib/core/settings.dart#L73

Added line #L73 was not covered by tests
// missing key is not an error since we expect them to be set in very few cases.
// TODO: Log error cases other than ERROR_FILE_NOT_FOUND.
return false;
}
}
}

// The registry key we want to read from.
const _keyPath = r'SOFTWARE\Canonical\UbuntuPro\';
9 changes: 8 additions & 1 deletion gui/packages/ubuntupro/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import 'app.dart';
import 'constants.dart';
import 'core/agent_api_client.dart';
import 'core/agent_monitor.dart';
import 'core/environment.dart';
import 'core/settings.dart';
import 'launch_agent.dart';

Future<void> main() async {
Expand All @@ -20,7 +22,12 @@ Future<void> main() async {
clientFactory: AgentApiClient.new,
onClient: registerServiceInstance<AgentApiClient>,
);
runApp(Pro4WSLApp(agentMonitor));

final settings = Environment()['UP4W_INTEGRATION_TESTING'] != null
? Settings.withOptions(Options.withAll)
: Settings(SettingsRepository());

runApp(Pro4WSLApp(agentMonitor, settings));
}

Future<bool> launch() => launchAgent(kAgentRelativePath);
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class LandscapeInput extends StatelessWidget {
],
),
const SizedBox(
height: 32.0,
height: 16.0,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
Expand Down Expand Up @@ -314,7 +314,7 @@ class _FileFormState extends State<_FileForm> {
style: widget.sectionBodyStyle,
),
const SizedBox(
height: 16.0,
height: 8.0,
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import 'package:flutter/foundation.dart';
import 'package:p4w_ms_store/p4w_ms_store.dart';
import 'package:url_launcher/url_launcher.dart';
import '/core/agent_api_client.dart';
import '/core/environment.dart';
import '/core/pro_token.dart';

class SubscribeNowModel {
final AgentApiClient client;
bool? _isPurchaseAllowed;
SubscribeNowModel(this.client);
final bool _isPurchaseAllowed;
SubscribeNowModel(this.client, {bool isPurchaseAllowed = false})
: _isPurchaseAllowed = isPurchaseAllowed;

Future<SubscriptionInfo> applyProToken(ProToken token) {
return client.applyProToken(token.value);
Expand Down Expand Up @@ -43,8 +43,5 @@ class SubscribeNowModel {
/// 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());
}
bool get purchaseAllowed => _isPurchaseAllowed;
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ class SubscribeNowPage extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
children: [
Tooltip(
message: model.purchaseAllowed()
? ''
: lang.subscribeNowTooltipDisabled,
message:
model.purchaseAllowed ? '' : lang.subscribeNowTooltipDisabled,
child: ElevatedButton(
onPressed: model.purchaseAllowed()
onPressed: model.purchaseAllowed
? () async {
final subs = await model.purchaseSubscription();

Expand Down Expand Up @@ -99,8 +98,14 @@ class SubscribeNowPage extends StatelessWidget {

static Widget create(BuildContext context) {
final client = getService<AgentApiClient>();
final storePurchaseIsAllowed =
Wizard.of(context).routeData as bool? ?? false;

return Provider<SubscribeNowModel>(
create: (context) => SubscribeNowModel(client),
create: (context) => SubscribeNowModel(
client,
isPurchaseAllowed: storePurchaseIsAllowed,
),
child: SubscribeNowPage(
onSubscriptionUpdate: (info) {
final src = context.read<ValueNotifier<ConfigSources>>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,37 @@ sealed class SubscriptionStatusModel {
/// Returns the appropriate view-model subclass based on the subscription source type that was passed.
factory SubscriptionStatusModel(
ConfigSources src,
AgentApiClient client,
) {
AgentApiClient client, {
bool canConfigureLandscape = false,
}) {
// Enforce this business logic here, as defaults may change in the future:
// - Org-managed Landscape configurations don't allow user changes via GUI.
if (src.landscapeSource.hasOrganization()) {
canConfigureLandscape = false;
}

final info = src.proSubscription;
switch (info.whichSubscriptionType()) {
case SubscriptionType.organization:
return OrgSubscriptionStatusModel(src);
return OrgSubscriptionStatusModel()
.._canConfigureLandscape = canConfigureLandscape;
case SubscriptionType.user:
return UserSubscriptionStatusModel(src, client);
return UserSubscriptionStatusModel(client)
.._canConfigureLandscape = canConfigureLandscape;
case SubscriptionType.microsoftStore:
return StoreSubscriptionStatusModel(src, info.productId);
return StoreSubscriptionStatusModel(info.productId)
.._canConfigureLandscape = canConfigureLandscape;
case SubscriptionType.none:
case SubscriptionType.notSet:
throw UnimplementedError('Unknown subscription type');
}
}

SubscriptionStatusModel._(ConfigSources src)
: _canConfigureLandscape = !src.landscapeSource.hasOrganization();
SubscriptionStatusModel._();

final bool _canConfigureLandscape;
/// Tells whether we can invoke the Landscape configuration page or not.
bool get canConfigureLandscape => _canConfigureLandscape;
bool _canConfigureLandscape = false;
}

/// Represents an active subscription through Microsoft Store.
Expand All @@ -37,7 +47,7 @@ class StoreSubscriptionStatusModel extends SubscriptionStatusModel {
@visibleForTesting
final Uri uri;

StoreSubscriptionStatusModel(super.src, String productID)
StoreSubscriptionStatusModel(String productID)
: uri = Uri.https(
'account.microsoft.com',
'/services/${productID.toLowerCase()}/details#billing',
Expand All @@ -51,7 +61,7 @@ class StoreSubscriptionStatusModel extends SubscriptionStatusModel {
/// Represents a subscription in which the user manually provided the Pro token.
/// The only action supported is Pro-detaching all instances.
class UserSubscriptionStatusModel extends SubscriptionStatusModel {
UserSubscriptionStatusModel(super.src, this._client) : super._();
UserSubscriptionStatusModel(this._client) : super._();

final AgentApiClient _client;

Expand All @@ -62,5 +72,5 @@ class UserSubscriptionStatusModel extends SubscriptionStatusModel {
/// Represents a subscription provided by the user's Organization.
/// There is no action supported.
class OrgSubscriptionStatusModel extends SubscriptionStatusModel {
OrgSubscriptionStatusModel(super.src) : super._();
OrgSubscriptionStatusModel() : super._();
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,14 @@ class SubscriptionStatusPage extends StatelessWidget {
/// Initializes the view-model and inject it in the widget tree so the child page can access it via the BuildContext.
static Widget create(BuildContext context) {
final client = getService<AgentApiClient>();
final landscapeFeatureIsEnabled =
Wizard.of(context).routeData as bool? ?? false;
return ProxyProvider<ValueNotifier<ConfigSources>, SubscriptionStatusModel>(
update: (context, src, _) => SubscriptionStatusModel(src.value, client),
update: (context, src, _) => SubscriptionStatusModel(
src.value,
client,
canConfigureLandscape: landscapeFeatureIsEnabled,
),
child: const SubscriptionStatusPage(),
);
}
Expand Down
Loading
Loading