feature: added pages for subscription settings feature

This commit is contained in:
vladimir.kuznetsov 2025-02-06 15:26:47 +07:00
parent 3f55f6a629
commit b183a3b232
27 changed files with 856 additions and 287 deletions

View file

@ -0,0 +1,57 @@
#include "apiSettingsController.h"
#include "core/controllers/gatewayController.h"
namespace
{
namespace configKey
{
constexpr char userCountryCode[] = "user_country_code";
constexpr char serverCountryCode[] = "server_country_code";
constexpr char serviceType[] = "service_type";
constexpr char serviceInfo[] = "service_info";
constexpr char apiConfig[] = "api_config";
constexpr char authData[] = "auth_data";
}
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
}
ApiSettingsController::ApiSettingsController(const QSharedPointer<ServersModel> &serversModel,
const QSharedPointer<ApiAccountInfoModel> &apiAccountInfoModel, const std::shared_ptr<Settings> &settings,
QObject *parent)
: QObject(parent), m_serversModel(serversModel), m_apiAccountInfoModel(apiAccountInfoModel), m_settings(settings)
{
}
ApiSettingsController::~ApiSettingsController()
{
}
bool ApiSettingsController::getAccountInfo()
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), requestTimeoutMsecs);
auto processedIndex = m_serversModel->getProcessedServerIndex();
auto serverConfig = m_serversModel->getServerConfig(processedIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto authData = serverConfig.value(configKey::authData).toObject();
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();
apiPayload[configKey::authData] = authData;
QByteArray responseBody;
ErrorCode errorCode = gatewayController.post(QString("%1v1/account_info"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
// emit errorOccured(errorCode);
return false;
}
QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object();
m_apiAccountInfoModel->updateModel(accountInfo, serverConfig);
return true;
}

View file

@ -0,0 +1,27 @@
#ifndef APISETTINGSCONTROLLER_H
#define APISETTINGSCONTROLLER_H
#include <QObject>
#include "ui/models/api/apiAccountInfoModel.h"
#include "ui/models/servers_model.h"
class ApiSettingsController : public QObject
{
Q_OBJECT
public:
ApiSettingsController(const QSharedPointer<ServersModel> &serversModel, const QSharedPointer<ApiAccountInfoModel> &apiAccountInfoModel,
const std::shared_ptr<Settings> &settings, QObject *parent = nullptr);
~ApiSettingsController();
public slots:
bool getAccountInfo();
private:
QSharedPointer<ServersModel> m_serversModel;
QSharedPointer<ApiAccountInfoModel> m_apiAccountInfoModel;
std::shared_ptr<Settings> m_settings;
};
#endif // APISETTINGSCONTROLLER_H

View file

@ -0,0 +1,2 @@
#include "importController.h"

View file

@ -0,0 +1,24 @@
#ifndef IMPORTCONTROLLER_H
#define IMPORTCONTROLLER_H
#include <QObject>
#include "ui/models/api/apiAccountInfoModel.h"
#include "ui/models/servers_model.h"
// namespace api
// {
// class ImportController : public QObject
// {
// Q_OBJECT
// public:
// ImportController(const QSharedPointer<ServersModel> &serversModel, QSharedPointer<AccountInfoModel> &accountInfoModel);
// ~ImportController();
// private:
// QSharedPointer<ServersModel> m_serversModel;
// QSharedPointer<AccountInfoModel> m_accountInfoModel;
// };
// }
#endif // IMPORTCONTROLLER_H

View file

@ -832,6 +832,7 @@ bool InstallController::updateServiceFromApi(const int serverIndex, const QStrin
auto authData = serverConfig.value(configKey::authData).toObject();
QJsonObject newServerConfig;
ErrorCode errorCode = apiController.getConfigForService(
m_settings->getInstallationUuid(true), apiConfig.value(configKey::userCountryCode).toString(),
apiConfig.value(configKey::serviceType).toString(), apiConfig.value(configKey::serviceProtocol).toString(), newCountryCode,

View file

@ -33,6 +33,8 @@ namespace PageLoader
PageSettingsAppSplitTunneling,
PageSettingsApiServerInfo,
PageSettingsApiAvailableCountries,
PageSettingsApiSupport,
PageSettingsApiInstructions,
PageServiceSftpSettings,
PageServiceTorWebsiteSettings,
@ -55,7 +57,7 @@ namespace PageLoader
PageProtocolOpenVpnSettings,
PageProtocolShadowSocksSettings,
PageProtocolCloakSettings,
PageProtocolXraySettings,
PageProtocolXraySettings,
PageProtocolWireGuardSettings,
PageProtocolAwgSettings,
PageProtocolIKev2Settings,
@ -106,7 +108,7 @@ public slots:
int incrementDrawerDepth();
int decrementDrawerDepth();
private slots:
private slots:
void onShowErrorMessage(amnezia::ErrorCode errorCode);
signals:

View file

@ -0,0 +1,105 @@
#include "apiAccountInfoModel.h"
#include <QJsonObject>
#include "core/api/apiUtils.h"
#include "logger.h"
namespace
{
Logger logger("AccountInfoModel");
namespace configKey
{
constexpr char availableCountries[] = "available_countries";
constexpr char serverCountryCode[] = "server_country_code";
constexpr char serverCountryName[] = "server_country_name";
constexpr char lastUpdated[] = "last_updated";
constexpr char activeDeviceCount[] = "active_device_count";
constexpr char maxDeviceCount[] = "max_device_count";
constexpr char subscriptionEndDate[] = "subscription_end_date";
}
}
ApiAccountInfoModel::ApiAccountInfoModel(QObject *parent) : QAbstractListModel(parent)
{
}
int ApiAccountInfoModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 1;
}
QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= static_cast<int>(rowCount()))
return QVariant();
switch (role) {
case SubscriptionStatusRole: {
return ApiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("Inactive") : tr("Active");
}
case EndDateRole: {
return QDateTime::fromString(m_accountInfoData.subscriptionEndDate, Qt::ISODate).toLocalTime().toString("d MMM yyyy");
}
case ConnectedDevicesRole: {
return tr("%1 out of %2").arg(m_accountInfoData.activeDeviceCount).arg(m_accountInfoData.maxDeviceCount);
}
// case ServiceDescriptionRole: {
// return apiServiceData.serviceInfo.name;
// }
}
return QVariant();
}
void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig)
{
beginResetModel();
AccountInfoData accountInfoData;
auto availableCountries = accountInfoObject.value(configKey::availableCountries).toArray();
for (const auto &country : availableCountries) {
auto countryObject = country.toObject();
CountryInfo countryInfo;
countryInfo.serverCountryCode = countryObject.value(configKey::serverCountryCode).toString();
countryInfo.serverCountryName = countryObject.value(configKey::serverCountryName).toString();
countryInfo.lastUpdated = countryObject.value(configKey::lastUpdated).toString();
accountInfoData.AvailableCountries.push_back(countryInfo);
}
accountInfoData.activeDeviceCount = accountInfoObject.value(configKey::activeDeviceCount).toInt();
accountInfoData.maxDeviceCount = accountInfoObject.value(configKey::maxDeviceCount).toInt();
accountInfoData.subscriptionEndDate = accountInfoObject.value(configKey::subscriptionEndDate).toString();
m_accountInfoData = accountInfoData;
endResetModel();
}
QVariant ApiAccountInfoModel::data(const QString &roleString)
{
QModelIndex modelIndex = index(0);
auto roles = roleNames();
for (auto it = roles.begin(); it != roles.end(); it++) {
if (QString(it.value()) == roleString) {
return data(modelIndex, it.key());
}
}
return {};
}
QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[SubscriptionStatusRole] = "subscriptionStatus";
roles[EndDateRole] = "endDate";
roles[ConnectedDevicesRole] = "connectedDevices";
roles[ServiceDescriptionRole] = "serviceDescription";
return roles;
}

