feat: add renewal for external-premium (#2485)

* feat: add renewal for external-premium

* chore: bump version

* chore: send subscription status for renewal link request
This commit is contained in:
vkamn
2026-04-17 15:01:24 +08:00
committed by GitHub
parent 4c18ceaa50
commit cebfcc846e
10 changed files with 41 additions and 22 deletions

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.15.0)
set(AMNEZIAVPN_VERSION 4.8.15.1)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN"

View File

@@ -10,7 +10,6 @@ namespace apiDefs
AmneziaFreeV3,
AmneziaPremiumV1,
AmneziaPremiumV2,
AmneziaTrialV2,
SelfHosted,
ExternalPremium,
ExternalTrial
@@ -57,6 +56,7 @@ namespace apiDefs
constexpr QLatin1String maxDeviceCount("max_device_count");
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
constexpr QLatin1String subscriptionExpiredByServer("subscription_expired_by_server");
constexpr QLatin1String subscriptionStatus("subscription_status");
constexpr QLatin1String subscription("subscription");
constexpr QLatin1String endDate("end_date");
constexpr QLatin1String issuedConfigs("issued_configs");
@@ -83,6 +83,7 @@ namespace apiDefs
constexpr QLatin1String serviceInfo("service_info");
constexpr QLatin1String isAdVisible("is_ad_visible");
constexpr QLatin1String isRenewalAvailable("is_renewal_available");
constexpr QLatin1String adHeader("ad_header");
constexpr QLatin1String adDescription("ad_description");
constexpr QLatin1String adEndpoint("ad_endpoint");

View File

@@ -101,7 +101,6 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
};
case apiDefs::ConfigSource::AmneziaGateway: {
constexpr QLatin1String servicePremium("amnezia-premium");
constexpr QLatin1String serviceTrial("amnezia-trial");
constexpr QLatin1String serviceFree("amnezia-free");
constexpr QLatin1String serviceExternalPremium("external-premium");
constexpr QLatin1String serviceExternalTrial("external-trial");
@@ -111,8 +110,6 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
if (serviceType == servicePremium) {
return apiDefs::ConfigType::AmneziaPremiumV2;
} else if (serviceType == serviceTrial) {
return apiDefs::ConfigType::AmneziaTrialV2;
} else if (serviceType == serviceFree) {
return apiDefs::ConfigType::AmneziaFreeV3;
} else if (serviceType == serviceExternalPremium) {
@@ -198,8 +195,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
{
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
apiDefs::ConfigType::AmneziaTrialV2, apiDefs::ConfigType::ExternalPremium,
apiDefs::ConfigType::ExternalTrial };
apiDefs::ConfigType::ExternalPremium, apiDefs::ConfigType::ExternalTrial };
return premiumTypes.contains(getConfigType(serverConfigObject));
}
@@ -244,8 +240,8 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
{
auto configType = apiUtils::getConfigType(serverConfigObject);
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::AmneziaTrialV2
&& configType != apiDefs::ConfigType::ExternalPremium && configType != apiDefs::ConfigType::ExternalTrial) {
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::ExternalPremium
&& configType != apiDefs::ConfigType::ExternalTrial) {
return {};
}

View File

@@ -13,7 +13,7 @@ namespace apiUtils
bool isSubscriptionExpired(const QString &subscriptionEndDate);
bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 10);
bool isSubscriptionExpiringSoon(const QString &subscriptionEndDate, int withinDays = 30);
bool isPremiumServer(const QJsonObject &serverConfigObject);

View File

