diff --git a/subiquity/client/controllers/ubuntu_pro.py b/subiquity/client/controllers/ubuntu_pro.py index 401b2c4c..678ec434 100644 --- a/subiquity/client/controllers/ubuntu_pro.py +++ b/subiquity/client/controllers/ubuntu_pro.py @@ -18,6 +18,8 @@ import asyncio import logging from typing import Callable, List, Optional +from urwid import Widget + from subiquitycore.async_helpers import schedule_task from subiquity.client.controller import SubiquityTuiController @@ -26,7 +28,12 @@ from subiquity.common.types import ( UbuntuProCheckTokenStatus as TokenStatus, UbuntuProService, ) -from subiquity.ui.views.ubuntu_pro import UbuntuProView +from subiquity.ui.views.ubuntu_pro import ( + UbuntuProView, + UpgradeYesNoForm, + UpgradeModeForm, + TokenAddedWidget, + ) from subiquitycore.lsb_release import lsb_release from subiquitycore.tuicontroller import Skip @@ -58,9 +65,66 @@ class UbuntuProController(SubiquityTuiController): ubuntu_pro_info = await self.endpoint.GET() return UbuntuProView(self, ubuntu_pro_info.token) - def run_answers(self) -> None: - if "token" in self.answers: - self.done(self.answers["token"]) + async def run_answers(self) -> None: + """ Interact with the UI to go through the pre-attach process if + requested. """ + if "token" not in self.answers: + return + + from subiquitycore.testing.view_helpers import ( + click, + enter_data, + find_button_matching, + find_with_pred, + keypress, + ) + + view = self.app.ui.body + + def run_yes_no_screen(skip: bool) -> None: + if skip: + radio = view.upgrade_yes_no_form.skip.widget + else: + radio = view.upgrade_yes_no_form.upgrade.widget + + keypress(radio, key="enter") + click(find_button_matching(view, UpgradeYesNoForm.ok_label)) + + def run_token_screen(token: str) -> None: + keypress(view.upgrade_mode_form.with_contract_token.widget, + key="enter") + data = {"with_contract_token_subform": {"token": token}} + # TODO: add this point, it would be good to trigger the validation + # code for the token field. + enter_data(view.upgrade_mode_form, data) + click(find_button_matching(view, UpgradeModeForm.ok_label)) + + async def run_token_added_overlay() -> None: + def is_token_added_overlay(widget: Widget) -> bool: + try: + if widget._text == f" {TokenAddedWidget.title} ": + return True + except AttributeError: + return False + + # Wait until the "Token added successfully" overlay is shown. + while not find_with_pred(view, is_token_added_overlay): + await asyncio.sleep(.2) + + click(find_button_matching(view, TokenAddedWidget.done_label)) + + def run_services_screen() -> None: + click(find_button_matching(view._w, + UbuntuProView.services_done_label)) + + if not self.answers["token"]: + run_yes_no_screen(skip=True) + return + + run_yes_no_screen(skip=False) + run_token_screen(self.answers["token"]) + await run_token_added_overlay() + run_services_screen() def check_token(self, token: str, on_success: Callable[[List[UbuntuProService]], None], @@ -70,6 +134,7 @@ class UbuntuProController(SubiquityTuiController): async def inner() -> None: answer = await self.endpoint.check_token.GET(token) if answer.status == TokenStatus.VALID_TOKEN: + await self.endpoint.POST(UbuntuProInfo(token=token)) on_success(answer.subscription.services) else: on_failure(answer.status) @@ -85,6 +150,12 @@ class UbuntuProController(SubiquityTuiController): self.app.prev_screen() def done(self, token: str) -> None: + """ Submit the token and move on to the next screen. """ self.app.next_screen( self.endpoint.POST(UbuntuProInfo(token=token)) ) + + def next_screen(self) -> None: + """ Move on to the next screen. Assume the token should not be + submitted (or has already been submitted). """ + self.app.next_screen() diff --git a/subiquity/server/ubuntu_advantage.py b/subiquity/server/ubuntu_advantage.py index 1315834f..4c0e79c5 100644 --- a/subiquity/server/ubuntu_advantage.py +++ b/subiquity/server/ubuntu_advantage.py @@ -79,12 +79,16 @@ class MockedUAInterfaceStrategy(UAInterfaceStrategy): UA token. No actual query is done to the UA servers in this implementation. Instead, we create a response based on the following rules: + * Empty tokens are considered invalid. * Tokens starting with "x" will be considered expired. * Tokens starting with "i" will be considered invalid. * Tokens starting with "f" will generate an internal error. """ await asyncio.sleep(1 / self.scale_factor) + if not token: + raise InvalidTokenError(token) + if token[0] == "x": path = "examples/uaclient-status-expired.json" elif token[0] == "i": @@ -118,6 +122,11 @@ class UAClientUAInterfaceStrategy(UAInterfaceStrategy): UA token. The information will be queried using the UA client executable passed to the initializer. """ + if not token: + # u-a-c does not produce the expected output when the contract + # token is empty ; so let's not call it at all. + raise InvalidTokenError(token) + command = tuple(self.executable) + ( "status", "--format", "json", diff --git a/subiquity/ui/views/ubuntu_pro.py b/subiquity/ui/views/ubuntu_pro.py index af166147..03e6a802 100644 --- a/subiquity/ui/views/ubuntu_pro.py +++ b/subiquity/ui/views/ubuntu_pro.py @@ -16,9 +16,10 @@ import logging import re -from typing import List +from typing import Callable, List from urwid import ( + Columns, connect_signal, LineBox, Text, @@ -34,15 +35,21 @@ from subiquitycore.ui.buttons import ( back_btn, cancel_btn, done_btn, + menu_btn, ok_btn, ) from subiquitycore.ui.container import ( + ListBox, Pile, WidgetWrap, ) from subiquitycore.ui.form import ( Form, + SubForm, + SubFormField, + NO_HELP, simple_field, + RadioButtonField, WantsToKnowFormField, ) from subiquitycore.ui.spinner import ( @@ -53,7 +60,7 @@ from subiquitycore.ui.stretchy import ( ) from subiquitycore.ui.utils import ( button_pile, - SomethingFailed, + screen, ) from subiquitycore.ui.interactive import StringEditor @@ -61,18 +68,13 @@ from subiquitycore.ui.interactive import StringEditor log = logging.getLogger('subiquity.ui.views.ubuntu_pro') -ua_help = _("If you want to enroll this system using your Ubuntu Pro " - "subscription, enter your Ubuntu Pro token here. " - "Otherwise, leave this blank.") - -class UATokenEditor(StringEditor, WantsToKnowFormField): +class ContractTokenEditor(StringEditor, WantsToKnowFormField): """ Represent a text-box editor for the Ubuntu Pro Token. """ def __init__(self): """ Initialize the text-field editor for UA token. """ - self.valid_char_pat = r"[a-zA-Z0-9]" - self.error_invalid_char = _("The only characters permitted in this " - "field are alphanumeric characters.") + self.valid_char_pat = r"[1-9A-HJ-NP-Za-km-z]" + self.error_invalid_char = _("'{}' is not a valid character.") super().__init__() def valid_char(self, ch: str) -> bool: @@ -81,23 +83,95 @@ class UATokenEditor(StringEditor, WantsToKnowFormField): """ if len(ch) == 1 and not re.match(self.valid_char_pat, ch): self.bff.in_error = True - self.bff.show_extra(("info_error", self.error_invalid_char)) + self.bff.show_extra( + ("info_error", self.error_invalid_char.format(ch))) return False return super().valid_char(ch) -class UbuntuProForm(Form): +class ContractTokenForm(SubForm): + """ Represents a sub-form requesting Ubuntu Pro token. + +---------------------------------------------------------+ + | Token: C123456789ABCDEF | + | This is your Ubuntu Pro token | + +---------------------------------------------------------+ """ - Represents a form requesting Ubuntu Pro information + ContractTokenField = simple_field(ContractTokenEditor) + + token = ContractTokenField( + _("Token:"), + help=_("This is your Ubuntu Pro token")) + + def validate_token(self): + """ Return an error if the token input does not match the expected + format. """ + if not 24 <= len(self.token.value) <= 30: + return _("Invalid token: should be between 24 and 30 characters") + + +class UpgradeModeForm(Form): + """ Represents a form requesting the Ubuntu Pro credentials. + +---------------------------------------------------------+ + | (X) Add token manually | + | Token: C123456789ABCDEF | + | This is your Ubuntu Pro token | + | | + | [ Continue ] | + | [ Back ] | + +---------------------------------------------------------+ """ cancel_label = _("Back") + ok_label = _("Continue") + group: List[RadioButtonField] = [] - UATokenField = simple_field(UATokenEditor) + with_contract_token = RadioButtonField( + group, _("Add token manually"), + help=NO_HELP) + with_contract_token_subform = SubFormField( + ContractTokenForm, "", help=NO_HELP) - token = UATokenField(_("Ubuntu Pro token:"), help=ua_help) + def __init__(self, initial) -> None: + """ Initializer that configures the callback to run when the radio is + checked/unchecked. Since there is a single radio for now, it can + only be unchecked programmatically. """ + super().__init__(initial) + connect_signal(self.with_contract_token.widget, + 'change', self._toggle_contract_token_input) + + def _toggle_contract_token_input(self, sender, new_value): + """ Enable/disable the sub-form that requests the contract token. """ + self.with_contract_token_subform.enabled = new_value -class CheckingUAToken(WidgetWrap): +class UpgradeYesNoForm(Form): + """ Represents a form asking if we want to upgrade to Ubuntu Pro. + +---------------------------------------------------------+ + | (X) Upgrade to Ubuntu Pro | + | | + | ( ) Do this later | + | | + | You can always enable Ubuntu Pro later via the | + | 'pro attach' command. | + | | + | [ Continue ] | + | [ Back ] | + +---------------------------------------------------------+ + """ + + cancel_label = _("Back") + ok_label = _("Continue") + group: List[RadioButtonField] = [] + + upgrade = RadioButtonField( + group, _("Upgrade to Ubuntu Pro"), + help=NO_HELP) + skip = RadioButtonField( + group, _("Do this later"), + help="\n" + _("You can always enable Ubuntu Pro later via the" + " 'pro attach' command.")) + + +class CheckingContractToken(WidgetWrap): """ Widget displaying a loading animation while checking ubuntu pro subscription. """ def __init__(self, parent: BaseView): @@ -125,76 +199,261 @@ class CheckingUAToken(WidgetWrap): class UbuntuProView(BaseView): """ Represent the view of the Ubuntu Pro configuration. """ - title = _("Enable Ubuntu Pro") - excerpt = _("Enter your Ubuntu Pro token if you want to enroll " - "this system.") + title = _("Upgrade to Ubuntu Pro") + services_done_label = _("Continue") def __init__(self, controller, token: str): """ Initialize the view with the default value for the token. """ self.controller = controller - self.form = UbuntuProForm(initial={"token": token}) + self.upgrade_yes_no_form = UpgradeYesNoForm(initial={ + "skip": not token, + "upgrade": bool(token), + }) + self.upgrade_mode_form = UpgradeModeForm(initial={ + "with_contract_token_subform": {"token": token}, + }) - def on_cancel(_: UbuntuProForm): + def on_upgrade_yes_no_cancel(unused: UpgradeYesNoForm): + """ Function to call when hitting Done from the upgrade/skip + screen. """ self.cancel() - connect_signal(self.form, 'submit', self.done) - connect_signal(self.form, 'cancel', on_cancel) + def on_upgrade_mode_cancel(unused: UpgradeModeForm): + """ Function to call when hitting Back from the contract token + form. """ + self._w = self.upgrade_yes_no_screen() - super().__init__(self.form.as_screen(excerpt=_(self.excerpt))) + connect_signal(self.upgrade_yes_no_form, + 'submit', self.upgrade_yes_no_done) + connect_signal(self.upgrade_yes_no_form, + 'cancel', on_upgrade_yes_no_cancel) + connect_signal(self.upgrade_mode_form, + 'submit', self.upgrade_mode_done) + connect_signal(self.upgrade_mode_form, + 'cancel', on_upgrade_mode_cancel) - def done(self, form: UbuntuProForm) -> None: - """ If no token was supplied, move on to the next screen. - If a token was provided, open the loading dialog and - asynchronously check if the token is valid. """ - token: str = form.token.value - if token: - def on_success(services: List[UbuntuProService]) -> None: + super().__init__(self.upgrade_yes_no_screen()) + + def upgrade_mode_screen(self) -> Widget: + """ Return a screen that asks the user for his information (e.g., + contract token). + +---------------------------------------------------------+ + | To upgrade to Ubuntu Pro, you can enter your token | + | manually. | + | | + | [ How to Register -> ] | + | | + | (X) Add token manually | + | Token: C123456789ABCDEF | + | This is your Ubuntu Pro token | + | | + | [ Continue ] | + | [ Back ] | + +---------------------------------------------------------+ + """ + + excerpt = _("To upgrade to Ubuntu Pro, you can enter your token" + " manually.") + + how_to_register_btn = menu_btn( + _("How to Register"), + on_press=lambda unused: self.show_how_to_register() + ) + bp = button_pile([how_to_register_btn]) + bp.align = "left" + rows = [ + bp, + Text(""), + ] + self.upgrade_mode_form.as_rows() + return screen( + ListBox(rows), + self.upgrade_mode_form.buttons, + excerpt=excerpt, + focus_buttons=True) + + def upgrade_yes_no_screen(self) -> Widget: + """ Return a screen that asks the user to skip or upgrade. + +---------------------------------------------------------+ + | Upgrade this machine to Ubuntu Pro or skip this step. | + | | + | [ About Ubuntu Pro -> ] | + | | + | ( ) Upgrade to Ubuntu Pro | + | | + | (X) Do this later | + | You can always enable Ubuntu Pro later via the | + | 'pro attach' command. | + | | + | [ Continue ] | + | [ Back ] | + +---------------------------------------------------------+ + """ + + excerpt = _("Upgrade this machine to Ubuntu Pro or skip this step.") + + about_pro_btn = menu_btn( + _("About Ubuntu Pro"), + on_press=lambda unused: self.show_about_ubuntu_pro()) + + bp = button_pile([about_pro_btn]) + bp.align = "left" + rows = [ + bp, + Text(""), + ] + self.upgrade_yes_no_form.as_rows() + return screen( + ListBox(rows), + self.upgrade_yes_no_form.buttons, + excerpt=excerpt, + focus_buttons=True) + + def services_screen(self, services) -> Widget: + """ + +---------------------------------------------------------+ + | List of your enabled services: | + | | + | * ... | + | * ... | + | | + | Other available services: | + | | + | * ... | + | * ... | + | | + | If you want to change the default enablements for your | + | token, you can do so via the ubuntu.com/pro web | + | interface. Alternatively, you can change enabled | + | services using the `pro' command-line tool once the | + | installation is finished. | + | | + | [ Continue ] | + | [ Back ] | + +---------------------------------------------------------+ + """ + auto_enabled = [svc for svc in services if svc.auto_enabled] + can_be_enabled = [svc for svc in services if not svc.auto_enabled] + + svc_rows: List[Widget] = [] + + if auto_enabled: + svc_rows.append(Text(_("List of your enabled services:"))) + svc_rows.append(Text("")) + svc_rows.extend( + [Text(f" * {svc.description}") for svc in auto_enabled]) + + if can_be_enabled: + if auto_enabled: + # available here means activable + svc_rows.append(Text("")) + svc_rows.append(Text(_("Other available services:"))) + else: + svc_rows.append(Text(_("Available services:"))) + svc_rows.append(Text("")) + svc_rows.extend( + [Text(f" * {svc.description}") for svc in can_be_enabled]) + + def on_continue() -> None: + self.controller.next_screen() + + def on_back() -> None: + self._w = self.upgrade_yes_no_screen() + + back_button = back_btn( + label=_("Back"), + on_press=lambda unused: on_back()) + continue_button = done_btn( + label=self.__class__.services_done_label, + on_press=lambda unused: on_continue()) + + widgets: List[Widget] = [ + Text(""), + Pile(svc_rows), + Text(""), + Text(_("If you want to change the default enablements for your" + " token, you can do so via the ubuntu.com/pro web" + " interface. Alternatively you can change enabled services" + " using the `pro` command-line tool once the installation" + " is finished.")), + Text(""), + ] + + return screen( + ListBox(widgets), + buttons=[continue_button, back_button], + excerpt=None, + focus_buttons=True) + + def upgrade_mode_done(self, form: UpgradeModeForm) -> None: + """ Open the loading dialog and asynchronously check if the token is + valid. """ + def on_success(services: List[UbuntuProService]) -> None: + def show_services() -> None: self.remove_overlay() self.show_activable_services(services) - def on_failure(status: UbuntuProCheckTokenStatus) -> None: - self.remove_overlay() - if status == UbuntuProCheckTokenStatus.INVALID_TOKEN: - self.show_invalid_token() - elif status == UbuntuProCheckTokenStatus.EXPIRED_TOKEN: - self.show_expired_token() - elif status == UbuntuProCheckTokenStatus.UNKNOWN_ERROR: - self.show_unknown_error() + self.remove_overlay() + widget = TokenAddedWidget( + parent=self, + on_continue=show_services) + self.show_stretchy_overlay(widget) - checking_token_overlay = CheckingUAToken(self) - self.show_overlay(checking_token_overlay, - width=checking_token_overlay.width, - min_width=None) + def on_failure(status: UbuntuProCheckTokenStatus) -> None: + self.remove_overlay() + token_field = form.with_contract_token_subform.widget.form.token + if status == UbuntuProCheckTokenStatus.INVALID_TOKEN: + self.show_invalid_token() + token_field.in_error = True + token_field.show_extra(("info_error", "Invalid token")) + form.validated() + elif status == UbuntuProCheckTokenStatus.EXPIRED_TOKEN: + self.show_expired_token() + token_field.in_error = True + token_field.show_extra(("info_error", "Expired token")) + form.validated() + elif status == UbuntuProCheckTokenStatus.UNKNOWN_ERROR: + self.show_unknown_error() - self.controller.check_token(token, - on_success=on_success, - on_failure=on_failure) + token: str = form.with_contract_token_subform.value["token"] + checking_token_overlay = CheckingContractToken(self) + self.show_overlay(checking_token_overlay, + width=checking_token_overlay.width, + min_width=None) + + self.controller.check_token(token, + on_success=on_success, + on_failure=on_failure) + + def upgrade_yes_no_done(self, form: UpgradeYesNoForm) -> None: + """ If skip is selected, move on to the next screen. + Otherwise, show the form requesting the contract token. """ + if form.skip.value: + self.controller.done("") else: - self.controller.done(token) + self._w = self.upgrade_mode_screen() def cancel(self) -> None: """ Called when the user presses the Back button. """ self.controller.cancel() + def show_about_ubuntu_pro(self) -> None: + """ Display an overlay that shows information about Ubuntu Pro. """ + self.show_stretchy_overlay(AboutProWidget(self)) + + def show_how_to_register(self) -> None: + """ Display an overlay that shows instructions to register to + Ubuntu Pro. """ + self.show_stretchy_overlay(HowToRegisterWidget(self)) + def show_invalid_token(self) -> None: """ Display an overlay that indicates that the user-supplied token is invalid. """ - self.show_stretchy_overlay( - SomethingFailed(self, - "Invalid token.", - "The Ubuntu Pro token that you provided" - " is invalid. Please make sure that you typed" - " your token correctly.")) + self.show_stretchy_overlay(InvalidTokenWidget(self)) def show_expired_token(self) -> None: """ Display an overlay that indicates that the user-supplied token has expired. """ - self.show_stretchy_overlay( - SomethingFailed(self, - "Token expired.", - "The Ubuntu Pro token that you provided" - " has expired. Please use a different token.")) + self.show_stretchy_overlay(ExpiredTokenWidget(self)) def show_unknown_error(self) -> None: """ Display an overlay that indicates that we were unable to retrieve @@ -206,47 +465,224 @@ class UbuntuProView(BaseView): def show_activable_services(self, services: List[UbuntuProService]) -> None: - """ Display an overlay with the list of services that can be enabled + """ Display a screen with the list of services that can be enabled via Ubuntu Pro subscription. After the user confirms, we will quit the current view and move on. """ - self.show_stretchy_overlay(ShowServicesWidget(self, services)) + self._w = self.services_screen(services) -class ShowServicesWidget(Stretchy): - """ Widget to show the activable services for UA subscription. """ +class ExpiredTokenWidget(Stretchy): + """ Widget that shows that the supplied token is expired. + + +--------------------- Expired token ---------------------+ + | | + | Your token has expired. Please use another token to | + | continue. | + | | + | [ Okay ] | + +---------------------------------------------------------+ + """ + def __init__(self, parent: BaseView) -> None: + """ Initializes the widget. """ + self.parent = parent + cont = done_btn(label=_("Okay"), on_press=lambda unused: self.close()) + widgets = [ + Text(_("Your token has expired. Please use another token" + " to continue.")), + Text(""), + button_pile([cont]), + ] + super().__init__("Expired token", widgets, + stretchy_index=0, focus_index=2) + + def close(self) -> None: + """ Close the overlay. """ + self.parent.remove_overlay() + + +class InvalidTokenWidget(Stretchy): + """ Widget that shows that the supplied token is invalid. + + +--------------------- Invalid token ---------------------+ + | | + | Your token could not be verified. Please ensure it is | + | correct and try again. | + | | + | [ Okay ] | + +---------------------------------------------------------+ + """ + def __init__(self, parent: BaseView) -> None: + """ Initializes the widget. """ + self.parent = parent + cont = done_btn(label=_("Okay"), on_press=lambda unused: self.close()) + widgets = [ + Text(_("Your token could not be verified. Please ensure it is" + " correct and try again.")), + Text(""), + button_pile([cont]), + ] + super().__init__("Invalid token", widgets, + stretchy_index=0, focus_index=2) + + def close(self) -> None: + """ Close the overlay. """ + self.parent.remove_overlay() + + +class TokenAddedWidget(Stretchy): + """ Widget that shows that the supplied token is valid and was "added". + +---------------- Token added successfully ---------------+ + | | + | Your token has been added successfully and your | + | subscription configuration will be applied at the first | + | boot. | + | | + | [ Continue ] | + +---------------------------------------------------------+ + """ + title = _("Token added successfully") + done_label = _("Continue") + def __init__(self, parent: UbuntuProView, - services: List[UbuntuProService]) -> None: - """ Initializes the widget by including the list of services as a - bullet-point list. """ + on_continue: Callable[[], None]) -> None: + """ Initializes the widget. """ + self.parent = parent + cont = done_btn( + label=self.__class__.done_label, + on_press=lambda unused: on_continue()) + widgets = [ + Text(_("Your token has been added successfully and your" + " subscription configuration will be applied at the first" + " boot.")), + Text(""), + button_pile([cont]), + ] + super().__init__(self.__class__.title, widgets, + stretchy_index=0, focus_index=2) + + +class AboutProWidget(Stretchy): + """ Widget showing some information about what Ubuntu Pro offers. + +------------------- About Ubuntu Pro --------------------+ + | | + | Ubuntu Pro subscription gives you access to multiple | + | security & compliance services, including: | + | | + | • Security patching for over 30.000 packages, with a | + | focus on High and Critical CVEs (extended from 2.500) | + | • ... | + | • ... | + | | + | Ubuntu Pro is free for personal use on up to 3 machines.| + | More information on ubuntu.com/pro | + | | + | [ Continue ] | + +---------------------------------------------------------+ + """ + def __init__(self, parent: UbuntuProView) -> None: + """ Initializes the widget.""" self.parent = parent - ok = ok_btn(label=_("OK"), on_press=self.ok) + ok = ok_btn(label=_("Continue"), on_press=lambda unused: self.close()) - title = _("Activable Services") - header = _("List of services that are activable through your " - "Ubuntu Pro subscription:") + title = _("About Ubuntu Pro") + header = _("Ubuntu Pro subscription gives you access to multiple" + " security & compliance services, including:") + + services = [ + _("Security patching for over 30.000 packages, with a focus on" + " High and Critical CVEs (extended from 2.500)"), + _("10 years of security Maintenance (extended from 5 years)"), + _("Kernel Livepatch service for increased uptime and security"), + _("Ubuntu Security Guide for hardening profiles, including CIS" + " and DISA-STIG"), + _("FIPS 140-2 NIST-certified crypto-modules for FedRAMP" + " compliance"), + ] + + def itemize(item: str, marker: str = "•") -> Columns: + """ Return the text specified in a Text widget prepended with a + bullet point / marker. If the text is too long to fit in a single + line, the continuation lines are indented as shown below: + +---------------------------+ + | * This is an example of | + | what such element would | + | look like. | + +---------------------------+ + """ + return Columns( + [(len(marker), Text(marker)), Text(item)], dividechars=1) widgets: List[Widget] = [ Text(header), Text(""), - Pile([Text(f"* {svc.description}") for svc in services]), + Pile([itemize(svc) for svc in services]), Text(""), - Text("Once the installation has finished, you can enable these " - "services using the `ua` command-line tool."), + Text(_("Ubuntu Pro is free for personal use on up to 3" + " machines.")), + Text(_("More information on ubuntu.com/pro")), Text(""), button_pile([ok]), ] - super().__init__(title, widgets, 2, 6) + super().__init__(title, widgets, stretchy_index=2, focus_index=7) - def ok(self, sender) -> None: - """ Close the overlay and submit the token. """ - self.parent.controller.done(self.parent.form.token.value) + def close(self) -> None: + """ Close the overlay. """ + self.parent.remove_overlay() + + +class HowToRegisterWidget(Stretchy): + """ Widget showing some instructions to register to Ubuntu Pro. + +-------------------- How to register --------------------+ + | | + | You can register for a free Ubuntu One account and get | + | a personal token for up to 3 machines. | + | | + | To register an account, visit ubuntu.com/pro on another | + | device. | + | | + | [ Continue ] | + +---------------------------------------------------------+ + """ + def __init__(self, parent: UbuntuProView) -> None: + """ Initializes the widget.""" + self.parent = parent + + ok = ok_btn(label=_("Continue"), on_press=lambda unused: self.close()) + + title = _("How to register") + header = _("You can register for a free Ubuntu One account and get a" + " personal token for up to 3 machines.") + + widgets: List[Widget] = [ + Text(header), + Text(""), + Text("To register an account, visit ubuntu.com/pro on another" + " device."), + Text(""), + button_pile([ok]), + ] + + super().__init__(title, widgets, stretchy_index=2, focus_index=4) + + def close(self) -> None: + """ Close the overlay. """ + self.parent.remove_overlay() class ContinueAnywayWidget(Stretchy): """ Widget that requests the user if he wants to go back or continue - anyway. """ + anyway. + +--------------------- Unknown error ---------------------+ + | | + | Unable to check your subscription information. Do you | + | want to go back or continue anyway? | + | | + | [ Back ] | + | [ Continue anyway ] | + +---------------------------------------------------------+ + """ def __init__(self, parent: UbuntuProView) -> None: """ Initializes the widget by showing two buttons, one to go back and one to move forward anyway. """ @@ -267,4 +703,5 @@ class ContinueAnywayWidget(Stretchy): def cont(self, sender) -> None: """ Move on to the next screen. """ - self.parent.controller.done(self.parent.form.token.value) + subform = self.parent.upgrade_mode_form.with_contract_token_subform + self.parent.controller.done(subform.value["token"])