View file

@ -0,0 +1,55 @@
#ifndef APIACCOUNTINFOMODEL_H
#define APIACCOUNTINFOMODEL_H
#include <QAbstractListModel>
#include <QJsonArray>
#include <QJsonObject>
class ApiAccountInfoModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
SubscriptionStatusRole = Qt::UserRole + 1,
ConnectedDevicesRole,
ServiceDescriptionRole,
EndDateRole
};
explicit ApiAccountInfoModel(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 QJsonObject &accountInfoObject, const QJsonObject &serverConfig);
QVariant data(const QString &roleString);
protected:
QHash<int, QByteArray> roleNames() const override;
private:
struct CountryInfo
{
QString serverCountryCode;
QString serverCountryName;
QString lastUpdated;
};
struct AccountInfoData
{
QString subscriptionEndDate;
int activeDeviceCount;
int maxDeviceCount;
QString vpnKey;
QVector<CountryInfo> AvailableCountries;
};
AccountInfoData m_accountInfoData;
};
#endif // APIACCOUNTINFOMODEL_H

View file

@ -112,9 +112,10 @@ ListView {
ServersModel.processedIndex = index
if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) {
ApiSettingsController.getAccountInfo()
if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) {
PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries)
} else {
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
}

View file

@ -0,0 +1,38 @@
import QtQuick
import QtQuick.Controls
ListView {
id: root
property bool isFocusable: true
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
Keys.onLeftPressed: {
FocusController.nextKeyLeftItem()
}
Keys.onRightPressed: {
FocusController.nextKeyRightItem()
}
ScrollBar.vertical: ScrollBarType {}
clip: true
reuseItems: true
snapMode: ListView.SnapToItem
}

