From 9a0222aee3f1e98f0298af827227c30c396990ca Mon Sep 17 00:00:00 2001 From: NickVs2015 Date: Wed, 25 Mar 2026 07:34:42 +0300 Subject: [PATCH] fix: ui fixes for renewal subscription (#2406) --- .../src/org/amnezia/vpn/AmneziaActivity.kt | 16 ++--- client/core/controllers/gatewayController.cpp | 3 + client/ui/models/api/apiAccountInfoModel.cpp | 2 +- client/ui/qml/Components/ServersListView.qml | 28 ++++++++ .../Components/SubscriptionExpiredDrawer.qml | 28 ++------ .../ui/qml/Controls2/LabelWithButtonType.qml | 10 +++ .../PageSettingsApiAvailableCountries.qml | 72 ++++++++++++++++++- .../qml/Pages2/PageSettingsApiServerInfo.qml | 71 +++++++++++++++++- 8 files changed, 193 insertions(+), 37 deletions(-) diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index 1d2e09ccb..ccdc22140 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -337,26 +337,26 @@ class AmneziaActivity : QtActivity() { private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean) override fun onPause() { + // Notify Qt to stop rendering BEFORE super.onPause() destroys the EGL surface. + // Using a coroutine here would be too late — the surface is gone by the time + // the coroutine runs. A direct synchronous call gives Qt's render thread the + // best chance to process visible=false before surface destruction. + if (qtInitialized.isCompleted) { + QtAndroidController.onActivityPaused() + } super.onPause() isActivityResumed = false // Cancel all pending operations when activity pauses resumeHandler.removeCallbacksAndMessages(null) openFileDeliveryScheduled = false Log.d(TAG, "Pause Amnezia activity") - // Notify Qt to stop rendering before the EGL surface is disconnected - mainScope.launch { - qtInitialized.await() - QtAndroidController.onActivityPaused() - } } override fun onResume() { super.onResume() isActivityResumed = true Log.d(TAG, "Resume Amnezia activity") - // Notify Qt to resume rendering after surface reconnects - mainScope.launch { - qtInitialized.await() + if (qtInitialized.isCompleted) { QtAndroidController.onActivityResumed() } diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 25a40c460..4a2fe8d22 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -46,6 +46,7 @@ namespace constexpr int httpStatusCodeConflict = 409; constexpr int httpStatusCodeNotImplemented = 501; + constexpr int httpStatusCodeUnprocessableEntity = 422; } GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, @@ -451,6 +452,8 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep } } else if (httpStatus == httpStatusCodeConflict) { return false; + } else if (httpStatus == httpStatusCodeUnprocessableEntity) { + return false; } else if (replyError != QNetworkReply::NetworkError::NoError) { qDebug() << replyError; return true; diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp index 6c59fc907..3bd6c80ca 100644 --- a/client/ui/models/api/apiAccountInfoModel.cpp +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -33,7 +33,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const } return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("

Inactive") - : tr("Active"); + : tr("

Active"); } case EndDateRole: { if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) { diff --git a/client/ui/qml/Components/ServersListView.qml b/client/ui/qml/Components/ServersListView.qml index 4417e0b29..47eceb3f8 100644 --- a/client/ui/qml/Components/ServersListView.qml +++ b/client/ui/qml/Components/ServersListView.qml @@ -19,6 +19,15 @@ ListViewType { id: root property int selectedIndex: ServersModel.defaultIndex + property int expiredServerIndex: -1 + property bool expiringSoon: false + + Connections { + target: ApiAccountInfoModel + function onModelReset() { + root.expiringSoon = ApiAccountInfoModel.data("isSubscriptionExpiringSoon") + } + } anchors.top: serversMenuHeader.bottom anchors.right: parent.right @@ -35,6 +44,13 @@ ListViewType { } } + Connections { + target: ApiConfigsController + function onSubscriptionExpiredOnServer() { + root.expiredServerIndex = ServersModel.defaultIndex + } + } + delegate: Item { id: menuContentDelegate objectName: "menuContentDelegate" @@ -126,6 +142,18 @@ ListViewType { } } + CaptionTextType { + visible: isServerFromGatewayApi && (index === root.expiredServerIndex || (root.expiringSoon && index === root.selectedIndex && index !== root.expiredServerIndex)) + + Layout.fillWidth: true + Layout.leftMargin: 64 + Layout.bottomMargin: 8 + + text: index === root.expiredServerIndex ? qsTr("Subscription expired. Please renew.") : qsTr("Subscription expiring soon.") + color: index === root.expiredServerIndex ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot + wrapMode: Text.WordWrap + } + DividerType { Layout.fillWidth: true Layout.leftMargin: 0 diff --git a/client/ui/qml/Components/SubscriptionExpiredDrawer.qml b/client/ui/qml/Components/SubscriptionExpiredDrawer.qml index 2a1adceda..97fb01b85 100644 --- a/client/ui/qml/Components/SubscriptionExpiredDrawer.qml +++ b/client/ui/qml/Components/SubscriptionExpiredDrawer.qml @@ -37,29 +37,11 @@ DrawerType2 { Header2TextType { id: titleText anchors.left: parent.left - anchors.right: icon.left - anchors.rightMargin: 8 + anchors.right: parent.right text: qsTr("Amnezia Premium subscription has expired") horizontalAlignment: Text.AlignLeft } - - Image { - id: icon - anchors.right: parent.right - anchors.top: parent.top - width: 40 - height: 40 - source: "qrc:/images/controls/history.svg" - fillMode: Image.PreserveAspectFit - visible: false - } - - ColorOverlay { - anchors.fill: icon - source: icon - color: AmneziaStyle.color.goldenApricot - } } ParagraphTextType { @@ -91,11 +73,11 @@ DrawerType2 { } BasicButtonType { - Layout.fillWidth: true - Layout.topMargin: 4 + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: 8 Layout.bottomMargin: 8 - Layout.rightMargin: 16 - Layout.leftMargin: 16 + + implicitHeight: 25 defaultColor: AmneziaStyle.color.transparent hoveredColor: AmneziaStyle.color.translucentWhite diff --git a/client/ui/qml/Controls2/LabelWithButtonType.qml b/client/ui/qml/Controls2/LabelWithButtonType.qml index 6a4a88106..24844bda5 100644 --- a/client/ui/qml/Controls2/LabelWithButtonType.qml +++ b/client/ui/qml/Controls2/LabelWithButtonType.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Qt5Compat.GraphicalEffects import Style 1.0 @@ -37,6 +38,7 @@ Item { property int borderFocusedWidth: 1 property string rightImageColor: AmneziaStyle.color.paleGray + property string leftImageColor: "" property bool descriptionOnTop: false property bool hideDescription: true @@ -140,6 +142,14 @@ Item { anchors.centerIn: parent source: leftImageSource + visible: leftImageColor === "" + } + + ColorOverlay { + anchors.fill: leftImage + source: leftImage + color: leftImageColor + visible: leftImageColor !== "" } } diff --git a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml index d2787f590..93599d960 100644 --- a/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml +++ b/client/ui/qml/Pages2/PageSettingsApiAvailableCountries.qml @@ -18,6 +18,22 @@ PageType { id: root property var processedServer + property bool subscriptionExpired: false + property bool subscriptionExpiringSoon: false + function updateSubscriptionState() { + root.subscriptionExpiringSoon = ApiAccountInfoModel.data("isSubscriptionExpiringSoon") + } + + Component.onCompleted: { + root.updateSubscriptionState() + } + + Connections { + target: ApiAccountInfoModel + function onModelReset() { + root.updateSubscriptionState() + } + } Connections { target: ServersModel @@ -27,6 +43,15 @@ PageType { } } + Connections { + target: ApiConfigsController + + function onSubscriptionExpiredOnServer() { + root.subscriptionExpired = true + root.subscriptionExpiringSoon = false + } + } + SortFilterProxyModel { id: proxyServersModel objectName: "proxyServersModel" @@ -76,12 +101,11 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.bottomMargin: 10 + Layout.bottomMargin: 4 actionButtonImage: "qrc:/images/controls/settings.svg" headerText: root.processedServer.name - descriptionText: qsTr("Location for connection") actionButtonFunction: function() { PageController.showBusyIndicator(true) @@ -94,6 +118,50 @@ PageType { PageController.goToPage(PageEnum.PageSettingsApiServerInfo) } } + + CaptionTextType { + visible: root.subscriptionExpired || root.subscriptionExpiringSoon + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 4 + + text: root.subscriptionExpired ? qsTr("Subscription expired") : qsTr("Subscription expiring soon") + color: root.subscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot + } + + BasicButtonType { + visible: root.subscriptionExpired || root.subscriptionExpiringSoon + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 8 + Layout.bottomMargin: 4 + + defaultColor: AmneziaStyle.color.paleGray + hoveredColor: AmneziaStyle.color.lightGray + pressedColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.midnightBlack + + text: qsTr("Renew subscription") + + clickedFunc: function() { + ApiSettingsController.getRenewalLink() + } + } + + CaptionTextType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: (root.subscriptionExpired || root.subscriptionExpiringSoon) ? 8 : 4 + Layout.bottomMargin: 8 + + text: qsTr("Location for connection") + color: AmneziaStyle.color.mutedGray + } } delegate: ColumnLayout { diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index 140b17f29..7e44138a5 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Dialogs +import Qt5Compat.GraphicalEffects import SortFilterProxyModel 0.2 @@ -128,7 +129,6 @@ PageType { actionButtonImage: "qrc:/images/controls/edit-3.svg" headerText: root.processedServer.name - descriptionText: ApiAccountInfoModel.data("serviceDescription") actionButtonFunction: function() { serverNameEditDrawer.openTriggered() @@ -156,6 +156,19 @@ PageType { wrapMode: Text.WordWrap } + ParagraphTextType { + visible: ApiAccountInfoModel.data("serviceDescription") !== "" + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 16 + Layout.bottomMargin: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon ? 0 : 10 + + text: ApiAccountInfoModel.data("serviceDescription") + color: AmneziaStyle.color.mutedGray + } + BasicButtonType { visible: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon @@ -213,6 +226,54 @@ PageType { readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible") + Item { + visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon + + Layout.fillWidth: true + implicitHeight: renewRow.implicitHeight + 32 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: ApiSettingsController.getRenewalLink() + } + + Row { + id: renewRow + anchors.centerIn: parent + spacing: 12 + + Item { + width: renewIcon.implicitWidth + height: renewIcon.implicitHeight + anchors.verticalCenter: parent.verticalCenter + + Image { + id: renewIcon + source: "qrc:/images/controls/refresh-cw.svg" + } + + ColorOverlay { + anchors.fill: renewIcon + source: renewIcon + color: AmneziaStyle.color.goldenApricot + } + } + + Text { + text: qsTr("Renew subscription") + color: AmneziaStyle.color.goldenApricot + font.pixelSize: 18 + font.weight: Font.Medium + anchors.verticalCenter: parent.verticalCenter + } + } + } + + DividerType { + visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon + } + SwitcherType { id: switcher @@ -239,10 +300,14 @@ PageType { } } + DividerType { + visible: footer.isVisibleForAmneziaFree + } + WarningType { id: warning - Layout.topMargin: 32 + Layout.topMargin: 24 Layout.rightMargin: 16 Layout.leftMargin: 16 Layout.fillWidth: true @@ -266,7 +331,7 @@ PageType { id: vpnKey Layout.fillWidth: true - Layout.topMargin: warning.visible ? 16 : 32 + Layout.topMargin: warning.visible ? 16 : 0 visible: footer.isVisibleForAmneziaFree