Merge pull request #1440 from amnezia-vpn/feature/subscription-settings-page

feature/subscription settings page
This commit is contained in:
Nethius 2025-02-28 18:17:43 +03:00 committed by GitHub
parent 7ccbfa48bc
commit 728b48044c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 466 additions and 81 deletions

View file

@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN) set(PROJECT AmneziaVPN)
project(${PROJECT} VERSION 4.8.4.1 project(${PROJECT} VERSION 4.8.4.2
DESCRIPTION "AmneziaVPN" DESCRIPTION "AmneziaVPN"
HOMEPAGE_URL "https://amnezia.org/" HOMEPAGE_URL "https://amnezia.org/"
) )
@ -11,7 +11,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}") set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}) set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2078) set(APP_ANDROID_VERSION_CODE 2079)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux") set(MZ_PLATFORM_NAME "linux")

View file

@ -93,6 +93,9 @@ void CoreController::initModels()
m_apiAccountInfoModel.reset(new ApiAccountInfoModel(this)); m_apiAccountInfoModel.reset(new ApiAccountInfoModel(this));
m_engine->rootContext()->setContextProperty("ApiAccountInfoModel", m_apiAccountInfoModel.get()); m_engine->rootContext()->setContextProperty("ApiAccountInfoModel", m_apiAccountInfoModel.get());
m_apiDevicesModel.reset(new ApiDevicesModel(m_settings, this));
m_engine->rootContext()->setContextProperty("ApiDevicesModel", m_apiDevicesModel.get());
} }
void CoreController::initControllers() void CoreController::initControllers()
@ -132,7 +135,8 @@ void CoreController::initControllers()
m_systemController.reset(new SystemController(m_settings)); m_systemController.reset(new SystemController(m_settings));
m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get()); m_engine->rootContext()->setContextProperty("SystemController", m_systemController.get());
m_apiSettingsController.reset(new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_settings)); m_apiSettingsController.reset(
new ApiSettingsController(m_serversModel, m_apiAccountInfoModel, m_apiCountryModel, m_apiDevicesModel, m_settings));
m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get()); m_engine->rootContext()->setContextProperty("ApiSettingsController", m_apiSettingsController.get());
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings)); m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));

View file

@ -25,8 +25,9 @@
#include "ui/models/protocols/ikev2ConfigModel.h" #include "ui/models/protocols/ikev2ConfigModel.h"
#endif #endif
#include "ui/models/api/apiAccountInfoModel.h" #include "ui/models/api/apiAccountInfoModel.h"
#include "ui/models/api/apiServicesModel.h"
#include "ui/models/api/apiCountryModel.h" #include "ui/models/api/apiCountryModel.h"
#include "ui/models/api/apiDevicesModel.h"
#include "ui/models/api/apiServicesModel.h"
#include "ui/models/appSplitTunnelingModel.h" #include "ui/models/appSplitTunnelingModel.h"
#include "ui/models/clientManagementModel.h" #include "ui/models/clientManagementModel.h"
#include "ui/models/protocols/awgConfigModel.h" #include "ui/models/protocols/awgConfigModel.h"
@ -117,6 +118,7 @@ private:
QSharedPointer<ApiServicesModel> m_apiServicesModel; QSharedPointer<ApiServicesModel> m_apiServicesModel;
QSharedPointer<ApiCountryModel> m_apiCountryModel; QSharedPointer<ApiCountryModel> m_apiCountryModel;
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel; QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
QScopedPointer<OpenVpnConfigModel> m_openVpnConfigModel; QScopedPointer<OpenVpnConfigModel> m_openVpnConfigModel;
QScopedPointer<ShadowSocksConfigModel> m_shadowSocksConfigModel; QScopedPointer<ShadowSocksConfigModel> m_shadowSocksConfigModel;

View file