View file

@ -299,9 +299,10 @@ PageType {
ServersModel.processedIndex = ServersModel.defaultIndex
if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) {
ApiSettingsController.getAccountInfo()
if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) {
PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries)
} else {
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
}

View file

@ -47,8 +47,7 @@ PageType {
readonly property string description: qsTr("For reviews and bug reports")
readonly property string imageSource: "qrc:/images/controls/mail.svg"
readonly property var handler: function() {
GC.copyToClipBoard(title)
PageController.showNotificationMessage(qsTr("Copied"))
Qt.openUrlExternally(qsTr("mailto:support@amnezia.org"))
}
}

View file

@ -0,0 +1,87 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
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
QtObject {
id: windows
readonly property string title: qsTr("Windows")
readonly property string imageSource: "qrc:/images/controls/external-link.svg"
readonly property var handler: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl())
}
}
QtObject {
id: linux
readonly property string title: qsTr("Windows")
readonly property string imageSource: "qrc:/images/controls/external-link.svg"
readonly property var handler: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl())
}
}
property list<QtObject> instructionsModel: [
windows,
linux
]
ListViewType {
id: listView
anchors.fill: parent
model: instructionsModel
header: ColumnLayout {
width: listView.width
BackButtonType {
id: backButton
}
HeaderType {
id: header
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: "Support"
descriptionText: qsTr("Our technical support specialists are ready to help you at any time")
}
}
delegate: ColumnLayout {
width: listView.width
LabelWithButtonType {
id: telegramButton
Layout.fillWidth: true
Layout.topMargin: 6
text: title
leftImageSource: imageSource
clickedFunction: handler
}
DividerType {}
}
}
}

View file

