Skip to content

Commit

Permalink
Add Choose security key page
Browse files Browse the repository at this point in the history
  • Loading branch information
jpnurmi committed Sep 3, 2021
1 parent 040cfa9 commit b4b1206
Show file tree
Hide file tree
Showing 11 changed files with 476 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/ubuntu_desktop_installer/lib/installer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class _UbuntuDesktopInstallerWizardState
Routes.updatesOtherSoftware: UpdatesOtherSoftwarePage.create,
if (model.hasBitLocker)
Routes.turnOffBitlocker: TurnOffBitLockerPage.create,
Routes.chooseSecurityKey: ChooseSecurityKeyPage.create,
Routes.allocateDiskSpace: AllocateDiskSpacePage.create,
Routes.writeChangesToDisk: WriteChangesToDiskPage.create,
Routes.whoAreYou: WhoAreYouPage.create,
Expand Down
13 changes: 13 additions & 0 deletions packages/ubuntu_desktop_installer/lib/l10n/app_en_US.arb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@
"installThirdPartyTitle": "Install third-party software for graphics and Wi-Fi hardware, as well as additional media formats",
"installThirdPartySubtitle": "This software is subject to license terms included with its documentation. Some are proprietary.",

"chooseSecurityKeyTitle": "Choose a security key",
"chooseSecurityKeyHeader": "Disk enscryption protects your files in case you lose your computer. It requires you to enter a security key each time the computer starts up.\n\nAny files outside of Ubuntu will not be encrypted.",
"chooseSecurityKeyHint": "Choose a security key",
"chooseSecurityKeyConfirmHint": "Confirm the security key",
"chooseSecurityKeyMismatch": "The security keys do not match",
"chooseSecurityKeyWarning": "<font color=\"{color}\">Warning</font>: If you lose this security key, all data will be lost. If you need to, write down your key and keep it in a safe place elsewhere.",
"@chooseSecurityKeyWarning": {
"type": "text",
"placeholders": {
"color": {}
}
},

"allocateDiskSpace": "Allocate disk space",
"startInstallingButtonText": "Start Installing",
"diskHeadersDevice": "Device",
Expand Down
1 change: 1 addition & 0 deletions packages/ubuntu_desktop_installer/lib/pages.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export 'pages/allocate_disk_space/allocate_disk_space_page.dart';
export 'pages/choose_security_key/choose_security_key_page.dart';
export 'pages/choose_your_look_page.dart';
export 'pages/installation_complete/installation_complete_page.dart';
export 'pages/installation_slides/installation_slides_page.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'package:flutter/foundation.dart';
import 'package:subiquity_client/subiquity_client.dart';