@ -148,7 +148,7 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
QByteArray encryptedResponseBody = reply->readAll(); QByteArray encryptedResponseBody = reply->readAll();
if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, false)) { if (sslErrors.isEmpty() && shouldBypassProxy(reply, encryptedResponseBody, true, key, iv, salt)) {
auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) { auto requestFunction = [&request, &encryptedResponseBody, &requestBody](const QString &url) {
request.setUrl(url); request.setUrl(url);
return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson()); return amnApp->networkManager()->post(request, QJsonDocument(requestBody).toJson());

View file

@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 3H4C2.89543 3 2 3.89543 2 5V15C2 16.1046 2.89543 17 4 17H20C21.1046 17 22 16.1046 22 15V5C22 3.89543 21.1046 3 20 3Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 21H16" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 17V21" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 522 B

View file

@ -229,6 +229,8 @@
<file>ui/qml/Pages2/PageSettingsApiSupport.qml</file> <file>ui/qml/Pages2/PageSettingsApiSupport.qml</file>
<file>ui/qml/Pages2/PageSettingsApiInstructions.qml</file> <file>ui/qml/Pages2/PageSettingsApiInstructions.qml</file>
<file>ui/qml/Pages2/PageSettingsApiNativeConfigs.qml</file> <file>ui/qml/Pages2/PageSettingsApiNativeConfigs.qml</file>
<file>ui/qml/Pages2/PageSettingsApiDevices.qml</file>
<file>images/controls/monitor.svg</file>
</qresource> </qresource>
<qresource prefix="/countriesFlags"> <qresource prefix="/countriesFlags">
<file>images/flagKit/ZW.svg</file> <file>images/flagKit/ZW.svg</file>

View file

@ -1,7 +1,7 @@
#include "apiConfigsController.h" #include "apiConfigsController.h"
#include <QEventLoop>
#include <QClipboard> #include <QClipboard>
#include <QEventLoop>
#include "amnezia_application.h" #include "amnezia_application.h"
#include "configurators/wireguard_configurator.h" #include "configurators/wireguard_configurator.h"
@ -251,6 +251,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
newServerConfig.insert(configKey::apiConfig, newApiConfig); newServerConfig.insert(configKey::apiConfig, newApiConfig);
newServerConfig.insert(configKey::authData, authData); newServerConfig.insert(configKey::authData, authData);
// newServerConfig.insert(
m_serversModel->editServer(newServerConfig, serverIndex); m_serversModel->editServer(newServerConfig, serverIndex);
if (reloadServiceConfig) { if (reloadServiceConfig) {
@ -354,6 +355,43 @@ bool ApiConfigsController::deactivateDevice()
return true; return true;
} }
bool ApiConfigsController::deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode)
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs);
auto serverIndex = m_serversModel->getProcessedServerIndex();
auto serverConfigObject = m_serversModel->getServerConfig(serverIndex);
auto apiConfigObject = serverConfigObject.value(configKey::apiConfig).toObject();
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
return true;
}
QString protocol = apiConfigObject.value(configKey::serviceProtocol).toString();
ApiPayloadData apiPayloadData = generateApiPayloadData(protocol);
QJsonObject apiPayload = fillApiPayload(protocol, apiPayloadData);
apiPayload[configKey::userCountryCode] = apiConfigObject.value(configKey::userCountryCode);
apiPayload[configKey::serverCountryCode] = serverCountryCode;
apiPayload[configKey::serviceType] = apiConfigObject.value(configKey::serviceType);
apiPayload[configKey::authData] = serverConfigObject.value(configKey::authData);
apiPayload[configKey::uuid] = uuid;
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/revoke_config"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError && errorCode != ErrorCode::ApiNotFoundError) {
emit errorOccurred(errorCode);
return false;
}
if (uuid == m_settings->getInstallationUuid(true)) {
serverConfigObject.remove(config_key::containers);
m_serversModel->editServer(serverConfigObject, serverIndex);
}
return true;
}
bool ApiConfigsController::isConfigValid() bool ApiConfigsController::isConfigValid()
{ {
int serverIndex = m_serversModel->getDefaultServerIndex(); int serverIndex = m_serversModel->getDefaultServerIndex();

View file

@ -31,6 +31,7 @@ public slots:
bool reloadServiceConfig = false); bool reloadServiceConfig = false);
bool updateServiceFromTelegram(const int serverIndex); bool updateServiceFromTelegram(const int serverIndex);
bool deactivateDevice(); bool deactivateDevice();
bool deactivateExternalDevice(const QString &uuid, const QString &serverCountryCode);
bool isConfigValid(); bool isConfigValid();

View file