@@ -23,6 +23,19 @@ namespace
}
const int requestTimeoutMsecs = 12 * 1000; // 12 secs
QString getSubscriptionStatusForRenewal(const QSharedPointer<ApiAccountInfoModel> &accountInfoModel)
{
if (!accountInfoModel.isNull() && accountInfoModel->data(QStringLiteral("isSubscriptionExpired")).toBool()) {
return QStringLiteral("expired");
}
if (!accountInfoModel.isNull() && accountInfoModel->data(QStringLiteral("isSubscriptionExpiringSoon")).toBool()) {
return QStringLiteral("expire_soon");
}
return QStringLiteral("active");
}
}
ApiSettingsController::ApiSettingsController(const QSharedPointer<ServersModel> &serversModel,
@@ -105,6 +118,7 @@ void ApiSettingsController::getRenewalLink()
apiPayload[configKey::authData] = authData;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
apiPayload[apiDefs::key::subscriptionStatus] = getSubscriptionStatusForRenewal(m_apiAccountInfoModel);
auto future = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
future.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {

View File

@@ -54,14 +54,11 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
}
case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
}
case IsSubscriptionRenewalAvailableRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
return m_accountInfoData.isRenewalAvailable;
}
case HasExpiredWorkerRole: {
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
@@ -133,6 +130,7 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
accountInfoData.isInAppPurchase = apiConfig.value(apiDefs::key::isInAppPurchase).toBool(false);
accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString();
accountInfoData.isRenewalAvailable = accountInfoObject.value(apiDefs::key::isRenewalAvailable).toBool(false);
for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) {
accountInfoData.supportedProtocols.push_back(protocol.toString());

View File

@@ -61,6 +61,7 @@ private:
QString subscriptionDescription;
bool isInAppPurchase = false;
bool isRenewalAvailable = false;
};
AccountInfoData m_accountInfoData;

View File

@@ -179,6 +179,9 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
case AdEndpointRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString();
}
case IsRenewalAvailableRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::isRenewalAvailable).toBool(false);
}
case IsSubscriptionExpiredRole: {
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) {
return false;
@@ -473,6 +476,7 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[AdHeaderRole] = "adHeader";
roles[AdDescriptionRole] = "adDescription";
roles[AdEndpointRole] = "adEndpoint";
roles[IsRenewalAvailableRole] = "isRenewalAvailable";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";

View File

@@ -51,6 +51,7 @@ public:
AdHeaderRole,
AdDescriptionRole,
AdEndpointRole,
IsRenewalAvailableRole,
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole,

View File

@@ -12,11 +12,10 @@ import "../Controls2/TextTypes"
DrawerType2 {
id: root
property bool isRenewalActionAvailable: false
property bool isRenewalAvailable: false
onOpened: {
isRenewalActionAvailable = ApiAccountInfoModel.data("isSubscriptionRenewalAvailable")
&& !ApiAccountInfoModel.data("isInAppPurchase")
isRenewalAvailable = ServersModel.getProcessedServerData("isRenewalAvailable") && !ApiAccountInfoModel.data("isInAppPurchase")
}
expandedStateContent: ColumnLayout {
@@ -44,13 +43,13 @@ DrawerType2 {
anchors.left: parent.left
anchors.right: parent.right
text: qsTr("Amnezia Premium subscription has expired")
text: ServersModel.getProcessedServerData("name") + qsTr(" subscription has expired")
horizontalAlignment: Text.AlignLeft
}
}
ParagraphTextType {
visible: root.isRenewalActionAvailable
visible: root.isRenewalAvailable
Layout.fillWidth: true
Layout.topMargin: 8
@@ -62,7 +61,7 @@ DrawerType2 {
}
BasicButtonType {
visible: root.isRenewalActionAvailable
visible: root.isRenewalAvailable
Layout.fillWidth: true
Layout.topMargin: 16
@@ -96,8 +95,13 @@ DrawerType2 {
text: qsTr("Support")
clickedFunc: function() {
root.closeTriggered()
PageController.goToPage(PageEnum.PageSettingsApiSupport)
PageController.showBusyIndicator(true)
let result = ApiSettingsController.getAccountInfo(false)
PageController.showBusyIndicator(false)
if (result) {
root.closeTriggered()
PageController.goToPage(PageEnum.PageSettingsApiSupport)
}
}
}
}