diff --git a/gnome-control-center.spec b/gnome-control-center.spec index 356b144..edd6566 100644 --- a/gnome-control-center.spec +++ b/gnome-control-center.spec @@ -24,6 +24,9 @@ License: GPL-2.0-or-later AND CC0-1.0 URL: https://gitlab.gnome.org/GNOME/gnome-control-center/ Source0: https://download.gnome.org/sources/%{name}/46/%{name}-%{tarball_version}.tar.xz +# https://issues.redhat.com/browse/RHEL-26117 +Patch0: subscription-manager.patch + BuildRequires: desktop-file-utils BuildRequires: docbook-style-xsl libxslt BuildRequires: gcc diff --git a/subscription-manager.patch b/subscription-manager.patch new file mode 100644 index 0000000..c032417 --- /dev/null +++ b/subscription-manager.patch @@ -0,0 +1,1142 @@ +From 015fd59e65295e8507f7df389833a6ba443c463f Mon Sep 17 00:00:00 2001 +From: Felipe Borges +Date: Thu, 30 Jan 2025 13:53:29 +0100 +Subject: [PATCH] system: Add subscription manager (Registration) page + +https://issues.redhat.com/browse/RHEL-26117 +--- + panels/system/cc-system-panel.c | 19 + + panels/system/cc-system-panel.ui | 13 + + panels/system/gnome-system-panel.desktop.in | 2 +- + .../icons/scalable/actions/key-symbolic.svg | 4 + + panels/system/meson.build | 4 + + panels/system/subman/cc-subman-page.c | 629 ++++++++++++++++++ + panels/system/subman/cc-subman-page.h | 38 ++ + panels/system/subman/cc-subman-page.ui | 254 +++++++ + .../subman/gnome-subman-panel.desktop.in | 14 + + panels/system/subman/meson.build | 9 + + panels/system/system.gresource.xml | 3 + + 11 files changed, 988 insertions(+), 1 deletion(-) + create mode 100644 panels/system/icons/scalable/actions/key-symbolic.svg + create mode 100644 panels/system/subman/cc-subman-page.c + create mode 100644 panels/system/subman/cc-subman-page.h + create mode 100644 panels/system/subman/cc-subman-page.ui + create mode 100644 panels/system/subman/gnome-subman-panel.desktop.in + create mode 100644 panels/system/subman/meson.build + +diff --git a/panels/system/cc-system-panel.c b/panels/system/cc-system-panel.c +index f28a96372..e7ff45bdf 100644 +--- a/panels/system/cc-system-panel.c ++++ b/panels/system/cc-system-panel.c +@@ -35,6 +35,8 @@ + #include "secure-shell/cc-secure-shell-page.h" + #include "users/cc-users-page.h" + ++#include "subman/cc-subman-page.h" ++ + struct _CcSystemPanel + { + CcPanel parent_instance; +@@ -47,6 +49,8 @@ struct _CcSystemPanel + + CcSecureShellPage *secure_shell_dialog; + AdwNavigationPage *software_updates_group; ++ ++ AdwActionRow *subman_row; + }; + + CC_PANEL_REGISTER (CcSystemPanel, cc_system_panel) +@@ -145,6 +149,8 @@ cc_system_panel_class_init (CcSystemPanelClass *klass) + gtk_widget_class_bind_template_child (widget_class, CcSystemPanel, users_row); + gtk_widget_class_bind_template_child (widget_class, CcSystemPanel, software_updates_group); + ++ gtk_widget_class_bind_template_child (widget_class, CcSystemPanel, subman_row); ++ + gtk_widget_class_bind_template_callback (widget_class, cc_system_page_open_software_update); + gtk_widget_class_bind_template_callback (widget_class, on_secure_shell_row_clicked); + +@@ -161,6 +167,7 @@ static void + cc_system_panel_init (CcSystemPanel *self) + { + CcServiceState service_state; ++ CcSubmanPage *subman_page; + + g_resources_register (cc_system_get_resource ()); + gtk_widget_init_template (GTK_WIDGET (self)); +@@ -176,4 +183,16 @@ cc_system_panel_init (CcSystemPanel *self) + cc_panel_add_static_subpage (CC_PANEL (self), "region", CC_TYPE_REGION_PAGE); + cc_panel_add_static_subpage (CC_PANEL (self), "remote-desktop", CC_TYPE_REMOTE_DESKTOP_PAGE); + cc_panel_add_static_subpage (CC_PANEL (self), "users", CC_TYPE_USERS_PAGE); ++ ++ /* Subscription Manager */ ++ subman_page = g_object_new (CC_TYPE_SUBMAN_PAGE, NULL); ++ if (cc_subman_page_is_available (subman_page)) ++ { ++ cc_panel_add_subpage (CC_PANEL (self), "subman", ADW_NAVIGATION_PAGE (subman_page)); ++ g_object_bind_property (subman_page, ++ "is-available", ++ self->subman_row, ++ "visible", ++ G_BINDING_SYNC_CREATE | G_BINDING_DEFAULT); ++ } + } +diff --git a/panels/system/cc-system-panel.ui b/panels/system/cc-system-panel.ui +index 6de8b22e8..507c6efc3 100644 +--- a/panels/system/cc-system-panel.ui ++++ b/panels/system/cc-system-panel.ui +@@ -70,6 +70,19 @@ + + + ++ ++ ++ False ++ Registration ++ Enable Red Hat updates, content, and services ++ True ++ key-symbolic ++ True ++ navigation.push ++ 'subman' ++ ++ ++ + + + _About +diff --git a/panels/system/gnome-system-panel.desktop.in b/panels/system/gnome-system-panel.desktop.in +index a437f8b20..45e3d9a50 100644 +--- a/panels/system/gnome-system-panel.desktop.in ++++ b/panels/system/gnome-system-panel.desktop.in +@@ -11,4 +11,4 @@ StartupNotify=true + Categories=GNOME;GTK;Settings;X-GNOME-Settings-Panel;X-GNOME-DetailsSettings; + OnlyShowIn=GNOME;Unity; + # Translators: Search terms to find the System panel. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! +-Keywords=device;system;information;details;hostname;memory;processor;version;software;operating;os;model;language;region;country;formats;numbers;units;clock;timezone;date;location;remote;desktop;rdp;vnc;ssh; ++Keywords=device;system;information;details;hostname;memory;processor;version;software;operating;os;model;language;region;country;formats;numbers;units;clock;timezone;date;location;remote;desktop;rdp;vnc;ssh;Registration;Subscription;Red Hat;Products;Updates;Registry;Account;Activation;Register;Subscribe; +diff --git a/panels/system/icons/scalable/actions/key-symbolic.svg b/panels/system/icons/scalable/actions/key-symbolic.svg +new file mode 100644 +index 000000000..7a5e13619 +--- /dev/null ++++ b/panels/system/icons/scalable/actions/key-symbolic.svg +@@ -0,0 +1,4 @@ ++ ++ ++ ++ +diff --git a/panels/system/meson.build b/panels/system/meson.build +index bbe004837..7ff9646c3 100644 +--- a/panels/system/meson.build ++++ b/panels/system/meson.build +@@ -58,6 +58,8 @@ sources = files( + 'users/pw-utils.c', + 'users/run-passwd.c', + 'users/user-utils.c', ++ ++ 'subman/cc-subman-page.c', + ) + + sources += gnome.compile_resources( +@@ -136,6 +138,8 @@ subdir('remote-desktop') + subdir('secure-shell') + subdir('users') + ++subdir('subman') ++ + panels_libs += static_library( + cappletname, + sources: sources, +diff --git a/panels/system/subman/cc-subman-page.c b/panels/system/subman/cc-subman-page.c +new file mode 100644 +index 000000000..7b29c3f72 +--- /dev/null ++++ b/panels/system/subman/cc-subman-page.c +@@ -0,0 +1,629 @@ ++/* ++ * cc-subman-page.c ++ * ++ * Copyright 2025 Red Hat Inc ++ * ++ * This program is free software: you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation, either version 3 of the License, or ++ * (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ * ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ * ++ * Author(s): ++ * Felipe Borges ++ */ ++ ++#undef G_LOG_DOMAIN ++#define G_LOG_DOMAIN "cc-subman-page" ++ ++#ifdef HAVE_CONFIG_H ++# include "config.h" ++#endif ++ ++#include "cc-subman-page.h" ++ ++#include "cc-list-row.h" ++#include "cc-list-row-info-button.h" ++ ++#include ++#include ++#include ++#include ++ ++#define SERVER_URL "subscription.rhsm.redhat.com" ++#define DBUS_TIMEOUT 300000 /* 5 minutes */ ++ ++typedef enum { ++ GSD_SUBMAN_SUBSCRIPTION_STATUS_NOT_READ = -1, ++ GSD_SUBMAN_SUBSCRIPTION_STATUS_UNKNOWN, ++ GSD_SUBMAN_SUBSCRIPTION_STATUS_VALID, ++ GSD_SUBMAN_SUBSCRIPTION_STATUS_INVALID, ++ GSD_SUBMAN_SUBSCRIPTION_STATUS_DISABLED, ++ GSD_SUBMAN_SUBSCRIPTION_STATUS_PARTIALLY_VALID, ++ GSD_SUBMAN_SUBSCRIPTION_STATUS_NO_INSTALLED_PRODUCTS, ++ GSD_SUBMAN_SUBSCRIPTION_STATUS_LAST ++} GsdSubmanSubscriptionStatus; ++ ++struct _CcSubmanPage { ++ AdwNavigationPage parent_instance; ++ ++ AdwNavigationView *navigation; ++ ++ GtkStack *registration_page_stack; ++ GtkWidget *register_button; ++ ++ GtkCheckButton *username_method; ++ AdwEntryRow *server_row; ++ ++ AdwEntryRow *username_row; ++ AdwPasswordEntryRow *password_row; ++ AdwEntryRow *activation_keys_row; ++ AdwEntryRow *organization_row; ++ CcListRowInfoButton *organization_info_text; ++ ++ AdwPreferencesGroup *products_group; ++ ++ /* Backend */ ++ GDBusProxy *proxy; ++ gboolean is_registered; ++ gboolean is_available; ++ ++ GPtrArray *products; ++ ++ GCancellable *cancellable; ++}; ++ ++enum { ++ PROP_0, ++ PROP_IS_AVAILABLE, ++ PROP_IS_REGISTERED, ++ N_PROPERTIES ++}; ++ ++static GParamSpec *properties[N_PROPERTIES] = { NULL }; ++ ++G_DEFINE_TYPE (CcSubmanPage, cc_subman_page, ADW_TYPE_NAVIGATION_PAGE) ++ ++typedef struct ++{ ++ gchar *product_name; ++ gchar *product_id; ++ gchar *version; ++ gchar *arch; ++ gchar *starts; ++ gchar *ends; ++} ProductData; ++ ++static void ++product_data_free (ProductData *product) ++{ ++ g_free (product->product_name); ++ g_free (product->product_id); ++ g_free (product->version); ++ g_free (product->arch); ++ g_free (product->starts); ++ g_free (product->ends); ++ g_free (product); ++} ++G_DEFINE_AUTOPTR_CLEANUP_FUNC (ProductData, product_data_free); ++ ++static void get_subscription_status (CcSubmanPage *self); ++ ++static void ++show_error (CcSubmanPage *self, ++ const gchar *message, ++ const gchar *description) ++{ ++ AdwDialog *dialog = adw_alert_dialog_new (message, description); ++ ++ adw_alert_dialog_add_responses (ADW_ALERT_DIALOG (dialog), ++ "close", _("Close"), NULL); ++ ++ adw_alert_dialog_set_default_response (ADW_ALERT_DIALOG (dialog), "close"); ++ adw_alert_dialog_set_close_response (ADW_ALERT_DIALOG (dialog), "close"); ++ ++ g_signal_connect (dialog, "response", G_CALLBACK (adw_dialog_close), NULL); ++ ++ adw_dialog_present (dialog, GTK_WIDGET (self)); ++} ++ ++static void ++unregistration_done_cb (GObject *source_object, ++ GAsyncResult *result, ++ gpointer user_data) ++{ ++ CcSubmanPage *self = CC_SUBMAN_PAGE (user_data); ++ g_autoptr(GVariant) variant_results = NULL; ++ g_autoptr(GError) error = NULL; ++ ++ gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE); ++ ++ variant_results = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), result, &error); ++ if (variant_results == NULL) { ++ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) ++ return; ++ ++ g_dbus_error_strip_remote_error (error); ++ show_error (self, _("Failed to Unregister System"), error->message); ++ return; ++ } ++ ++ g_object_set (self, "is-registered", FALSE, NULL); ++ g_debug ("System is no longer registered"); ++} ++ ++static void ++on_unregister_response_cb (AdwAlertDialog *dialog, ++ GAsyncResult *result, ++ CcSubmanPage *self) ++{ ++ const gchar *response = adw_alert_dialog_choose_finish (dialog, result); ++ ++ if (g_strcmp0 (response, "unregister") == 0) { ++ g_dbus_proxy_call (self->proxy, ++ "Unregister", ++ NULL, ++ G_DBUS_CALL_FLAGS_NONE, ++ DBUS_TIMEOUT, ++ self->cancellable, ++ unregistration_done_cb, ++ self); ++ } else { ++ gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE); ++ } ++} ++ ++static void ++unregister (CcSubmanPage *self) ++{ ++ AdwDialog *dialog = adw_alert_dialog_new (_("Remove Registration?"), ++ _("Subscriptions will be removed and the device will no longer receive software updates")); ++ adw_alert_dialog_add_responses (ADW_ALERT_DIALOG (dialog), ++ "cancel", _("Cancel"), ++ "unregister", _("Remove Registration"), NULL); ++ adw_alert_dialog_set_response_appearance (ADW_ALERT_DIALOG (dialog), ++ "unregister", ++ ADW_RESPONSE_DESTRUCTIVE); ++ adw_alert_dialog_set_default_response (ADW_ALERT_DIALOG (dialog), "cancel"); ++ adw_alert_dialog_set_close_response (ADW_ALERT_DIALOG (dialog), "cancel"); ++ ++ gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE); ++ ++ adw_alert_dialog_choose (ADW_ALERT_DIALOG (dialog), ++ GTK_WIDGET (self), ++ self->cancellable, ++ (GAsyncReadyCallback) on_unregister_response_cb, ++ self); ++} ++ ++static void ++add_product_row (GtkWidget *expander, ++ const gchar *title, ++ const gchar *value) ++{ ++ CcListRow *row = g_object_new (CC_TYPE_LIST_ROW, NULL); ++ ++ adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), title); ++ cc_list_row_set_secondary_label (row, value); ++ ++ adw_expander_row_add_row (ADW_EXPANDER_ROW (expander), GTK_WIDGET (row)); ++} ++ ++static void ++add_product (CcSubmanPage *self, ++ ProductData *product) ++{ ++ GtkWidget *product_row; ++ ++ product_row = adw_expander_row_new (); ++ adw_preferences_row_set_title (ADW_PREFERENCES_ROW (product_row), product->product_name); ++ ++ add_product_row (product_row, _("Product ID"), product->product_id); ++ add_product_row (product_row, _("Version"), product->version); ++ add_product_row (product_row, _("Arch"), product->arch); ++ ++ if (product->starts[0] != '\0' && product->ends[0] != '\0') { ++ add_product_row (product_row, _("Starts"), product->starts); ++ add_product_row (product_row, _("Ends"), product->ends); ++ } ++ ++ adw_preferences_group_add (self->products_group, product_row); ++} ++ ++static ProductData * ++parse_product_variant (GVariant *product_variant) ++{ ++ ProductData *product = g_new0 (ProductData, 1); ++ g_auto(GVariantDict) dict; ++ ++ g_variant_dict_init (&dict, product_variant); ++ ++ g_variant_dict_lookup (&dict, "product-name", "s", &product->product_name); ++ g_variant_dict_lookup (&dict, "product-id", "s", &product->product_id); ++ g_variant_dict_lookup (&dict, "version", "s", &product->version); ++ g_variant_dict_lookup (&dict, "arch", "s", &product->arch); ++ g_variant_dict_lookup (&dict, "starts", "s", &product->starts); ++ g_variant_dict_lookup (&dict, "ends", "s", &product->ends); ++ ++ return g_steal_pointer (&product); ++} ++ ++static void ++fetch_installed_products (CcSubmanPage *self) ++{ ++ g_autoptr(GVariant) installed_products_variant = NULL; ++ GVariantIter iter_array; ++ GVariant *child; ++ ++ installed_products_variant = g_dbus_proxy_get_cached_property (self->proxy, "InstalledProducts"); ++ if (installed_products_variant == NULL) { ++ g_debug ("Unable to get 'InstalledProducts' DBus property"); ++ return; ++ } ++ ++ g_ptr_array_set_size (self->products, 0); ++ ++ g_variant_iter_init (&iter_array, installed_products_variant); ++ while ((child = g_variant_iter_next_value (&iter_array)) != NULL) { ++ g_autoptr(GVariant) product_variant = g_steal_pointer (&child); ++ g_ptr_array_add (self->products, parse_product_variant (product_variant)); ++ } ++ ++ if (self->products == NULL || self->products->len == 0) { ++ // Show a no-products page? ++ } ++ ++ for (guint i = 0; i < self->products->len; i++) { ++ ProductData *product = g_ptr_array_index (self->products, i); ++ ++ add_product (self, product); ++ } ++} ++ ++static void ++registration_done_cb (GObject *source_object, ++ GAsyncResult *result, ++ gpointer user_data) ++{ ++ CcSubmanPage *self = CC_SUBMAN_PAGE (user_data); ++ g_autoptr(GVariant) results = NULL; ++ g_autoptr(GError) error = NULL; ++ ++ gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE); ++ ++ results = g_dbus_proxy_call_finish (G_DBUS_PROXY (source_object), result, &error); ++ if (results == NULL) { ++ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) ++ return; ++ ++ g_dbus_error_strip_remote_error (error); ++ ++ show_error (self, _("Failed to Register System"), error->message); ++ ++ g_warning ("Failed to register: %s", error->message); ++ return; ++ } ++ ++ g_debug ("Registration successful"); ++ get_subscription_status (self); ++} ++ ++static void ++update_register_button (CcSubmanPage *self) ++{ ++ gboolean is_registering = !gtk_widget_get_sensitive (GTK_WIDGET (self)); ++ ++ gtk_widget_set_visible (self->register_button, !is_registering); ++} ++ ++static void ++register_subscription (CcSubmanPage *self) ++{ ++ gboolean register_with_username = gtk_check_button_get_active (self->username_method); ++ g_autoptr(GVariantBuilder) options_builder = NULL; ++ const gchar *hostname; ++ const gchar *organization; ++ const gchar *username; ++ const gchar *password; ++ const gchar *activation_keys; ++ ++ gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE); ++ ++ hostname = gtk_editable_get_text (GTK_EDITABLE (self->server_row)); ++ organization = gtk_editable_get_text (GTK_EDITABLE (self->organization_row)); ++ ++ if (register_with_username) { ++ username = gtk_editable_get_text (GTK_EDITABLE (self->username_row)); ++ password = gtk_editable_get_text (GTK_EDITABLE (self->password_row)); ++ } else { ++ activation_keys = gtk_editable_get_text (GTK_EDITABLE (self->activation_keys_row)); ++ } ++ ++ options_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); ++ g_variant_builder_add (options_builder, "{sv}", "hostname", g_variant_new_string (hostname)); ++ g_variant_builder_add (options_builder, "{sv}", "organisation", g_variant_new_string (organization)); ++ if (register_with_username) { ++ g_variant_builder_add (options_builder, "{sv}", "kind", g_variant_new_string ("username")); ++ g_variant_builder_add (options_builder, "{sv}", "username", g_variant_new_string (username)); ++ g_variant_builder_add (options_builder, "{sv}", "password", g_variant_new_string (password)); ++ } else { ++ g_variant_builder_add (options_builder, "{sv}", "kind", g_variant_new_string ("key")); ++ g_variant_builder_add (options_builder, "{sv}", "activation-key", g_variant_new_string (activation_keys)); ++ } ++ ++ g_dbus_proxy_call (self->proxy, ++ "Register", ++ g_variant_new ("(a{sv})", options_builder), ++ G_DBUS_CALL_FLAGS_NONE, ++ DBUS_TIMEOUT, ++ self->cancellable, ++ registration_done_cb, ++ self); ++} ++ ++static void ++update_for_status (CcSubmanPage *self, ++ GsdSubmanSubscriptionStatus status) ++{ ++ switch (status) { ++ case GSD_SUBMAN_SUBSCRIPTION_STATUS_NOT_READ: ++ g_object_set (G_OBJECT (self), "is-registered", FALSE, NULL); ++ break;// Why return?; ++ case GSD_SUBMAN_SUBSCRIPTION_STATUS_UNKNOWN: ++ g_object_set (G_OBJECT (self), "is-registered", FALSE, NULL); ++ break; ++ case GSD_SUBMAN_SUBSCRIPTION_STATUS_DISABLED: ++ g_object_set (G_OBJECT (self), "is-registered", TRUE, NULL); ++ break; ++ case GSD_SUBMAN_SUBSCRIPTION_STATUS_VALID: ++ case GSD_SUBMAN_SUBSCRIPTION_STATUS_PARTIALLY_VALID: ++ g_object_set (G_OBJECT (self), "is-registered", TRUE, NULL); ++ break; ++ case GSD_SUBMAN_SUBSCRIPTION_STATUS_INVALID: ++ g_object_set (G_OBJECT (self), "is-registered", TRUE, NULL); ++ break; ++ case GSD_SUBMAN_SUBSCRIPTION_STATUS_NO_INSTALLED_PRODUCTS: ++ g_object_set (G_OBJECT (self), "is-registered", FALSE, NULL); ++ break; ++ default: ++ g_assert_not_reached (); ++ } ++} ++ ++static void ++get_subscription_status (CcSubmanPage *self) ++{ ++ g_autoptr(GVariant) status_variant = NULL; ++ g_autoptr(GError) error = NULL; ++ guint32 u; ++ ++ if (self->proxy == NULL) { ++ self->proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, ++ G_DBUS_PROXY_FLAGS_NONE, ++ NULL, ++ "org.gnome.SettingsDaemon.Subscription", ++ "/org/gnome/SettingsDaemon/Subscription", ++ "org.gnome.SettingsDaemon.Subscription", ++ NULL, &error); ++ if (error != NULL) { ++ g_debug ("Unable to create proxy for org.gnome.SettingsDaemon.Subscription: %s", error->message); ++ ++ self->is_available = FALSE; ++ return; ++ } ++ ++ g_signal_connect_swapped (self->proxy, "g-properties-changed", ++ G_CALLBACK (get_subscription_status), self); ++ } ++ ++ status_variant = g_dbus_proxy_get_cached_property (self->proxy, "SubscriptionStatus"); ++ if (!status_variant) { ++ g_debug ("Unable to get SubscriptionStatus property"); ++ ++ self->is_available = FALSE; ++ return; ++ } ++ ++ g_debug ("Subscription manager is available"); ++ self->is_available = TRUE; ++ ++ g_variant_get (status_variant, "u", &u); ++ update_for_status (self, u); ++} ++ ++static void ++validate_registration_details (CcSubmanPage *self) ++{ ++ gboolean username_auth = gtk_check_button_get_active (self->username_method); ++ const gchar *server_url = gtk_editable_get_text (GTK_EDITABLE (self->server_row)); ++ gboolean can_register = FALSE; ++ ++ can_register = (g_strcmp0 (server_url, "") != 0); ++ if (username_auth) { ++ const gchar *username = gtk_editable_get_text (GTK_EDITABLE (self->username_row)); ++ const gchar *password = gtk_editable_get_text (GTK_EDITABLE (self->password_row)); ++ ++ can_register = can_register && (g_strcmp0 (username, "") != 0) && (g_strcmp0 (password, "") != 0); ++ } else { ++ const gchar *activation_keys = gtk_editable_get_text (GTK_EDITABLE (self->activation_keys_row)); ++ const gchar *organization = gtk_editable_get_text (GTK_EDITABLE (self->organization_row)); ++ ++ can_register = can_register && (g_strcmp0 (activation_keys, "") != 0) && (g_strcmp0 (organization, "") != 0); ++ } ++ ++ gtk_widget_set_sensitive (self->register_button, can_register); ++} ++ ++static void ++on_is_registered_changed (CcSubmanPage *self) ++{ ++ adw_navigation_view_replace (self->navigation, NULL, 0); ++ adw_navigation_view_push_by_tag (self->navigation, ++ self->is_registered ? "products-page" : "registration-page"); ++} ++ ++static void ++update_organization_info_text (CcSubmanPage *self) ++{ ++ gboolean username_auth = gtk_check_button_get_active (self->username_method); ++ ++ cc_list_row_info_button_set_text (self->organization_info_text, ++ username_auth ? _("If the user account belongs to multiple organizations, an organization must be specified.") : ++ _("Organization must be specified when registering with an activation key.")); ++} ++ ++static void ++reset_server_row_button_clicked_cb (CcSubmanPage *self) ++{ ++ gtk_editable_set_text (GTK_EDITABLE (self->server_row), SERVER_URL); ++} ++ ++static void ++on_register_system_button_clicked_cb (CcSubmanPage *self) ++{ ++ gtk_stack_set_visible_child_name (self->registration_page_stack, "registration"); ++} ++ ++static void ++cc_subman_page_set_property (GObject *object, ++ guint prop_id, ++ const GValue *value, ++ GParamSpec *pspec) ++{ ++ CcSubmanPage *self = CC_SUBMAN_PAGE (object); ++ ++ switch (prop_id) { ++ case PROP_IS_REGISTERED: ++ self->is_registered = g_value_get_boolean (value); ++ break; ++ default: ++ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); ++ break; ++ } ++} ++ ++static void ++cc_subman_page_get_property (GObject *object, ++ guint prop_id, ++ GValue *value, ++ GParamSpec *pspec) ++{ ++ CcSubmanPage *self = CC_SUBMAN_PAGE (object); ++ ++ switch (prop_id) { ++ case PROP_IS_AVAILABLE: ++ g_value_set_boolean (value, self->is_available); ++ break; ++ case PROP_IS_REGISTERED: ++ g_value_set_boolean (value, self->is_registered); ++ break; ++ default: ++ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); ++ break; ++ } ++} ++ ++static void ++cc_subman_page_dispose (GObject *object) ++{ ++ CcSubmanPage *self = CC_SUBMAN_PAGE (object); ++ ++ g_cancellable_cancel (self->cancellable); ++ g_clear_object (&self->cancellable); ++ ++ g_clear_object (&self->proxy); ++ ++ G_OBJECT_CLASS (cc_subman_page_parent_class)->dispose (object); ++} ++ ++static void ++cc_subman_page_init (CcSubmanPage *self) ++{ ++ gtk_widget_init_template (GTK_WIDGET (self)); ++ ++ gtk_editable_set_text (GTK_EDITABLE (self->server_row), SERVER_URL); ++ update_organization_info_text (self); ++ self->products = g_ptr_array_new_with_free_func ((GDestroyNotify) product_data_free); ++ ++ g_signal_connect_swapped (G_OBJECT (self), ++ "notify::is-registered", ++ G_CALLBACK (on_is_registered_changed), ++ self); ++ ++ self->cancellable = g_cancellable_new (); ++} ++ ++static void ++cc_subman_page_class_init (CcSubmanPageClass * klass) ++{ ++ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); ++ GObjectClass *object_class = G_OBJECT_CLASS (klass); ++ ++ object_class->set_property = cc_subman_page_set_property; ++ object_class->get_property = cc_subman_page_get_property; ++ object_class->finalize = cc_subman_page_dispose; ++ ++ properties[PROP_IS_AVAILABLE] = ++ g_param_spec_boolean ("is-available", ++ "Subscription Manager is available", ++ "Whether Subscription Manager service is available in the system.", ++ FALSE, ++ G_PARAM_READABLE); ++ properties[PROP_IS_REGISTERED] = ++ g_param_spec_boolean ("is-registered", ++ "System is registered", ++ "The system is registered with subscription manager", ++ FALSE, ++ G_PARAM_READWRITE); ++ ++ g_object_class_install_properties (object_class, N_PROPERTIES, properties); ++ ++ g_type_ensure (CC_TYPE_LIST_ROW); ++ g_type_ensure (CC_TYPE_LIST_ROW_INFO_BUTTON); ++ ++ gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/system/subman/cc-subman-page.ui"); ++ ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, navigation); ++ ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, username_method); ++ ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, server_row); ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, organization_row); ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, organization_info_text); ++ ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, username_row); ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, password_row); ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, activation_keys_row); ++ ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, register_button); ++ ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, registration_page_stack); ++ gtk_widget_class_bind_template_child (widget_class, CcSubmanPage, products_group); ++ ++ gtk_widget_class_bind_template_callback (widget_class, validate_registration_details); ++ gtk_widget_class_bind_template_callback (widget_class, register_subscription); ++ gtk_widget_class_bind_template_callback (widget_class, unregister); ++ gtk_widget_class_bind_template_callback (widget_class, reset_server_row_button_clicked_cb); ++ gtk_widget_class_bind_template_callback (widget_class, update_organization_info_text); ++ gtk_widget_class_bind_template_callback (widget_class, update_register_button); ++ gtk_widget_class_bind_template_callback (widget_class, on_register_system_button_clicked_cb); ++} ++ ++gboolean ++cc_subman_page_is_available (CcSubmanPage *self) ++{ ++ get_subscription_status (self); ++ ++ fetch_installed_products (self); ++ ++ return self->is_available; ++} +diff --git a/panels/system/subman/cc-subman-page.h b/panels/system/subman/cc-subman-page.h +new file mode 100644 +index 000000000..875b1e57a +--- /dev/null ++++ b/panels/system/subman/cc-subman-page.h +@@ -0,0 +1,38 @@ ++/* ++ * cc-subman-page.h ++ * ++ * Copyright 2025 Red Hat Inc ++ * ++ * This program is free software: you can redistribute it and/or modify ++ * it under the terms of the GNU General Public License as published by ++ * the Free Software Foundation, either version 3 of the License, or ++ * (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ * ++ * SPDX-License-Identifier: GPL-3.0-or-later ++ * ++ * Author(s): ++ * Felipe Borges ++ */ ++ ++#pragma once ++ ++#include ++ ++G_BEGIN_DECLS ++ ++#define CC_TYPE_SUBMAN_PAGE (cc_subman_page_get_type ()) ++ ++G_DECLARE_FINAL_TYPE (CcSubmanPage, cc_subman_page, CC, SUBMAN_PAGE, AdwNavigationPage) ++ ++gboolean cc_subman_page_is_available (CcSubmanPage *self); ++ ++G_END_DECLS ++ +diff --git a/panels/system/subman/cc-subman-page.ui b/panels/system/subman/cc-subman-page.ui +new file mode 100644 +index 000000000..025f23172 +--- /dev/null ++++ b/panels/system/subman/cc-subman-page.ui +@@ -0,0 +1,254 @@ ++ ++ ++ ++ +diff --git a/panels/system/subman/gnome-subman-panel.desktop.in b/panels/system/subman/gnome-subman-panel.desktop.in +new file mode 100644 +index 000000000..ca67b33af +--- /dev/null ++++ b/panels/system/subman/gnome-subman-panel.desktop.in +@@ -0,0 +1,14 @@ ++[Desktop Entry] ++Name=Registration ++Comment=Enable Red Hat updates, content, and services ++Exec=gnome-control-center system subman ++# Translators: Do NOT translate or transliterate this text (this is an icon file name)! ++Icon=key-symbolic ++Terminal=false ++Type=Application ++NoDisplay=true ++StartupNotify=true ++Categories=GNOME;GTK;Settings;X-GNOME-Settings-Panel;X-GNOME-SystemSettings; ++OnlyShowIn=GNOME;Unity; ++# Translators: Search terms to find the Users panel. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! ++Keywords=Registration;Subscription;Red Hat;Products;Updates;Registry;Account;Activation;Register;Subscribe; +diff --git a/panels/system/subman/meson.build b/panels/system/subman/meson.build +new file mode 100644 +index 000000000..58a99ee49 +--- /dev/null ++++ b/panels/system/subman/meson.build +@@ -0,0 +1,9 @@ ++desktop = 'gnome-subman-panel.desktop' ++i18n.merge_file( ++ type: 'desktop', ++ input: desktop + '.in', ++ output: desktop, ++ po_dir: po_dir, ++ install: true, ++ install_dir: control_center_desktopdir ++) +diff --git a/panels/system/system.gresource.xml b/panels/system/system.gresource.xml +index acb4169b1..3d5c0684e 100644 +--- a/panels/system/system.gresource.xml ++++ b/panels/system/system.gresource.xml +@@ -29,9 +29,12 @@ + users/cc-user-page.ui + users/data/join-dialog.ui + users/users.css ++ ++ subman/cc-subman-page.ui + + + ++ icons/scalable/actions/key-symbolic.svg + icons/scalable/actions/system-update-symbolic.svg + icons/scalable/status/fingerprint-detection-complete-symbolic.svg + icons/scalable/status/fingerprint-detection-symbolic.svg +-- +2.47.0 +