@ -25,11 +25,13 @@ namespace
ApiSettingsController::ApiSettingsController(const QSharedPointer<ServersModel> &serversModel, ApiSettingsController::ApiSettingsController(const QSharedPointer<ServersModel> &serversModel,
const QSharedPointer<ApiAccountInfoModel> &apiAccountInfoModel, const QSharedPointer<ApiAccountInfoModel> &apiAccountInfoModel,
const QSharedPointer<ApiCountryModel> &apiCountryModel, const QSharedPointer<ApiCountryModel> &apiCountryModel,
const QSharedPointer<ApiDevicesModel> &apiDevicesModel,
const std::shared_ptr<Settings> &settings, QObject *parent) const std::shared_ptr<Settings> &settings, QObject *parent)
: QObject(parent), : QObject(parent),
m_serversModel(serversModel), m_serversModel(serversModel),
m_apiAccountInfoModel(apiAccountInfoModel), m_apiAccountInfoModel(apiAccountInfoModel),
m_apiCountryModel(apiCountryModel), m_apiCountryModel(apiCountryModel),
m_apiDevicesModel(apiDevicesModel),
m_settings(settings) m_settings(settings)
{ {
} }
@ -73,6 +75,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
if (reload) { if (reload) {
updateApiCountryModel(); updateApiCountryModel();
updateApiDevicesModel();
} }
return true; return true;
@ -83,3 +86,8 @@ void ApiSettingsController::updateApiCountryModel()
m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), ""); m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), "");
m_apiCountryModel->updateIssuedConfigsInfo(m_apiAccountInfoModel->getIssuedConfigsInfo()); m_apiCountryModel->updateIssuedConfigsInfo(m_apiAccountInfoModel->getIssuedConfigsInfo());
} }
void ApiSettingsController::updateApiDevicesModel()
{
m_apiDevicesModel->updateModel(m_apiAccountInfoModel->getIssuedConfigsInfo());
}

View file

@ -5,6 +5,7 @@
#include "ui/models/api/apiAccountInfoModel.h" #include "ui/models/api/apiAccountInfoModel.h"
#include "ui/models/api/apiCountryModel.h" #include "ui/models/api/apiCountryModel.h"
#include "ui/models/api/apiDevicesModel.h"
#include "ui/models/servers_model.h" #include "ui/models/servers_model.h"
class ApiSettingsController : public QObject class ApiSettingsController : public QObject
@ -12,13 +13,14 @@ class ApiSettingsController : public QObject
Q_OBJECT Q_OBJECT
public: public:
ApiSettingsController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ApiAccountInfoModel> &apiAccountInfoModel, ApiSettingsController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ApiAccountInfoModel> &apiAccountInfoModel,
const QSharedPointer<ApiCountryModel> &apiCountryModel, const std::shared_ptr<Settings> &settings, const QSharedPointer<ApiCountryModel> &apiCountryModel, const QSharedPointer<ApiDevicesModel> &apiDevicesModel,
QObject *parent = nullptr); const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
~ApiSettingsController(); ~ApiSettingsController();
public slots: public slots:
bool getAccountInfo(bool reload); bool getAccountInfo(bool reload);
void updateApiCountryModel(); void updateApiCountryModel();
void updateApiDevicesModel();
signals: signals:
void errorOccurred(ErrorCode errorCode); void errorOccurred(ErrorCode errorCode);
@ -27,6 +29,7 @@ private:
QSharedPointer<ServersModel> m_serversModel; QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel; QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
QSharedPointer<ApiCountryModel> m_apiCountryModel; QSharedPointer<ApiCountryModel> m_apiCountryModel;
QSharedPointer<ApiDevicesModel> m_apiDevicesModel;
std::shared_ptr<Settings> m_settings; std::shared_ptr<Settings> m_settings;
}; };

View file

@ -36,6 +36,7 @@ namespace PageLoader
PageSettingsApiSupport, PageSettingsApiSupport,
PageSettingsApiInstructions, PageSettingsApiInstructions,
PageSettingsApiNativeConfigs, PageSettingsApiNativeConfigs,
PageSettingsApiDevices,
PageServiceSftpSettings, PageServiceSftpSettings,
PageServiceTorWebsiteSettings, PageServiceTorWebsiteSettings,

View file

