feat: add trial api support

This commit is contained in:
vkamn
2026-03-26 20:03:18 +08:00
parent 041219187b
commit bae2dd452b
10 changed files with 225 additions and 84 deletions
+1
View File
@@ -234,6 +234,7 @@
<file>ui/qml/Pages2/PageProtocolWireGuardClientSettings.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiFreeInfo.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiPremiumInfo.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiTrialEmail.qml</file>
<file>ui/qml/Pages2/PageSetupWizardApiServicesList.qml</file>
<file>ui/qml/Pages2/PageSetupWizardConfigSource.qml</file>
<file>ui/qml/Pages2/PageSetupWizardCredentials.qml</file>
@@ -12,6 +12,7 @@
#include <QDebug>
#include <QEventLoop>
#include <QSet>
#include <QVariantMap>
#include "platforms/ios/ios_controller.h"
@@ -459,7 +460,7 @@ bool ApiConfigsController::importService()
return importSerivceFromAppStore();
}
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
importServiceFromGateway();
importFreeFromGateway();
return true;
}
return false;
@@ -675,7 +676,7 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
return true;
}
bool ApiConfigsController::importServiceFromGateway()
bool ApiConfigsController::importFreeFromGateway()
{
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
@@ -727,6 +728,72 @@ bool ApiConfigsController::importServiceFromGateway()
}
}
bool ApiConfigsController::importTrialFromGateway(const QString &email)
{
const QString trimmedEmail = email.trimmed();
if (trimmedEmail.isEmpty()) {
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return false;
}
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
QString(APP_VERSION),
m_settings->getAppLanguage().name().split("_").first(),
m_settings->getInstallationUuid(true),
m_apiServicesModel->getCountryCode(),
"",
m_apiServicesModel->getSelectedServiceType(),
m_apiServicesModel->getSelectedServiceProtocol(),
QJsonObject() };
if (m_serversModel->isServerFromApiAlreadyExists(gatewayRequestData.userCountryCode, gatewayRequestData.serviceType,
gatewayRequestData.serviceProtocol)) {
emit errorOccurred(ErrorCode::ApiConfigAlreadyAdded);
return false;
}
ProtocolData protocolData = generateProtocolData(gatewayRequestData.serviceProtocol);
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
appendProtocolDataToApiPayload(gatewayRequestData.serviceProtocol, protocolData, apiPayload);
apiPayload.insert(apiDefs::key::email, trimmedEmail);
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/trial"), apiPayload, responseBody);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
QString key = responseObject.value(apiDefs::key::config).toString();
if (key.isEmpty()) {
qWarning().noquote() << "[Trial] trial response does not contain config field";
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return false;
}
key.replace(QStringLiteral("vpn://"), QString());
QByteArray configBytes = QByteArray::fromBase64(key.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
QByteArray uncompressed = qUncompress(configBytes);
if (!uncompressed.isEmpty()) {
configBytes = uncompressed;
}
if (configBytes.isEmpty()) {
qWarning().noquote() << "[Trial] trial response config payload is empty";
emit errorOccurred(ErrorCode::ApiConfigEmptyError);
return false;
}
QJsonObject configObject = QJsonDocument::fromJson(configBytes).object();
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
configObject.insert(config_key::crc, crc);
m_serversModel->addServer(configObject);
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
return true;
}
bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig)
{
@@ -18,9 +18,6 @@ public:
const QSharedPointer<ApiBenefitsModel> &benefitsModel, const std::shared_ptr<Settings> &settings,
QObject *parent = nullptr);
Q_PROPERTY(ApiSubscriptionPlansModel *subscriptionPlansModel READ subscriptionPlansModel CONSTANT)
Q_PROPERTY(ApiBenefitsModel *benefitsModel READ benefitsModel CONSTANT)
Q_PROPERTY(QList<QString> qrCodes READ getQrCodes NOTIFY vpnKeyExportReady)
Q_PROPERTY(int qrCodesCount READ getQrCodesCount NOTIFY vpnKeyExportReady)
Q_PROPERTY(QString vpnKey READ getVpnKey NOTIFY vpnKeyExportReady)
@@ -36,7 +33,8 @@ public slots:
bool importService();
bool importSerivceFromAppStore();
bool restoreSerivceFromAppStore();
bool importServiceFromGateway();
bool importFreeFromGateway();
bool importTrialFromGateway(const QString &email);
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false);
bool updateServiceFromTelegram(const int serverIndex);
@@ -45,9 +43,6 @@ public slots:
bool isConfigValid();
ApiSubscriptionPlansModel *subscriptionPlansModel() const { return m_subscriptionPlansModel.get(); }
ApiBenefitsModel *benefitsModel() const { return m_benefitsModel.get(); }
void setCurrentProtocol(const QString &protocolName);
bool isVlessProtocol();
+1
View File
@@ -77,6 +77,7 @@ namespace PageLoader
PageShareConnection,
PageSetupWizardApiPremiumInfo,
PageSetupWizardApiTrialEmail,
PageDevMenu
};
+2 -6
View File
@@ -45,7 +45,6 @@ namespace
{
constexpr char amneziaFree[] = "amnezia-free";
constexpr char amneziaPremium[] = "amnezia-premium";
constexpr char amneziaTrial[] = "amnezia-trial";
}
}
@@ -76,7 +75,7 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
case CardDescriptionRole: {
auto speed = apiServiceData.serviceInfo.speed;
if (serviceType == serviceType::amneziaPremium || serviceType == serviceType::amneziaTrial) {
if (serviceType == serviceType::amneziaPremium) {
return apiServiceData.serviceInfo.cardDescription.arg(speed);
} else if (serviceType == serviceType::amneziaFree) {
QString description = apiServiceData.serviceInfo.cardDescription;
@@ -132,11 +131,8 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
if (serviceType == serviceType::amneziaPremium) {
return 0;
}
if (serviceType == serviceType::amneziaTrial) {
return 1;
}
if (serviceType == serviceType::amneziaFree) {
return 2;
return 1;
}
return QVariant();
}
@@ -14,13 +14,13 @@ namespace configKey
constexpr char subtitle[] = "subtitle";
constexpr char recommended[] = "recommended";
constexpr char checkoutUrl[] = "checkout_url";
constexpr char serviceType[] = "service_type";
constexpr char isTrial[] = "is_trial";
constexpr char serviceProtocol[] = "service_protocol";
constexpr char primaryLeftCamel[] = "primaryLeft";
constexpr char primaryRightCamel[] = "primaryRight";
constexpr char checkoutUrlCamel[] = "checkoutUrl";
constexpr char serviceTypeCamel[] = "serviceType";
constexpr char isTrialCamel[] = "isTrial";
constexpr char serviceProtocolCamel[] = "serviceProtocol";
}
}
@@ -55,8 +55,8 @@ QVariant ApiSubscriptionPlansModel::data(const QModelIndex &index, int role) con
return plan.recommended;
case CheckoutUrlRole:
return plan.checkoutUrl;
case ServiceTypeRole:
return plan.serviceType;
case IsTrialRole:
return plan.isTrial;
case ServiceProtocolRole:
return plan.serviceProtocol;
default:
@@ -72,7 +72,7 @@ QHash<int, QByteArray> ApiSubscriptionPlansModel::roleNames() const
{ SubtitleRole, "subtitle" },
{ RecommendedRole, "recommended" },
{ CheckoutUrlRole, "checkoutUrl" },
{ ServiceTypeRole, "serviceType" },
{ IsTrialRole, "isTrial" },
{ ServiceProtocolRole, "serviceProtocol" },
};
}
@@ -93,7 +93,7 @@ void ApiSubscriptionPlansModel::updateModel(const QJsonArray &arr)
subscriptionPlan.subtitle = planObject.value(configKey::subtitle).toString();
subscriptionPlan.recommended = planObject.value(configKey::recommended).toBool();
subscriptionPlan.checkoutUrl = planObject.value(configKey::checkoutUrl).toString();
subscriptionPlan.serviceType = planObject.value(configKey::serviceType).toString();
subscriptionPlan.isTrial = planObject.value(configKey::isTrial).toBool();
subscriptionPlan.serviceProtocol = planObject.value(configKey::serviceProtocol).toString();
m_subscriptionPlans.append(std::move(subscriptionPlan));
}
@@ -119,7 +119,7 @@ QVariantMap ApiSubscriptionPlansModel::planAt(int row) const
planMap.insert(QLatin1String(configKey::subtitle), plan.subtitle);
planMap.insert(QLatin1String(configKey::recommended), plan.recommended);
planMap.insert(QLatin1String(configKey::checkoutUrlCamel), plan.checkoutUrl);
planMap.insert(QLatin1String(configKey::serviceTypeCamel), plan.serviceType);
planMap.insert(QLatin1String(configKey::isTrialCamel), plan.isTrial);
planMap.insert(QLatin1String(configKey::serviceProtocolCamel), plan.serviceProtocol);
return planMap;
}
@@ -16,7 +16,7 @@ public:
SubtitleRole,
RecommendedRole,
CheckoutUrlRole,
ServiceTypeRole,
IsTrialRole,
ServiceProtocolRole
};
Q_ENUM(Roles)
@@ -41,7 +41,7 @@ private:
QString subtitle;
bool recommended = false;
QString checkoutUrl;
QString serviceType;
bool isTrial = false;
QString serviceProtocol;
};
@@ -74,7 +74,7 @@ PageType {
Layout.rightMargin: 16
Layout.bottomMargin: 12
text: qsTr("Available with Free")
text: qsTr("Free features")
color: AmneziaStyle.color.mutedGray
font.pixelSize: 13
}
@@ -85,7 +85,7 @@ PageType {
Layout.rightMargin: 16
Layout.bottomMargin: 24
benefitsModel: ApiConfigsController.benefitsModel
benefitsModel: ApiBenefitsModel
}
ParagraphTextType {
@@ -94,7 +94,7 @@ PageType {
Layout.rightMargin: 16
Layout.bottomMargin: 16
visible: root.freeFeaturesHtml.length > 0 && ApiConfigsController.benefitsModel.rowCount() === 0
visible: root.freeFeaturesHtml.length > 0 && ApiBenefitsModel.rowCount() === 0
textFormat: Text.RichText
text: root.freeFeaturesHtml
@@ -127,7 +127,7 @@ PageType {
var termsUrl = LanguageModel.getCurrentSiteUrl()
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
.arg(termsUrl).arg(privacyUrl).arg(Qt.colorToString(AmneziaStyle.color.goldenApricot))
.arg(termsUrl).arg(privacyUrl).arg("#FBB26A")
}
onLinkActivated: function(link) {
@@ -158,7 +158,7 @@ PageType {
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
.arg(termsUrl).arg(privacyUrl).arg(Qt.colorToString(AmneziaStyle.color.goldenApricot))
.arg(termsUrl).arg(privacyUrl).arg("#FBB26A")
}
onLinkActivated: function(link) {
@@ -185,7 +185,7 @@ PageType {
anchors.rightMargin: 16
anchors.bottomMargin: 16 + SettingsController.safeAreaBottomMargin
text: ApiServicesModel.getSelectedServiceType() === "amnezia-trial" ? qsTr("Try Trial") : qsTr("Continue")
text: qsTr("Continue")
clickedFunc: function() {
PageController.showBusyIndicator(true)
@@ -9,21 +9,20 @@ import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
import PageEnum 1.0
PageType {
id: root
property int selectedPlanIndex: 0
property string premiumFeaturesHtml: ""
property string premiumHeaderName: ""
property string premiumHeaderDescription: ""
readonly property var currentPlan: ApiConfigsController.subscriptionPlansModel.planAt(selectedPlanIndex)
readonly property var currentPlan: ApiSubscriptionPlansModel.planAt(selectedPlanIndex)
function syncFromModel() {
root.selectedPlanIndex = ApiConfigsController.subscriptionPlansModel.recommendedRowIndex()
root.selectedPlanIndex = ApiSubscriptionPlansModel.recommendedRowIndex()
root.premiumFeaturesHtml = String(ApiServicesModel.getSelectedServiceData("features")).replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "")
root.premiumHeaderName = String(ApiServicesModel.getSelectedServiceData("name"))
root.premiumHeaderDescription = String(ApiServicesModel.getSelectedServiceData("serviceDescription"))
}
@@ -72,27 +71,17 @@ PageType {
descriptionText: root.premiumHeaderDescription
}
LabelTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 12
text: qsTr("Choose a plan")
color: AmneziaStyle.color.mutedGray
font.pixelSize: 13
}
Repeater {
model: ApiConfigsController.subscriptionPlansModel
model: ApiSubscriptionPlansModel
delegate: SubscriptionPlanCard {
required property int index
required property var model
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: index === ApiConfigsController.subscriptionPlansModel.rowCount() - 1 ? 24 : 12
Layout.bottomMargin: index === ApiSubscriptionPlansModel.rowCount() - 1 ? 24 : 12
selected: root.selectedPlanIndex === index
primaryLeft: String(model.primaryLeft)
@@ -116,32 +105,13 @@ PageType {
font.pixelSize: 13
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 16
textFormat: Text.RichText
text: root.premiumFeaturesHtml
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
BenefitsPanel {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 24
benefitsModel: ApiConfigsController.benefitsModel
benefitsModel: ApiBenefitsModel
}
ParagraphTextType {
@@ -177,7 +147,7 @@ PageType {
var termsUrl = LanguageModel.getCurrentSiteUrl()
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
.arg(termsUrl).arg(privacyUrl).arg(Qt.colorToString(AmneziaStyle.color.goldenApricot))
.arg(termsUrl).arg(privacyUrl).arg("#FBB26A")
}
onLinkActivated: function(link) {
@@ -208,7 +178,7 @@ PageType {
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: %3;\">Terms of Use</a> and <a href=\"%2\" style=\"color: %3;\">Privacy Policy</a>")
.arg(termsUrl).arg(privacyUrl).arg(Qt.colorToString(AmneziaStyle.color.goldenApricot))
.arg(termsUrl).arg(privacyUrl).arg("#FBB26A")
}
onLinkActivated: function(link) {
@@ -248,27 +218,24 @@ PageType {
if (!plan) {
return
}
if (plan.isTrial) {
PageController.goToPage(PageEnum.PageSetupWizardApiTrialEmail)
return
}
if (plan.checkoutUrl) {
Qt.openUrlExternally(plan.checkoutUrl)
PageController.closePage()
PageController.closePage()
return
}
if (plan.serviceType) {
var idx = ApiServicesModel.serviceIndexForType(plan.serviceType)
if (idx < 0) {
return
}
ApiServicesModel.setServiceIndex(idx)
PageController.showBusyIndicator(true)
var ok = ApiConfigsController.importService()
PageController.showBusyIndicator(false)
if (!ok) {
var endpoint = ApiServicesModel.getStoreEndpoint()
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
}
PageController.showBusyIndicator(true)
var ok = ApiConfigsController.importService()
PageController.showBusyIndicator(false)
if (!ok) {
var endpoint = ApiServicesModel.getStoreEndpoint()
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
}
}
}
@@ -0,0 +1,114 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
BackButtonType {
id: backButton
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
onFocusChanged: {
if (activeFocus) {
flick.contentY = 0
}
}
}
FlickableType {
id: flick
anchors.top: backButton.bottom
anchors.bottom: continueButton.top
anchors.left: parent.left
anchors.right: parent.right
contentHeight: scrollColumn.implicitHeight + 24
ColumnLayout {
id: scrollColumn
width: flick.width
spacing: 0
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 24
headerText: qsTr("Create an account")
descriptionText: qsTr("To manage your subscription")
}
TextFieldWithHeaderType {
id: emailField
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 24
headerText: qsTr("Email")
textField.placeholderText: qsTr("Email")
textField.inputMethodHints: Qt.ImhEmailCharactersOnly
}
ParagraphTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 24
wrapMode: Text.WordWrap
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: qsTr("Additional details for this step can be described here.")
}
}
}
BasicButtonType {
id: continueButton
z: 2
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.bottomMargin: 16 + SettingsController.safeAreaBottomMargin
text: qsTr("Continue")
clickedFunc: function() {
var raw = emailField.textField.text.trim()
if (raw.length === 0 || raw.indexOf("@") < 0) {
PageController.showNotificationMessage(qsTr("Enter a valid email address"))
return
}
PageController.showBusyIndicator(true)
var ok = ApiConfigsController.importTrialFromGateway(raw)
PageController.showBusyIndicator(false)
if (ok) {
PageController.closePage()
PageController.closePage()
}
}
}
}