@ -18,28 +18,19 @@ PageType {
id: root
property list<QtObject> labelsModel: [
regionObject,
priceObject,
statusObject,
endDateObject,
speedObject
deviceCountObject
]
QtObject {
id: regionObject
id: statusObject
readonly property string title: qsTr("For the region")
readonly property string contentKey: "region"
readonly property string title: qsTr("Subscription status")
readonly property string contentKey: "subscriptionStatus"
readonly property string objectImageSource: "qrc:/images/controls/map-pin.svg"
}
QtObject {
id: priceObject
readonly property string title: qsTr("Price")
readonly property string contentKey: "price"
readonly property string objectImageSource: "qrc:/images/controls/tag.svg"
}
QtObject {
id: endDateObject
@ -49,10 +40,10 @@ PageType {
}
QtObject {
id: speedObject
id: deviceCountObject
readonly property string title: qsTr("Speed")
readonly property string contentKey: "speed"
readonly property string title: qsTr("Connected devices")
readonly property string contentKey: "connectedDevices"
readonly property string objectImageSource: "qrc:/images/controls/gauge.svg"
}
@ -83,43 +74,11 @@ PageType {
}
}
ListView {
ListViewType {
id: listView
property bool isFocusable: true
anchors.fill: parent
Keys.onTabPressed: {
FocusController.nextKeyTabItem()
}
Keys.onBacktabPressed: {
FocusController.previousKeyTabItem()
}
Keys.onUpPressed: {
FocusController.nextKeyUpItem()
}
Keys.onDownPressed: {
FocusController.nextKeyDownItem()
}
Keys.onLeftPressed: {
FocusController.nextKeyLeftItem()
}
Keys.onRightPressed: {
FocusController.nextKeyRightItem()
}
ScrollBar.vertical: ScrollBarType {}
clip: true
reuseItems: true
snapMode: ListView.SnapToItem
model: labelsModel
header: ColumnLayout {
@ -175,7 +134,7 @@ PageType {
imageSource: objectImageSource
leftText: title
rightText: ApiServicesModel.getSelectedServiceData(contentKey)
rightText: ApiAccountInfoModel.data(contentKey)
visible: rightText !== ""
}
@ -185,53 +144,61 @@ PageType {
width: listView.width
spacing: 0
ParagraphTextType {
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
textFormat: Text.RichText
text: {
var text = ApiServicesModel.getSelectedServiceData("features")
if (text === undefined) {
return ""
}
return text.replace("%1", LanguageModel.getCurrentSiteUrl())
}
visible: text !== ""
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
LabelWithButtonType {
id: supportUuid
Layout.fillWidth: true
Layout.topMargin: 32
text: qsTr("Support tag")
descriptionText: SettingsController.getInstallationUuid()
visible: false
descriptionOnTop: true
rightImageSource: "qrc:/images/controls/copy.svg"
rightImageColor: AmneziaStyle.color.paleGray
text: qsTr("Subscription key")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
GC.copyToClipBoard(descriptionText)
PageController.showNotificationMessage(qsTr("Copied"))
if (!GC.isMobile()) {
this.rightButton.forceActiveFocus()
}
}
}
DividerType {}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Configuration files")
descriptionText: qsTr("To connect the router")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
}
}
DividerType {}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Support")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsApiSupport)
}
}
DividerType {}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("How to connect on another devicey")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
PageController.goToPage(PageEnum.PageSettingsApiInstructions)
}
}
DividerType {}
BasicButtonType {
id: resetButton
Layout.alignment: Qt.AlignHCenter

View file

@ -0,0 +1,107 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
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
ColumnLayout {
id: backButtonLayout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20
BackButtonType {
id: backButton
}
HeaderType {
id: header
Layout.fillWidth: true
Layout.rightMargin: 16
Layout.leftMargin: 16
headerText: "Support"
descriptionText: qsTr("Our technical support specialists are ready to help you at any time")
}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Telegram")
descriptionText: qsTr("@amnezia_premium_support_bot")
rightImageSource: "qrc:/images/controls/external-link.svg"
clickedFunction: function() {
Qt.openUrlExternally(qsTr("https://t.me/amnezia_premium_support_bot"))
}
}
DividerType {}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Mail")
descriptionText: qsTr("support@amnezia.org")
rightImageSource: "qrc:/images/controls/external-link.svg"
clickedFunction: function() {
Qt.openUrlExternally(qsTr("mailto:support@amnezia.org"))
}
}
DividerType {}
LabelWithButtonType {
Layout.fillWidth: true
text: qsTr("Site")
descriptionText: qsTr("amnezia.org")
rightImageSource: "qrc:/images/controls/external-link.svg"
clickedFunction: function() {
Qt.openUrlExternally(LanguageModel.getCurrentSiteUrl())
}
}
DividerType {}
LabelWithButtonType {
id: supportUuid
Layout.fillWidth: true
text: qsTr("Support tag")
descriptionText: SettingsController.getInstallationUuid()
descriptionOnTop: true
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()
}
}
}
}
}

View file

@ -36,16 +36,6 @@ PageType {
PageController.showErrorMessage(message)
}
function onRemoveProcessedServerFinished(finishedMessage) {
if (!ServersModel.getServersCount()) {
PageController.goToPageHome()
} else {
PageController.goToStartPage()
PageController.goToPage(PageEnum.PageSettingsServersList)
}
PageController.showNotificationMessage(finishedMessage)
}
function onRebootProcessedServerFinished(finishedMessage) {
PageController.showNotificationMessage(finishedMessage)
}

View file

@ -95,12 +95,9 @@ PageType {
ServersModel.processedIndex = index
if (ServersModel.getProcessedServerData("isServerFromGatewayApi")) {
if (ServersModel.getProcessedServerData("isCountrySelectionAvailable")) {
PageController.goToPage(PageEnum.PageSettingsApiAvailableCountries)
ApiSettingsController.getAccountInfo()
} else {
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
}
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
} else {
PageController.goToPage(PageEnum.PageSettingsServerInfo)
}

View file

@ -154,6 +154,16 @@ PageType {
PageController.goToPageHome()
PageController.showNotificationMessage(message)
}
function onRemoveProcessedServerFinished(finishedMessage) {
if (!ServersModel.getServersCount()) {
PageController.goToPageHome()
} else {
PageController.goToStartPage()
PageController.goToPage(PageEnum.PageSettingsServersList)
}
PageController.showNotificationMessage(finishedMessage)
}
}
Connections {