@ -58,6 +58,19 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
case IsComponentVisibleRole: { case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2; return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2;
} }
case HasExpiredWorkerRole: {
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
QJsonObject issuedConfigObject = m_issuedConfigsInfo.at(i).toObject();
auto lastDownloaded = QDateTime::fromString(issuedConfigObject.value(apiDefs::key::lastDownloaded).toString());
auto workerLastUpdated = QDateTime::fromString(issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString());
if (lastDownloaded < workerLastUpdated) {
return true;
}
}
return false;
}
} }
return QVariant(); return QVariant();
@ -124,6 +137,7 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
roles[ConnectedDevicesRole] = "connectedDevices"; roles[ConnectedDevicesRole] = "connectedDevices";
roles[ServiceDescriptionRole] = "serviceDescription"; roles[ServiceDescriptionRole] = "serviceDescription";
roles[IsComponentVisibleRole] = "isComponentVisible"; roles[IsComponentVisibleRole] = "isComponentVisible";
roles[HasExpiredWorkerRole] = "hasExpiredWorker";
return roles; return roles;
} }

View file

@ -17,7 +17,8 @@ public:
ConnectedDevicesRole, ConnectedDevicesRole,
ServiceDescriptionRole, ServiceDescriptionRole,
EndDateRole, EndDateRole,
IsComponentVisibleRole IsComponentVisibleRole,
HasExpiredWorkerRole
}; };
explicit ApiAccountInfoModel(QObject *parent = nullptr); explicit ApiAccountInfoModel(QObject *parent = nullptr);

View file

@ -44,6 +44,9 @@ QVariant ApiCountryModel::data(const QModelIndex &index, int role) const
case IsIssuedRole: { case IsIssuedRole: {
return isIssued; return isIssued;
} }
case IsWorkerExpiredRole: {
return issuedConfigInfo.lastDownloaded < issuedConfigInfo.workerLastUpdated;
}
} }
return QVariant(); return QVariant();
@ -114,5 +117,6 @@ QHash<int, QByteArray> ApiCountryModel::roleNames() const
roles[CountryCodeRole] = "countryCode"; roles[CountryCodeRole] = "countryCode";
roles[CountryImageCodeRole] = "countryImageCode"; roles[CountryImageCodeRole] = "countryImageCode";
roles[IsIssuedRole] = "isIssued"; roles[IsIssuedRole] = "isIssued";
roles[IsWorkerExpiredRole] = "isWorkerExpired";
return roles; return roles;
} }

View file

@ -14,7 +14,8 @@ public:
CountryNameRole = Qt::UserRole + 1, CountryNameRole = Qt::UserRole + 1,
CountryCodeRole, CountryCodeRole,
CountryImageCodeRole, CountryImageCodeRole,
IsIssuedRole IsIssuedRole,
IsWorkerExpiredRole
}; };
explicit ApiCountryModel(QObject *parent = nullptr); explicit ApiCountryModel(QObject *parent = nullptr);

View file

@ -0,0 +1,90 @@
#include "apiDevicesModel.h"
#include <QJsonObject>
#include "core/api/apiDefs.h"
#include "logger.h"
namespace
{
Logger logger("ApiDevicesModel");
constexpr QLatin1String gatewayAccount("gateway_account");
}
ApiDevicesModel::ApiDevicesModel(std::shared_ptr<Settings> settings, QObject *parent) : m_settings(settings), QAbstractListModel(parent)
{
}
int ApiDevicesModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return m_issuedConfigs.size();
}
QVariant ApiDevicesModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
return QVariant();
IssuedConfigInfo issuedConfigInfo = m_issuedConfigs.at(index.row());
switch (role) {
case OsVersionRole: {
return issuedConfigInfo.osVersion;
}
case SupportTagRole: {
return issuedConfigInfo.installationUuid;
}
case CountryCodeRole: {
return issuedConfigInfo.countryCode;
}
case LastUpdateRole: {
return QDateTime::fromString(issuedConfigInfo.lastDownloaded, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
}
case IsCurrentDeviceRole: {
return issuedConfigInfo.installationUuid == m_settings->getInstallationUuid(false);
}
}
return QVariant();
}
void ApiDevicesModel::updateModel(const QJsonArray &issuedConfigs)
{
beginResetModel();
m_issuedConfigs.clear();
for (int i = 0; i < issuedConfigs.size(); i++) {
IssuedConfigInfo issuedConfigInfo;
QJsonObject issuedConfigObject = issuedConfigs.at(i).toObject();
if (issuedConfigObject.value(apiDefs::key::sourceType).toString() != gatewayAccount) {
continue;
}
issuedConfigInfo.installationUuid = issuedConfigObject.value(apiDefs::key::installationUuid).toString();
issuedConfigInfo.workerLastUpdated = issuedConfigObject.value(apiDefs::key::workerLastUpdated).toString();
issuedConfigInfo.lastDownloaded = issuedConfigObject.value(apiDefs::key::lastDownloaded).toString();
issuedConfigInfo.sourceType = issuedConfigObject.value(apiDefs::key::sourceType).toString();
issuedConfigInfo.osVersion = issuedConfigObject.value(apiDefs::key::osVersion).toString();
issuedConfigInfo.countryName = issuedConfigObject.value(apiDefs::key::serverCountryName).toString();
issuedConfigInfo.countryCode = issuedConfigObject.value(apiDefs::key::serverCountryCode).toString();
m_issuedConfigs.push_back(issuedConfigInfo);
}
endResetModel();
}
QHash<int, QByteArray> ApiDevicesModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[OsVersionRole] = "osVersion";
roles[SupportTagRole] = "supportTag";
roles[CountryCodeRole] = "countryCode";
roles[LastUpdateRole] = "lastUpdate";
roles[IsCurrentDeviceRole] = "isCurrentDevice";
return roles;
}