/// View model for [ChooseSecurityKeyPage].
class ChooseSecurityKeyModel extends ChangeNotifier {
/// Creates the model with the given client.
ChooseSecurityKeyModel(this._client) {
Listenable.merge([
_securityKey,
_confirmedSecurityKey,
]).addListener(notifyListeners);
}

final SubiquityClient _client;
final _securityKey = ValueNotifier<String>('');
final _confirmedSecurityKey = ValueNotifier<String>('');

/// The current password.
String get securityKey => _securityKey.value;
set securityKey(String value) => _securityKey.value = value;

/// The confirmed password for validation.
String get confirmedSecurityKey => _confirmedSecurityKey.value;
set confirmedSecurityKey(String value) => _confirmedSecurityKey.value = value;

/// Whether the current input is valid.
bool get isValid => securityKey == confirmedSecurityKey;

/// Loads the security key.
Future<void> loadSecurityKey() async {}

/// Saves the security key.
Future<void> saveSecurityKey() async {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:provider/provider.dart';
import 'package:subiquity_client/subiquity_client.dart';
import 'package:ubuntu_wizard/constants.dart';
import 'package:ubuntu_wizard/utils.dart';
import 'package:ubuntu_wizard/widgets.dart';

import '../../l10n.dart';
import '../../widgets.dart';
import 'choose_security_key_model.dart';

part 'choose_security_key_widgets.dart';

/// Choose security key page.
///
/// See also:
/// * [ChooseSecurityKeyModel]
class ChooseSecurityKeyPage extends StatefulWidget {
/// Use [create] instead.
@visibleForTesting
const ChooseSecurityKeyPage({
Key? key,
}) : super(key: key);

/// Creates an instance with [ChooseSecurityKeyModel].
static Widget create(BuildContext context) {
final client = Provider.of<SubiquityClient>(context, listen: false);
return ChangeNotifierProvider(
create: (_) => ChooseSecurityKeyModel(client),
child: ChooseSecurityKeyPage(),
);
}

@override
_ChooseSecurityKeyPageState createState() => _ChooseSecurityKeyPageState();
}

class _ChooseSecurityKeyPageState extends State<ChooseSecurityKeyPage> {
@override
void initState() {
super.initState();

final model = Provider.of<ChooseSecurityKeyModel>(context, listen: false);
model.loadSecurityKey();
}

@override
Widget build(BuildContext context) {
final lang = AppLocalizations.of(context);
return LocalizedView(builder: (context, lang) {
return WizardPage(
title: Text(lang.chooseSecurityKeyTitle),
header: Text(lang.chooseSecurityKeyHeader),
content: LayoutBuilder(builder: (context, constraints) {
final fieldWidth = constraints.maxWidth * kContentWidthFraction;
return ListView(
children: <Widget>[
_SecurityKeyFormField(fieldWidth: fieldWidth),
const SizedBox(height: kContentSpacing),
_ConfirmSecurityKeyFormField(fieldWidth: fieldWidth),
const SizedBox(height: kContentSpacing),
Html(
data: lang.chooseSecurityKeyWarning(
Theme.of(context).errorColor.toHex()),
style: {'body': Style(margin: EdgeInsets.all(0))},
),
],
);
}),
actions: <WizardAction>[
WizardAction(
label: lang.backButtonText,
onActivated: Wizard.of(context).back,
),
WizardAction(
label: lang.continueButtonText,
enabled: context
.select<ChooseSecurityKeyModel, bool>((model) => model.isValid),
onActivated: () async {
final model =
Provider.of<ChooseSecurityKeyModel>(context, listen: false);
await model.saveSecurityKey();

Wizard.of(context).next();
},
),
],
);
});
}
}

extension _HexColor on Color {
String toHex() =>
_formatHex(alpha.toHex(), red.toHex(), green.toHex(), blue.toHex());
String _formatHex(String a, String r, String g, String b) => '#$a$r$g$b';
}

extension _IntHex on int {
String toHex() => toRadixString(16).padLeft(2, '0');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
part of 'choose_security_key_page.dart';

class _SecurityKeyFormField extends StatelessWidget {
const _SecurityKeyFormField({
Key? key,
this.fieldWidth,
}) : super(key: key);

final double? fieldWidth;

@override
Widget build(BuildContext context) {
final lang = AppLocalizations.of(context);
final securityKey = context
.select<ChooseSecurityKeyModel, String>((model) => model.securityKey);

return ValidatedFormField(
fieldWidth: fieldWidth,
labelText: lang.chooseSecurityKeyHint,
obscureText: true,
successWidget: securityKey.isNotEmpty ? SuccessIcon() : null,
initialValue: securityKey,
onChanged: (value) {
final model =
Provider.of<ChooseSecurityKeyModel>(context, listen: false);
model.securityKey = value;
},
);
}
}

class _ConfirmSecurityKeyFormField extends StatelessWidget {
const _ConfirmSecurityKeyFormField({
Key? key,
required this.fieldWidth,
}) : super(key: key);

final double? fieldWidth;

@override
Widget build(BuildContext context) {
final lang = AppLocalizations.of(context);
final securityKey = context
.select<ChooseSecurityKeyModel, String>((model) => model.securityKey);
final confirmedSecurityKey = context.select<ChooseSecurityKeyModel, String>(
(model) => model.confirmedSecurityKey);

return ValidatedFormField(
fieldWidth: fieldWidth,
labelText: lang.chooseSecurityKeyConfirmHint,
obscureText: true,
successWidget: confirmedSecurityKey.isNotEmpty ? SuccessIcon() : null,
initialValue: confirmedSecurityKey,
// TODO: https://github.com/canonical/ubuntu-desktop-installer/pull/296
// autovalidateMode: AutovalidateMode.always,
validator: EqualValidator(
securityKey,
errorText: lang.chooseSecurityKeyMismatch,
),
onChanged: (value) {
final model =
Provider.of<ChooseSecurityKeyModel>(context, listen: false);
model.confirmedSecurityKey = value;
},
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class _ConfirmPasswordFormField extends StatelessWidget {
obscureText: true,
fieldWidth: fieldWidth,
labelText: lang.whoAreYouPageConfirmPasswordLabel,
successWidget: SuccessIcon(),
successWidget: password.isNotEmpty ? SuccessIcon() : null,
initialValue: confirmedPassword,
validator: EqualValidator(
password,
Expand Down
1 change: 1 addition & 0 deletions packages/ubuntu_desktop_installer/lib/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ abstract class Routes {
static const whoAreYou = '/whoareyou';
static const installationSlides = '/installationslides';
static const turnOffBitlocker = '/turnoffbitlocker';
static const chooseSecurityKey = '/choosesecuritykey';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:ubuntu_desktop_installer/pages/choose_security_key/choose_security_key_model.dart';
import 'package:ubuntu_test/mocks.dart';

void main() {
test('notify changes', () {
final model = ChooseSecurityKeyModel(MockSubiquityClient());

var wasNotified = false;
model.addListener(() => wasNotified = true);

wasNotified = false;
expect(model.securityKey, isEmpty);
model.securityKey = 'foo';
expect(wasNotified, isTrue);

wasNotified = false;
expect(model.confirmedSecurityKey, isEmpty);
model.confirmedSecurityKey = 'bar';
expect(wasNotified, isTrue);
});

test('validation', () {
final model = ChooseSecurityKeyModel(MockSubiquityClient());
expect(model.isValid, isTrue);

void testValid(
String securityKey,
String confirmedSecurityKey,
Matcher matcher,
) {
model.securityKey = securityKey;
model.confirmedSecurityKey = confirmedSecurityKey;
expect(model.isValid, matcher);
}

testValid('', '', isTrue);
testValid('foo', 'foo', isTrue);
testValid('foo', 'bar', isFalse);
});
}

0 comments on commit b4b1206

Please sign in to comment.