feat: add purchase to UI

This commit is contained in:
NickVs2015
2026-02-19 12:55:00 +03:00
parent 4c03463344
commit fe99cdeb85
11 changed files with 656 additions and 256 deletions
+55 -5
View File
@@ -662,13 +662,55 @@ jobs:
shell: bash
run: ./deploy/build_android.sh --aab --play --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
- name: 'Build OSS AAB (in-app purchase)'
env:
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
QT_HOST_PATH: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64
ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
ANDROID_KEYSTORE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
shell: bash
run: ./deploy/build_android.sh --aab --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
- name: 'Upload OSS x86_64 apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-x86_64
path: deploy/build/AmneziaVPN-oss-x86_64-release.apk
compression-level: 0
retention-days: 7
- name: 'Upload OSS x86 apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-x86
path: deploy/build/AmneziaVPN-oss-x86-release.apk
compression-level: 0
retention-days: 7
- name: 'Upload OSS arm64-v8a apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-arm64-v8a
path: deploy/build/AmneziaVPN-oss-arm64-v8a-release.apk
compression-level: 0
retention-days: 7
- name: 'Upload OSS armeabi-v7a apk'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-armeabi-v7a
path: deploy/build/AmneziaVPN-oss-armeabi-v7a-release.apk
compression-level: 0
retention-days: 7
- name: 'Rename Android APKs'
run: |
cd deploy/build
mv AmneziaVPN-x86_64-release.apk AmneziaVPN_${VERSION}_android9+_x86_64.apk
mv AmneziaVPN-x86-release.apk AmneziaVPN_${VERSION}_android9+_x86.apk
mv AmneziaVPN-arm64-v8a-release.apk AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
mv AmneziaVPN-armeabi-v7a-release.apk AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
mv AmneziaVPN-oss-x86_64-release.apk AmneziaVPN_${VERSION}_android9+_x86_64.apk
mv AmneziaVPN-oss-x86-release.apk AmneziaVPN_${VERSION}_android9+_x86.apk
mv AmneziaVPN-oss-arm64-v8a-release.apk AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
mv AmneziaVPN-oss-armeabi-v7a-release.apk AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
cd ../..
- name: 'Upload x86_64 apk'
@@ -703,7 +745,7 @@ jobs:
compression-level: 0
retention-days: 7
- name: 'Upload aab'
- name: 'Upload Play AAB'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android
@@ -711,6 +753,14 @@ jobs:
compression-level: 0
retention-days: 7
- name: 'Upload OSS AAB (in-app purchase)'
uses: actions/upload-artifact@v4
with:
name: AmneziaVPN-android-oss-aab
path: deploy/build/AmneziaVPN-oss-release.aab
compression-level: 0
retention-days: 7
Extra:
runs-on: ubuntu-latest
steps:
+23
View File
@@ -1,5 +1,9 @@
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
# Option to build Play variant (with Google Play Billing) instead of OSS
# When ON, adds target android_play_apk: cmake --build . --target android_play_apk
option(ANDROID_BUILD_PLAY "Add android_play_apk target for Google Play Billing build" OFF)
set(APP_ANDROID_MIN_SDK 28)
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
"The minimum API level supported by the application or library" FORCE)
@@ -57,3 +61,22 @@ endforeach()
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/xray/android/libxray.aar
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
# Custom target to build Play variant (with Google Play Billing)
# Enable with: cmake -DANDROID_BUILD_PLAY=ON ...
# Then run: cmake --build <build_dir> --target android_play_apk
# Note: Do a normal build first so androiddeployqt creates the android-build folder
if(ANDROID_BUILD_PLAY)
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
set(_gradle_suffix "Debug")
else()
set(_gradle_suffix "Release")
endif()
set(_android_build_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build-${PROJECT}")
add_custom_target(android_play_apk
COMMAND ./gradlew assemblePlay${_gradle_suffix} -DexplicitRun=1
WORKING_DIRECTORY "${_android_build_dir}"
COMMENT "Building Android Play variant (assemblePlay${_gradle_suffix})"
DEPENDS ${PROJECT}
)
endif()
+1
View File
@@ -123,6 +123,7 @@ namespace amnezia
ApiUpdateRequestError = 1111,
ApiSubscriptionExpiredError = 1112,
ApiPurchaseError = 1113,
ApiNoPurchasesToRestore = 1114,
// QFile errors
OpenError = 1200,
+9
View File
@@ -80,6 +80,15 @@ QString errorString(ErrorCode code) {
case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break;
case (ErrorCode::ApiSubscriptionExpiredError): errorMessage = QObject::tr("Your Amnezia Premium subscription has expired.\n Please check your email for renewal instructions.\n If you haven't received an email, please contact our support."); break;
case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break;
case (ErrorCode::ApiNoPurchasesToRestore):
#if defined(Q_OS_ANDROID)
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same Google account used for the purchase.");
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same Apple ID used for the purchase.");
#else
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same account used for the purchase.");
#endif
break;
// QFile errors
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
+1 -1
View File
@@ -170,7 +170,7 @@ QJsonObject Deserialize(const QString &vmessStr, QString *alias, QString *errMes
// - It can be empty, if so, if the key is not in the JSON, or the value is empty, report an error.
// - Else if it contains one thing. if the key is not in the JSON, or the value is empty, use that one.
// - Else if it contains many things, when the key IS in the JSON but not within the THINGS, use the first in the THINGS
// - Else -------------------------------------------->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> use the JSON value
// - Else -------------------------------------------- use the JSON value
//
#define __vmess_checker__func(key, values) \
{ \
@@ -11,9 +11,14 @@
#include <QClipboard>
#include <QDebug>
#include <QEventLoop>
#include <QFutureWatcher>
#include <QSet>
#include <QtConcurrent>
#include "platforms/ios/ios_controller.h"
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
#endif
namespace
{
@@ -425,6 +430,86 @@ bool ApiConfigsController::fillAvailableServices()
}
}
}
#elif defined(Q_OS_ANDROID)
// Get price from Google Play Billing
auto androidController = AndroidController::instance();
QJsonObject plansResult = androidController->getSubscriptionPlans();
int responseCode = plansResult.value("responseCode").toInt(-1);
if (responseCode == 0) {
QJsonArray products = plansResult.value("products").toArray();
QString formattedPrice;
int billingPeriodDays = 180;
for (const QJsonValue &productValue : products) {
QJsonObject product = productValue.toObject();
if (product.value("productId").toString() == "premium") {
QJsonArray offers = product.value("offers").toArray();
if (!offers.isEmpty()) {
QJsonObject firstOffer = offers.at(0).toObject();
QJsonArray pricingPhases = firstOffer.value("pricingPhases").toArray();
if (!pricingPhases.isEmpty()) {
QJsonObject pricingPhase = pricingPhases.at(0).toObject();
formattedPrice = pricingPhase.value("formatedPrice").toString();
QString billingPeriod = pricingPhase.value("billingPeriod").toString();
if (billingPeriod.contains("D")) {
int idx = billingPeriod.indexOf("D");
billingPeriodDays = billingPeriod.mid(1, idx - 1).toInt();
} else if (billingPeriod.contains("M")) {
int idx = billingPeriod.indexOf("M");
int months = billingPeriod.mid(1, idx - 1).toInt();
if (months > 0) billingPeriodDays = months * 30;
}
}
}
break;
}
}
if (!formattedPrice.isEmpty()) {
QJsonArray services = data.value("services").toArray();
bool premiumFound = false;
for (int i = 0; i < services.size(); ++i) {
QJsonObject service = services[i].toObject();
if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) {
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
serviceInfo["price"] = formattedPrice;
service[configKey::serviceInfo] = serviceInfo;
services[i] = service;
data["services"] = services;
premiumFound = true;
qInfo() << "[Billing] Updated premium service price in data:" << formattedPrice;
break;
}
}
if (!premiumFound) {
// Gateway did not return premium; add it from billing data
QString region = data.value(configKey::userCountryCode).toString();
QJsonObject serviceInfo;
serviceInfo["name"] = tr("Amnezia Premium");
serviceInfo["price"] = formattedPrice;
serviceInfo["region"] = region;
serviceInfo["speed"] = "200";
serviceInfo["timelimit"] = QString::number(billingPeriodDays);
QJsonObject serviceDescription;
serviceDescription["card_description"] = tr("Amnezia Premium is classic VPN for seamless work, downloading large files, and watching videos.");
serviceDescription["description"] = serviceDescription["card_description"];
serviceDescription["features"] = "";
QJsonObject premiumService;
premiumService[configKey::serviceType] = serviceType::amneziaPremium;
premiumService[configKey::serviceProtocol] = "amnezia-premium";
premiumService[configKey::serviceInfo] = serviceInfo;
premiumService["service_description"] = serviceDescription;
premiumService["available_countries"] = QJsonArray();
premiumService["is_available"] = true;
premiumService["store_endpoint"] = "";
premiumService["subscription"] = QJsonObject();
services.prepend(premiumService);
data["services"] = services;
qInfo() << "[Billing] Added premium service from billing (gateway did not return it)";
}
}
} else {
qWarning() << "[Billing] Failed to fetch product price, responseCode:" << responseCode;
}
#endif
m_apiServicesModel->updateModel(data);
@@ -436,25 +521,19 @@ bool ApiConfigsController::fillAvailableServices()
bool ApiConfigsController::importService()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
bool isIosOrMacOsNe = true;
#else
bool isIosOrMacOsNe = false;
#endif
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
if (isIosOrMacOsNe) {
importSerivceFromAppStore();
return true;
}
} else {
importServiceFromGateway();
#if defined(Q_OS_IOS) || defined(MACOS_NE) || defined(Q_OS_ANDROID)
importSerivceFromPaymentMarket();
return true;
#else
return false; // premium only via App Store / Play
#endif
}
return false;
importServiceFromGateway();
return true;
}
bool ApiConfigsController::importSerivceFromAppStore()
bool ApiConfigsController::importSerivceFromPaymentMarket()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
bool purchaseOk = false;
@@ -511,12 +590,112 @@ bool ApiConfigsController::importSerivceFromAppStore()
return false;
}
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
#elif defined(Q_OS_ANDROID)
auto androidController = AndroidController::instance();
QString purchaseToken;
bool purchaseOk = false;
QFutureWatcher<QPair<bool, QString>> watcher;
QEventLoop waitLoop;
connect(&watcher, &QFutureWatcher<QPair<bool, QString>>::finished, &waitLoop, &QEventLoop::quit);
QFuture<QPair<bool, QString>> future = QtConcurrent::run([androidController]() {
QJsonObject plansResult = androidController->getSubscriptionPlans();
int responseCode = plansResult.value("responseCode").toInt(-1);
if (responseCode != 0) {
qWarning() << "[Billing] Failed to get subscription plans, responseCode:" << responseCode;
return qMakePair(false, QString());
}
QJsonArray products = plansResult.value("products").toArray();
QString offerToken;
for (const QJsonValue &productValue : products) {
QJsonObject product = productValue.toObject();
if (product.value("productId").toString() == "premium") {
QJsonArray offers = product.value("offers").toArray();
if (!offers.isEmpty()) {
QJsonObject firstOffer = offers.at(0).toObject();
offerToken = firstOffer.value("offerToken").toString();
qInfo() << "[Billing] Found offer token:" << offerToken;
break;
}
}
}
if (offerToken.isEmpty()) {
qWarning() << "[Billing] No offer token found for premium subscription";
return qMakePair(false, QString());
}
QJsonObject purchaseResult = androidController->purchaseSubscription(offerToken);
responseCode = purchaseResult.value("responseCode").toInt(-1);
if (responseCode != 0) {
qWarning() << "[Billing] Purchase failed, responseCode:" << responseCode;
return qMakePair(false, QString());
}
QJsonArray purchases = purchaseResult.value("purchases").toArray();
if (purchases.isEmpty()) {
qWarning() << "[Billing] Purchase succeeded but no purchases returned";
return qMakePair(false, QString());
}
QJsonObject purchase = purchases.at(0).toObject();
QString token = purchase.value("purchaseToken").toString();
bool isAcknowledged = purchase.value("isAcknowledged").toBool();
qInfo() << "[Billing] Purchase success. purchaseToken:" << token << "isAcknowledged:" << isAcknowledged;
if (!isAcknowledged) {
QJsonObject ackResult = androidController->acknowledgePurchase(token);
if (ackResult.value("responseCode").toInt(-1) != 0) {
qWarning() << "[Billing] Acknowledge failed";
} else {
qInfo() << "[Billing] Purchase acknowledged successfully";
}
}
return qMakePair(true, token);
});
watcher.setFuture(future);
waitLoop.exec();
purchaseOk = watcher.result().first;
purchaseToken = watcher.result().second;
if (!purchaseOk || purchaseToken.isEmpty()) {
emit errorOccurred(ErrorCode::ApiPurchaseError);
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() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = purchaseToken;
bool isTestPurchase = false; // TODO: detect if this is a test purchase
ErrorCode errorCode;
QByteArray responseBody;
errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
errorCode = importServiceFromBilling(responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return false;
}
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
#endif
return true;
}
bool ApiConfigsController::restoreSerivceFromAppStore()
bool ApiConfigsController::restoreSerivceFromPaymentMarket()
{
#if defined(Q_OS_IOS) || defined(MACOS_NE)
const QString premiumServiceType = QStringLiteral("amnezia-premium");
@@ -639,6 +818,131 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
qInfo().noquote() << "[IAP] Skipped" << duplicateCount
<< "duplicate restored transactions for original transaction IDs already processed";
}
#elif defined(Q_OS_ANDROID)
// Android Google Play Billing restore implementation
const QString premiumServiceType = QStringLiteral("amnezia-premium");
if (!fillAvailableServices()) {
qWarning() << "[Billing] Unable to fetch services list before restore";
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
if (m_apiServicesModel->rowCount() <= 0) {
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
// Ensure we have a valid premium selection for gateway requests
bool premiumSelected = false;
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
m_apiServicesModel->setServiceIndex(i);
if (m_apiServicesModel->getSelectedServiceType() == premiumServiceType) {
premiumSelected = true;
break;
}
}
if (!premiumSelected) {
emit errorOccurred(ErrorCode::ApiServicesMissingError);
return false;
}
auto androidController = AndroidController::instance();
// Query existing purchases
QJsonObject purchasesResult = androidController->queryPurchases();
int responseCode = purchasesResult.value("responseCode").toInt(-1);
if (responseCode != 0) {
qWarning() << "[Billing] Failed to query purchases, responseCode:" << responseCode;
emit errorOccurred(ErrorCode::ApiPurchaseError);
return false;
}
QJsonArray purchases = purchasesResult.value("purchases").toArray();
if (purchases.isEmpty()) {
qInfo() << "[Billing] No purchases found to restore";
emit errorOccurred(ErrorCode::ApiNoPurchasesToRestore);
return false;
}
bool hasInstalledConfig = false;
bool duplicateConfigAlreadyPresent = false;
QSet<QString> processedTokens;
for (const QJsonValue &purchaseValue : purchases) {
QJsonObject purchase = purchaseValue.toObject();
QString purchaseToken = purchase.value("purchaseToken").toString();
bool isAcknowledged = purchase.value("isAcknowledged").toBool();
if (purchaseToken.isEmpty()) {
qWarning() << "[Billing] Skipping purchase without token";
continue;
}
if (processedTokens.contains(purchaseToken)) {
continue;
}
processedTokens.insert(purchaseToken);
qInfo() << "[Billing] Restoring purchase. purchaseToken:" << purchaseToken
<< "isAcknowledged:" << isAcknowledged;
// Acknowledge purchase if needed
if (!isAcknowledged) {
QJsonObject ackResult = androidController->acknowledgePurchase(purchaseToken);
int ackResponseCode = ackResult.value("responseCode").toInt(-1);
if (ackResponseCode != 0) {
qWarning() << "[Billing] Acknowledge failed, responseCode:" << ackResponseCode;
} else {
qInfo() << "[Billing] Purchase acknowledged successfully";
}
}
// Send purchase token to gateway
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() };
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
apiPayload[apiDefs::key::transactionId] = purchaseToken;
bool isTestPurchase = false; // TODO: detect if this is a test purchase
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
if (errorCode != ErrorCode::NoError) {
qWarning() << "[Billing] Failed to restore purchase" << purchaseToken
<< "errorCode =" << static_cast<int>(errorCode);
continue;
}
ErrorCode installError = importServiceFromBilling(responseBody, isTestPurchase);
if (installError == ErrorCode::ApiConfigAlreadyAdded) {
duplicateConfigAlreadyPresent = true;
qInfo() << "[Billing] Skipping restored purchase" << purchaseToken
<< "because subscription config with the same vpn_key already exists";
} else if (installError != ErrorCode::NoError) {
qWarning() << "[Billing] Failed to process restored subscription response for purchase" << purchaseToken;
} else {
hasInstalledConfig = true;
}
}
if (!hasInstalledConfig) {
const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
emit errorOccurred(restoreError);
return false;
}
emit installServerFromApiFinished(tr("Subscription restored successfully."));
#endif
return true;
}
@@ -944,16 +1248,16 @@ QString ApiConfigsController::getVpnKey()
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase)
{
#ifdef Q_OS_IOS
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
QString key = responseObject.value(QStringLiteral("key")).toString();
if (key.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
qWarning().noquote() << "[IAP/Billing] Subscription response does not contain a key field";
return ErrorCode::ApiPurchaseError;
}
if (m_serversModel->hasServerWithVpnKey(key)) {
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
qInfo().noquote() << "[IAP/Billing] Subscription config with the same vpn_key already exists";
return ErrorCode::ApiConfigAlreadyAdded;
}
@@ -967,7 +1271,7 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
}
if (configString.isEmpty()) {
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
qWarning().noquote() << "[IAP/Billing] Subscription response config payload is empty";
return ErrorCode::ApiPurchaseError;
}
@@ -27,8 +27,8 @@ public slots:
bool fillAvailableServices();
bool importService();
bool importSerivceFromAppStore();
bool restoreSerivceFromAppStore();
bool importSerivceFromPaymentMarket();
bool restoreSerivceFromPaymentMarket();
bool importServiceFromGateway();
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
bool reloadServiceConfig = false);
@@ -114,6 +114,8 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
#if defined(Q_OS_IOS) || defined(MACOS_NE)
return tr("%1 $").arg(price);
#elif defined(Q_OS_ANDROID)
return price;
#else
return tr("%1 $/month").arg(price);
#endif
@@ -1,226 +1,237 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
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 (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 32
headerText: ApiServicesModel.getSelectedServiceData("name")
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
}
}
model: inputFields
spacing: 0
delegate: ColumnLayout {
width: listView.width
LabelWithImageType {
Layout.fillWidth: true
Layout.margins: 16
imageSource: imagePath
leftText: lText
rightText: rText
visible: isVisible
}
}
footer: ColumnLayout {
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")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
horizontalAlignment: Text.AlignHCenter
textFormat: Text.PlainText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
}
BasicButtonType {
id: continueButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.bottomMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
clickedFunc: function() {
PageController.showBusyIndicator(true)
var result = ApiConfigsController.importService()
PageController.showBusyIndicator(false)
if (!result) {
var endpoint = ApiServicesModel.getStoreEndpoint()
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
}
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 32
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: {
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: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
}
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
}
property list<QtObject> inputFields: [
region,
price,
timeLimit,
speed,
features
]
QtObject {
id: region
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
readonly property string lText: qsTr("For the region")
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
property bool isVisible: true
}
QtObject {
id: price
readonly property string imagePath: "qrc:/images/controls/tag.svg"
readonly property string lText: qsTr("Price")
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
property bool isVisible: true
}
QtObject {
id: timeLimit
readonly property string imagePath: "qrc:/images/controls/history.svg"
readonly property string lText: qsTr("Work period")
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
property bool isVisible: rText !== ""
}
QtObject {
id: speed
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
readonly property string lText: qsTr("Speed")
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
property bool isVisible: true
}
QtObject {
id: features
readonly property string imagePath: "qrc:/images/controls/info.svg"
readonly property string lText: qsTr("Features")
readonly property string rText: ""
property bool isVisible: true
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
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 (this.activeFocus) {
listView.positionViewAtBeginning()
}
}
}
ListViewType {
id: listView
anchors.top: backButton.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.left: parent.left
header: ColumnLayout {
width: listView.width
BaseHeaderType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.bottomMargin: 32
headerText: ApiServicesModel.getSelectedServiceData("name")
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
}
}
model: inputFields
spacing: 0
delegate: ColumnLayout {
width: listView.width
LabelWithImageType {
Layout.fillWidth: true
Layout.margins: 16
imageSource: imagePath
leftText: lText
rightText: rText
visible: isVisible
}
}
footer: ColumnLayout {
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")
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
visible: ((Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium") ||
(Qt.platform.os === "android" && ApiServicesModel.getSelectedServiceType() === "amnezia-premium")
horizontalAlignment: Text.AlignHCenter
textFormat: Text.PlainText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: {
if (Qt.platform.os === "ios" || IsMacOsNeBuild) {
return qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
} else if (Qt.platform.os === "android") {
return qsTr("Charged to your Google Play account at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Google Play settings.")
}
return ""
}
}
BasicButtonType {
id: continueButton
Layout.fillWidth: true
Layout.topMargin: 32
Layout.bottomMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
clickedFunc: function() {
PageController.showBusyIndicator(true)
var result = ApiConfigsController.importService()
PageController.showBusyIndicator(false)
if (!result) {
var endpoint = ApiServicesModel.getStoreEndpoint()
Qt.openUrlExternally(endpoint)
PageController.closePage()
PageController.closePage()
}
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 32
visible: ((Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium") ||
(Qt.platform.os === "android" && ApiServicesModel.getSelectedServiceType() === "amnezia-premium")
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
color: AmneziaStyle.color.mutedGray
font.pixelSize: 12
text: {
var termsUrl = Qt.platform.os === "ios" || IsMacOsNeBuild ?
"https://www.apple.com/legal/internet-services/itunes/dev/stdeula/" :
"https://play.google.com/intl/en_us/about/play-terms/"
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
}
onLinkActivated: function(link) {
Qt.openUrlExternally(link)
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
}
}
}
}
property list<QtObject> inputFields: [
region,
price,
timeLimit,
speed,
features
]
QtObject {
id: region
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
readonly property string lText: qsTr("For the region")
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
property bool isVisible: true
}
QtObject {
id: price
readonly property string imagePath: "qrc:/images/controls/tag.svg"
readonly property string lText: qsTr("Price")
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
property bool isVisible: true
}
QtObject {
id: timeLimit
readonly property string imagePath: "qrc:/images/controls/history.svg"
readonly property string lText: qsTr("Work period")
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
property bool isVisible: rText !== ""
}
QtObject {
id: speed
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
readonly property string lText: qsTr("Speed")
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
property bool isVisible: true
}
QtObject {
id: features
readonly property string imagePath: "qrc:/images/controls/info.svg"
readonly property string lText: qsTr("Features")
readonly property string rText: ""
property bool isVisible: true
}
}
@@ -358,10 +358,10 @@ PageType {
property string title: qsTr("Restore purchases")
property string description: qsTr("")
property string imageSource: "qrc:/images/controls/refresh-cw.svg"
property bool isVisible: Qt.platform.os === "ios" || IsMacOsNeBuild
property bool isVisible: Qt.platform.os === "ios" || IsMacOsNeBuild || Qt.platform.os === "android"
property var handler: function() {
PageController.showBusyIndicator(true)
ApiConfigsController.restoreSerivceFromAppStore()
ApiConfigsController.restoreSerivceFromPaymentMarket()
PageController.showBusyIndicator(false)
}
}
+1 -1
View File
@@ -193,4 +193,4 @@ if [[ -v CI || -v MOVE_RESULT ]]; then
$PROJECT_DIR/deploy/build/
done
fi
fi
fi