View file

@ -0,0 +1,52 @@
#ifndef APIDEVICESMODEL_H
#define APIDEVICESMODEL_H
#include <QAbstractListModel>
#include <QJsonArray>
#include <QVector>
#include "settings.h"
class ApiDevicesModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
OsVersionRole = Qt::UserRole + 1,
SupportTagRole,
CountryCodeRole,
LastUpdateRole,
IsCurrentDeviceRole
};
explicit ApiDevicesModel(std::shared_ptr<Settings> settings, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
public slots:
void updateModel(const QJsonArray &issuedConfigs);
protected:
QHash<int, QByteArray> roleNames() const override;
private:
struct IssuedConfigInfo
{
QString installationUuid;
QString workerLastUpdated;
QString lastDownloaded;
QString sourceType;
QString osVersion;
QString countryName;
QString countryCode;
};
QVector<IssuedConfigInfo> m_issuedConfigs;
std::shared_ptr<Settings> m_settings;
};
#endif // APIDEVICESMODEL_H

View file

@ -27,5 +27,6 @@ QtObject {
readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8) readonly property color mistyGray: Qt.rgba(215/255, 216/255, 219/255, 0.8)
readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65) readonly property color cloudyGray: Qt.rgba(215/255, 216/255, 219/255, 0.65)
readonly property color pearlGray: '#EAEAEC' readonly property color pearlGray: '#EAEAEC'
readonly property color translucentRichBrown: Qt.rgba(99/255, 51/255, 3/255, 0.26)
} }
} }

View file

@ -0,0 +1,100 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtCore
import SortFilterProxyModel 0.2
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
ListViewType {
id: listView
anchors.fill: parent
anchors.topMargin: 20
anchors.bottomMargin: 24
model: ApiDevicesModel
header: ColumnLayout {
width: listView.width
BackButtonType {
id: backButton
}
HeaderType {
id: header
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: qsTr("Connected devices")
descriptionText: qsTr("To manage connected devices")
}
WarningType {
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.fillWidth: true
textString: qsTr("You can find the identifier on the Support tab or, for older versions of the app, "
+ "by tapping '+' and then the three dots at the top of the page.")
iconPath: "qrc:/images/controls/alert-circle.svg"
}
}
delegate: ColumnLayout {
width: listView.width
LabelWithButtonType {
Layout.fillWidth: true
Layout.topMargin: 6
text: osVersion + (isCurrentDevice ? qsTr(" (current device)") : "")
descriptionText: qsTr("Support tag: ") + "\n" + supportTag + "\n" + qsTr("Last updated: ") + lastUpdate
rightImageSource: "qrc:/images/controls/trash.svg"
clickedFunction: function() {
var headerText = qsTr("Deactivate the subscription on selected device")
var descriptionText = qsTr("The next time the “Connect” button is pressed, the device will be activated again")
var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() {
Qt.callLater(deactivateExternalDevice, supportTag, countryCode)
}
var noButtonFunction = function() {
}
showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
}
}
DividerType {}
}
}
function deactivateExternalDevice(supportTag, countryCode) {
PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateExternalDevice(supportTag, countryCode)) {
ApiSettingsController.getAccountInfo(true)
}
PageController.showBusyIndicator(false)
}
}

View file

@ -107,7 +107,6 @@ PageType {
width: listView.width width: listView.width
LabelWithButtonType { LabelWithButtonType {
id: telegramButton
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 6 Layout.topMargin: 6

View file

@ -58,6 +58,9 @@ PageType {
Layout.topMargin: 6 Layout.topMargin: 6
text: countryName text: countryName
descriptionText: isWorkerExpired ? qsTr("The configuration needs to be reissued") : ""
descriptionColor: AmneziaStyle.color.vibrantRed
leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg" leftImageSource: "qrc:/countriesFlags/images/flagKit/" + countryImageCode + ".svg"
rightImageSource: isIssued ? "qrc:/images/controls/more-vertical.svg" : "qrc:/images/controls/download.svg" rightImageSource: isIssued ? "qrc:/images/controls/more-vertical.svg" : "qrc:/images/controls/download.svg"

View file

@ -28,7 +28,7 @@ PageType {
readonly property string title: qsTr("Subscription status") readonly property string title: qsTr("Subscription status")
readonly property string contentKey: "subscriptionStatus" readonly property string contentKey: "subscriptionStatus"
readonly property string objectImageSource: "qrc:/images/controls/map-pin.svg" readonly property string objectImageSource: "qrc:/images/controls/info.svg"
} }
QtObject { QtObject {
@ -44,7 +44,7 @@ PageType {
readonly property string title: qsTr("Connected devices") readonly property string title: qsTr("Connected devices")
readonly property string contentKey: "connectedDevices" readonly property string contentKey: "connectedDevices"
readonly property string objectImageSource: "qrc:/images/controls/gauge.svg" readonly property string objectImageSource: "qrc:/images/controls/monitor.svg"
} }
property var processedServer property var processedServer
@ -158,11 +158,28 @@ PageType {
readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible") readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible")
WarningType {
id: warning
Layout.topMargin: 32
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.fillWidth: true
backGroundColor: AmneziaStyle.color.translucentRichBrown
textString: qsTr("Configurations have been updated for some countries. Download and install the updated configuration files")
iconPath: "qrc:/images/controls/alert-circle.svg"
visible: ApiAccountInfoModel.data("hasExpiredWorker")
}
LabelWithButtonType { LabelWithButtonType {
id: vpnKey id: vpnKey
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 32 Layout.topMargin: warning.visible ? 16 : 32
visible: false //footer.isVisibleForAmneziaFree visible: false //footer.isVisibleForAmneziaFree
@ -192,7 +209,7 @@ PageType {
LabelWithButtonType { LabelWithButtonType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 32 Layout.topMargin: warning.visible ? 16 : 32
visible: footer.isVisibleForAmneziaFree visible: footer.isVisibleForAmneziaFree
@ -211,6 +228,26 @@ PageType {
visible: footer.isVisibleForAmneziaFree visible: footer.isVisibleForAmneziaFree
} }
LabelWithButtonType {
Layout.fillWidth: true
visible: footer.isVisibleForAmneziaFree
text: qsTr("Connected devices")
descriptionText: qsTr("To manage connected devices")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
ApiSettingsController.updateApiDevicesModel()
PageController.goToPage(PageEnum.PageSettingsApiDevices)
}
}
DividerType {
visible: footer.isVisibleForAmneziaFree
}
LabelWithButtonType { LabelWithButtonType {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: footer.isVisibleForAmneziaFree ? 0 : 32 Layout.topMargin: footer.isVisibleForAmneziaFree ? 0 : 32
@ -292,12 +329,13 @@ PageType {
clickedFunc: function() { clickedFunc: function() {
var headerText = qsTr("Deactivate the subscription on this device?") var headerText = qsTr("Deactivate the subscription on this device?")
var descriptionText = qsTr("The next time the “Connect” button is pressed, the device will be activated again")
var yesButtonText = qsTr("Continue") var yesButtonText = qsTr("Continue")
var noButtonText = qsTr("Cancel") var noButtonText = qsTr("Cancel")
var yesButtonFunction = function() { var yesButtonFunction = function() {
if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
PageController.showNotificationMessage(qsTr("The next time the “Connect” button is pressed, the device will be activated again")) PageController.showNotificationMessage(qsTr("Cannot deactivate subscription during active connection"))
} else { } else {
PageController.showBusyIndicator(true) PageController.showBusyIndicator(true)
if (ApiConfigsController.deactivateDevice()) { if (ApiConfigsController.deactivateDevice()) {
@ -309,7 +347,7 @@ PageType {
var noButtonFunction = function() { var noButtonFunction = function() {
} }
showQuestionDrawer(headerText, "", yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction)
} }
} }

View file

@ -16,92 +16,110 @@ import "../Components"
PageType { PageType {
id: root id: root
ColumnLayout { QtObject {
id: backButtonLayout id: telegram
anchors.top: parent.top readonly property string title: qsTr("Telegram")
anchors.left: parent.left readonly property string description: "@" + ApiAccountInfoModel.getTelegramBotLink()
anchors.right: parent.right readonly property string link: "https://t.me/" + ApiAccountInfoModel.getTelegramBotLink()
}
QtObject {
id: techSupport
readonly property string title: qsTr("For technical support")
readonly property string description: qsTr("support@amnezia.org")
readonly property string link: "mailto:support@amnezia.org"
}
QtObject {
id: paymentSupport
readonly property string title: qsTr("For payment issues")
readonly property string description: qsTr("help@vpnpay.io")
readonly property string link: "mailto:help@vpnpay.io"
}
QtObject {
id: site
readonly property string title: qsTr("Site")
readonly property string description: qsTr("amnezia.org")
readonly property string link: LanguageModel.getCurrentSiteUrl()
}
property list<QtObject> supportModel: [
telegram,
techSupport,
paymentSupport,
site
]
ListViewType {
id: listView
anchors.fill: parent
anchors.topMargin: 20 anchors.topMargin: 20
anchors.bottomMargin: 24
BackButtonType { model: supportModel
id: backButton
}
header: ColumnLayout {
width: listView.width
HeaderType { BackButtonType {
id: header id: backButton
}
Layout.fillWidth: true HeaderType {
Layout.rightMargin: 16 id: header
Layout.leftMargin: 16
headerText: qsTr("Support") Layout.fillWidth: true
descriptionText: qsTr("Our technical support specialists are ready to help you at any time") Layout.rightMargin: 16
} Layout.leftMargin: 16
LabelWithButtonType { headerText: qsTr("Support")
Layout.fillWidth: true descriptionText: qsTr("Our technical support specialists are ready to help you at any time")
readonly property string telegramBotLink: ApiAccountInfoModel.getTelegramBotLink()
text: qsTr("Telegram")
descriptionText: "@" + telegramBotLink
rightImageSource: "qrc:/images/controls/external-link.svg"
clickedFunction: function() {
Qt.openUrlExternally("https://t.me/" + telegramBotLink)
} }
} }
DividerType {} delegate: ColumnLayout {
width: listView.width
LabelWithButtonType { LabelWithButtonType {
Layout.fillWidth: true Layout.fillWidth: true
text: title
text: qsTr("Mail") descriptionText: description
descriptionText: qsTr("support@amnezia.org") rightImageSource: "qrc:/images/controls/external-link.svg"
rightImageSource: "qrc:/images/controls/external-link.svg" clickedFunction: function() {
Qt.openUrlExternally(link)
clickedFunction: function() { }
Qt.openUrlExternally(qsTr("mailto:support@amnezia.org"))
} }
DividerType {}
} }
DividerType {}
LabelWithButtonType { footer: ColumnLayout {
Layout.fillWidth: true width: listView.width
text: qsTr("Site") LabelWithButtonType {
descriptionText: qsTr("amnezia.org") id: supportUuid
rightImageSource: "qrc:/images/controls/external-link.svg" Layout.fillWidth: true
clickedFunction: function() { text: qsTr("Support tag")
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl()) descriptionText: SettingsController.getInstallationUuid()
}
}
DividerType {} descriptionOnTop: true
LabelWithButtonType { rightImageSource: "qrc:/images/controls/copy.svg"
id: supportUuid rightImageColor: AmneziaStyle.color.paleGray
Layout.fillWidth: true
text: qsTr("Support tag") clickedFunction: function() {
descriptionText: SettingsController.getInstallationUuid() GC.copyToClipBoard(descriptionText)
PageController.showNotificationMessage(qsTr("Copied"))
descriptionOnTop: true if (!GC.isMobile()) {
this.rightButton.forceActiveFocus()
rightImageSource: "qrc:/images/controls/copy.svg" }
rightImageColor: AmneziaStyle.color.paleGray
clickedFunction: function() {
GC.copyToClipBoard(descriptionText)
PageController.showNotificationMessage(qsTr("Copied"))
if (!GC.isMobile()) {
this.rightButton.forceActiveFocus()
} }
} }
} }