mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-07-28 03:22:01 +03:00
Compare commits
50 Commits
chore-xcod
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2aea5492d | ||
|
|
ab96b140d6 | ||
|
|
f5802c3f63 | ||
|
|
4a28cc56ce | ||
|
|
af4f5d5c81 | ||
|
|
a3ac87d737 | ||
|
|
fcaf76082e | ||
|
|
450fdbe181 | ||
|
|
66473627ba | ||
|
|
9e06a510e9 | ||
|
|
30e8300626 | ||
|
|
657a921ac3 | ||
|
|
e746638239 | ||
|
|
1ef249f01e | ||
|
|
1d1c928c25 | ||
|
|
6b533d7e06 | ||
|
|
bdd7309372 | ||
|
|
abe31672e0 | ||
|
|
4f2d4e045b | ||
|
|
a45e0db296 | ||
|
|
dc5aaf9f80 | ||
|
|
75ad6a87b0 | ||
|
|
b7e420a519 | ||
|
|
a44b59656a | ||
|
|
423f27ebe6 | ||
|
|
2986000f89 | ||
|
|
0d19cd8255 | ||
|
|
90ed45f571 | ||
|
|
4936fced71 | ||
|
|
afbe17c625 | ||
|
|
b12c916b50 | ||
|
|
5e69365c18 | ||
|
|
9088319671 | ||
|
|
673327a033 | ||
|
|
ad48f3b7e3 | ||
|
|
93cf7d60d5 | ||
|
|
4d28f81965 | ||
|
|
5c035bb6cf | ||
|
|
26c22327af | ||
|
|
5e9def4184 | ||
|
|
a98c60d967 | ||
|
|
0faf3e6d2f | ||
|
|
a32a26bb67 | ||
|
|
431ece7fff | ||
|
|
5e82c6a558 | ||
|
|
84a2d13b68 | ||
|
|
324b09cca3 | ||
|
|
e70563a071 | ||
|
|
bc2a0ca649 | ||
|
|
3c6ac431b4 |
48
.github/workflows/deploy.yml
vendored
48
.github/workflows/deploy.yml
vendored
@@ -531,6 +531,8 @@ jobs:
|
||||
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||
CODESIGN_SIGNATURE: ${{ secrets.MAC_SIGNER_ID }}
|
||||
CODESIGN_INSTALLER_SIGNATURE: ${{ secrets.MAC_INSTALLER_SIGNER_ID }}
|
||||
CODESIGN_KEYCHAIN: ${{ steps.setup-keychain.outputs.keychain-path }}
|
||||
CODESIGN_INSTALLER_KEYCHAIN: ${{ steps.setup-keychain.outputs.keychain-path }}
|
||||
NOTARYTOOL_TEAM_ID: ${{ secrets.MAC_TEAM_ID }}
|
||||
NOTARYTOOL_EMAIL: ${{ secrets.APPLE_DEV_EMAIL }}
|
||||
NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }}
|
||||
@@ -862,13 +864,41 @@ jobs:
|
||||
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
|
||||
|
||||
(cd deploy/build/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_universal.apk)
|
||||
(cd deploy/build/client/android-build/build/outputs/bundle/release && mv android-build-release.aab AmneziaVPN_${VERSION}.aab)
|
||||
(cd deploy/build/client/android-build/build/outputs/bundle/release && mv android-build-release.aab AmneziaVPN_${VERSION}_oss.aab)
|
||||
|
||||
for abi in arm64-v8a armeabi-v7a x86 x86_64; do
|
||||
deploy/build.sh -t android --sign --abi ${abi} --build ./deploy/build/${abi}
|
||||
(cd deploy/build/${abi}/client/android-build && mv AmneziaVPN.apk AmneziaVPN_${VERSION}_android9+_${abi}.apk)
|
||||
done
|
||||
|
||||
- name: 'Build Play AAB'
|
||||
env:
|
||||
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||
QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
|
||||
QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
|
||||
QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
|
||||
|
||||
deploy/build.sh -t android --sign --aab --play --build ./deploy/build/play
|
||||
|
||||
(cd deploy/build/play/client/android-build/build/outputs/bundle/playRelease && mv *.aab AmneziaVPN_${VERSION}_play.aab)
|
||||
|
||||
- name: 'Build Play APK'
|
||||
env:
|
||||
QT_INSTALL_DIR: ${{ runner.temp }}
|
||||
QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
|
||||
QT_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
|
||||
QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(grep CMAKE_PROJECT_VERSION:STATIC deploy/build/CMakeCache.txt | cut -d= -f2)
|
||||
|
||||
deploy/build.sh -t android --sign --apk --play --abi arm64-v8a --build ./deploy/build/play-apk
|
||||
|
||||
(cd deploy/build/play-apk/client/android-build/build/outputs/apk/play/release && mv *.apk AmneziaVPN_${VERSION}_play.apk)
|
||||
|
||||
- name: 'Upload universal APK'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -876,13 +906,27 @@ jobs:
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload AAB'
|
||||
- name: 'Upload OSS AAB'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/client/android-build/build/outputs/bundle/release/*.aab
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload Play AAB'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/play/client/android-build/build/outputs/bundle/playRelease/*.aab
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload Play APK'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: deploy/build/play-apk/client/android-build/build/outputs/apk/play/release/*.apk
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload arm64-v8a APK'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
|
||||
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.9.0.4)
|
||||
set(AMNEZIAVPN_VERSION 4.9.5.5)
|
||||
|
||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
||||
@@ -28,7 +28,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||
|
||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||
set(APP_ANDROID_VERSION_CODE 2133)
|
||||
set(APP_ANDROID_VERSION_CODE 2137)
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
|
||||
14
README.md
14
README.md
@@ -38,14 +38,14 @@
|
||||
|
||||
## Links
|
||||
|
||||
- [https://amnezia.org](https://amnezia.org) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/kldscp/amnezia.org)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org) - Documentation
|
||||
- [https://amnezia.org](https://amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Project website | [Alternative link (mirror)](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Documentation | [Alternative link (mirror)](https://storage.googleapis.com/amnezia/docs?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
|
||||
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Telegram support channel (English)
|
||||
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
|
||||
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Telegram support channel (Myanmar)
|
||||
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Telegram support channel (Russian)
|
||||
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium
|
||||
- [https://telegram.me/amnezia_vpn_en](https://telegram.me/amnezia_vpn_en) - Telegram support channel (English)
|
||||
- [https://telegram.me/amnezia_vpn_ir](https://telegram.me/amnezia_vpn_ir) - Telegram support channel (Farsi)
|
||||
- [https://telegram.me/amnezia_vpn_mm](https://telegram.me/amnezia_vpn_mm) - Telegram support channel (Myanmar)
|
||||
- [https://telegram.me/amnezia_vpn](https://telegram.me/amnezia_vpn) - Telegram support channel (Russian)
|
||||
- [Get Premium for 6 or 12 months](https://storage.googleapis.com/amnezia/pay?utm_source=github&utm_campaign=ampay-read)
|
||||
|
||||
## Tech
|
||||
|
||||
|
||||
14
README_RU.md
14
README_RU.md
@@ -35,14 +35,14 @@
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [https://amnezia.org](https://amnezia.org) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/kldscp/amnezia.org)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org) - Документация
|
||||
- [https://amnezia.org](https://amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Веб-сайт проекта | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/amnezia/amnezia.org?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [https://docs.amnezia.org](https://docs.amnezia.org/?utm_source=github&utm_campaign=amnezia_website-read) - Документация | [Альтернативная ссылка (зеркало)](https://storage.googleapis.com/amnezia/docs?utm_source=github&utm_campaign=amnezia_website-read)
|
||||
- [https://www.reddit.com/r/AmneziaVPN](https://www.reddit.com/r/AmneziaVPN) - Reddit
|
||||
- [https://t.me/amnezia_vpn_en](https://t.me/amnezia_vpn_en) - Канал поддержки в Telegram (Английский)
|
||||
- [https://t.me/amnezia_vpn_ir](https://t.me/amnezia_vpn_ir) - Канал поддержки в Telegram (Фарси)
|
||||
- [https://t.me/amnezia_vpn_mm](https://t.me/amnezia_vpn_mm) - Канал поддержки в Telegram (Мьянма)
|
||||
- [https://t.me/amnezia_vpn](https://t.me/amnezia_vpn) - Канал поддержки в Telegram (Русский)
|
||||
- [https://vpnpay.io/en/amnezia-premium/](https://vpnpay.io/en/amnezia-premium/) - Amnezia Premium | [Зеркало](https://storage.googleapis.com/kldscp/vpnpay.io/ru/amnezia-premium\)
|
||||
- [https://telegram.me/amnezia_vpn_en](https://telegram.me/amnezia_vpn_en) - Канал поддержки в Telegram (Английский)
|
||||
- [https://telegram.me/amnezia_vpn_ir](https://telegram.me/amnezia_vpn_ir) - Канал поддержки в Telegram (Фарси)
|
||||
- [https://telegram.me/amnezia_vpn_mm](https://telegram.me/amnezia_vpn_mm) - Канал поддержки в Telegram (Мьянма)
|
||||
- [https://telegram.me/amnezia_vpn](https://telegram.me/amnezia_vpn) - Канал поддержки в Telegram (Русский)
|
||||
- [Оформите Premium на 6 или 12 месяцев](https://storage.googleapis.com/amnezia/pay?utm_source=github&utm_campaign=ampay-read)
|
||||
|
||||
## Технологии
|
||||
|
||||
|
||||
2
client/3rd/amneziawg-apple
vendored
2
client/3rd/amneziawg-apple
vendored
Submodule client/3rd/amneziawg-apple updated: cf63135331...4bafa5958a
@@ -26,6 +26,10 @@
|
||||
#include "version.h"
|
||||
|
||||
#include "platforms/ios/QRCodeReaderBase.h"
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
|
||||
bool AmneziaApplication::m_forceQuit = false;
|
||||
@@ -139,12 +143,21 @@ void AmneziaApplication::init()
|
||||
m_engine->rootContext()->setContextProperty("IsMacOsNeBuild", false);
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
m_engine->rootContext()->setContextProperty("IsPlayBuild", AndroidController::instance()->isPlay());
|
||||
#else
|
||||
m_engine->rootContext()->setContextProperty("IsPlayBuild", false);
|
||||
#endif
|
||||
|
||||
m_vpnConnection.reset(new VpnConnection(nullptr, nullptr));
|
||||
m_vpnConnection->moveToThread(&m_vpnConnectionThread);
|
||||
m_vpnConnectionThread.start();
|
||||
|
||||
m_coreController.reset(new CoreController(m_vpnConnection, m_settings, m_engine));
|
||||
|
||||
m_marketplaceUpdateController.reset(new MarketplaceUpdateController());
|
||||
m_marketplaceUpdateController->start();
|
||||
|
||||
m_engine->addImportPath("qrc:/ui/qml/Modules/");
|
||||
|
||||
if (m_parser.isSet(m_optImport)) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
#include "core/controllers/coreController.h"
|
||||
#include "secureQSettings.h"
|
||||
#include "ui/controllers/marketplaceUpdateController.h"
|
||||
#include "vpnConnection.h"
|
||||
#include "ui/models/containerProps.h"
|
||||
#include "ui/models/protocolProps.h"
|
||||
@@ -56,6 +57,7 @@ private:
|
||||
SecureQSettings* m_settings;
|
||||
|
||||
QScopedPointer<CoreController> m_coreController;
|
||||
QScopedPointer<MarketplaceUpdateController> m_marketplaceUpdateController;
|
||||
|
||||
QSharedPointer<ContainerProps> m_containerProps;
|
||||
QSharedPointer<ProtocolProps> m_protocolProps;
|
||||
|
||||
19
client/android/billing/build.gradle.kts
Normal file
19
client/android/billing/build.gradle.kts
Normal file
@@ -0,0 +1,19 @@
|
||||
plugins {
|
||||
id(libs.plugins.android.library.get().pluginId)
|
||||
id(libs.plugins.kotlin.android.get().pluginId)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.amnezia.vpn.billing"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(project(":utils"))
|
||||
implementation(libs.androidx.core)
|
||||
implementation(libs.kotlinx.coroutines)
|
||||
implementation(libs.android.billing)
|
||||
}
|
||||
65
client/android/billing/src/main/kotlin/BillingException.kt
Normal file
65
client/android/billing/src/main/kotlin/BillingException.kt
Normal file
@@ -0,0 +1,65 @@
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.BILLING_UNAVAILABLE
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.DEVELOPER_ERROR
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.ERROR
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_NOT_OWNED
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.ITEM_UNAVAILABLE
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.NETWORK_ERROR
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_DISCONNECTED
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode.USER_CANCELED
|
||||
import com.android.billingclient.api.BillingResult
|
||||
import org.amnezia.vpn.util.ErrorCode
|
||||
|
||||
internal class BillingException(
|
||||
billingResult: BillingResult,
|
||||
retryable: Boolean = false
|
||||
) : Exception(billingResult.toString()) {
|
||||
|
||||
constructor(msg: String) : this(BillingResult.newBuilder()
|
||||
.setResponseCode(DEVELOPER_ERROR)
|
||||
.setDebugMessage(msg)
|
||||
.build())
|
||||
|
||||
val errorCode: Int
|
||||
val isCanceled = billingResult.responseCode == USER_CANCELED
|
||||
val isRetryable = retryable || billingResult.responseCode in setOf(
|
||||
NETWORK_ERROR,
|
||||
SERVICE_DISCONNECTED,
|
||||
SERVICE_UNAVAILABLE,
|
||||
ERROR
|
||||
)
|
||||
|
||||
init {
|
||||
when (billingResult.responseCode) {
|
||||
ERROR -> {
|
||||
errorCode = ErrorCode.BillingGooglePlayError
|
||||
}
|
||||
|
||||
BILLING_UNAVAILABLE, SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE -> {
|
||||
errorCode = ErrorCode.BillingUnavailable
|
||||
}
|
||||
|
||||
DEVELOPER_ERROR, FEATURE_NOT_SUPPORTED, ITEM_NOT_OWNED -> {
|
||||
errorCode = ErrorCode.BillingError
|
||||
}
|
||||
|
||||
ITEM_ALREADY_OWNED -> {
|
||||
errorCode = ErrorCode.SubscriptionAlreadyOwned
|
||||
}
|
||||
|
||||
ITEM_UNAVAILABLE -> {
|
||||
errorCode = ErrorCode.SubscriptionUnavailable
|
||||
}
|
||||
|
||||
NETWORK_ERROR -> {
|
||||
errorCode = ErrorCode.BillingNetworkError
|
||||
}
|
||||
|
||||
else -> {
|
||||
errorCode = ErrorCode.BillingError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
356
client/android/billing/src/main/kotlin/BillingProvider.kt
Normal file
356
client/android/billing/src/main/kotlin/BillingProvider.kt
Normal file
@@ -0,0 +1,356 @@
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams
|
||||
import com.android.billingclient.api.BillingClient
|
||||
import com.android.billingclient.api.BillingClient.BillingResponseCode
|
||||
import com.android.billingclient.api.BillingClient.ProductType
|
||||
import com.android.billingclient.api.BillingClientStateListener
|
||||
import com.android.billingclient.api.BillingFlowParams
|
||||
import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
|
||||
import com.android.billingclient.api.BillingResult
|
||||
import com.android.billingclient.api.GetBillingConfigParams
|
||||
import com.android.billingclient.api.PendingPurchasesParams
|
||||
import com.android.billingclient.api.ProductDetails
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||
import com.android.billingclient.api.QueryProductDetailsParams
|
||||
import com.android.billingclient.api.QueryProductDetailsParams.Product
|
||||
import com.android.billingclient.api.QueryPurchasesParams
|
||||
import com.android.billingclient.api.acknowledgePurchase
|
||||
import com.android.billingclient.api.queryProductDetails
|
||||
import com.android.billingclient.api.queryPurchasesAsync
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.vpn.util.ErrorCode
|
||||
import org.amnezia.vpn.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
|
||||
private const val TAG = "BillingProvider"
|
||||
private const val PRODUCT_ID = "premium"
|
||||
|
||||
private val billingPeriodRegex = Regex("""P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?""")
|
||||
|
||||
private fun billingPeriodToMonths(period: String): Double {
|
||||
val match = billingPeriodRegex.matchEntire(period) ?: return 0.0
|
||||
val years = match.groupValues[1].toDoubleOrNull() ?: 0.0
|
||||
val months = match.groupValues[2].toDoubleOrNull() ?: 0.0
|
||||
val weeks = match.groupValues[3].toDoubleOrNull() ?: 0.0
|
||||
val days = match.groupValues[4].toDoubleOrNull() ?: 0.0
|
||||
return years * 12.0 + months + weeks * 7.0 / 30.0 + days / 30.0
|
||||
}
|
||||
|
||||
private fun displayPricePerMonth(priceAmountMicros: Long, currencyCode: String, billingMonths: Double): String? {
|
||||
if (billingMonths <= 1e-6 || currencyCode.isBlank()) return null
|
||||
return try {
|
||||
val amountPerMonth = (priceAmountMicros / 1_000_000.0) / billingMonths
|
||||
NumberFormat.getCurrencyInstance().apply { currency = Currency.getInstance(currencyCode) }
|
||||
.format(amountPerMonth)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.w(TAG, "Unknown currency code from Play Billing: $currencyCode")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
class BillingProvider(context: Context) : AutoCloseable {
|
||||
|
||||
private var billingClient: BillingClient
|
||||
private var subscriptionPurchases = MutableStateFlow<Pair<BillingResult, List<Purchase>?>?>(null)
|
||||
|
||||
private val purchasesUpdatedListeners = PurchasesUpdatedListener { billingResult, purchases ->
|
||||
Log.v(TAG, "Purchases updated: $billingResult")
|
||||
subscriptionPurchases.value = billingResult to purchases
|
||||
}
|
||||
|
||||
init {
|
||||
billingClient = BillingClient.newBuilder(context)
|
||||
.setListener(purchasesUpdatedListeners)
|
||||
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
|
||||
.build()
|
||||
}
|
||||
|
||||
private suspend fun connect() {
|
||||
if (billingClient.isReady) return
|
||||
|
||||
Log.v(TAG, "Billing client connection")
|
||||
val connection = CompletableDeferred<Unit>()
|
||||
withContext(Dispatchers.IO) {
|
||||
billingClient.startConnection(object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||
Log.v(TAG, "Billing setup finished: $billingResult")
|
||||
if (billingResult.isOk) {
|
||||
connection.complete(Unit)
|
||||
} else {
|
||||
Log.e(TAG, "Billing setup failed: $billingResult")
|
||||
connection.completeExceptionally(BillingException(billingResult))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
Log.w(TAG, "Billing service disconnected")
|
||||
}
|
||||
})
|
||||
}
|
||||
connection.await()
|
||||
}
|
||||
|
||||
private suspend fun handleBillingApiCall(block: suspend () -> JSONObject): JSONObject {
|
||||
val numberAttempts = 3
|
||||
var attemptCount = 0
|
||||
while (true) {
|
||||
try {
|
||||
return block()
|
||||
} catch (e: BillingException) {
|
||||
if (e.isCanceled) {
|
||||
Log.w(TAG, "Billing canceled")
|
||||
return JSONObject().put("responseCode", ErrorCode.BillingCanceled)
|
||||
} else if (e.isRetryable && attemptCount < numberAttempts) {
|
||||
Log.d(TAG, "Retryable error: $e")
|
||||
++attemptCount
|
||||
delay(1000)
|
||||
} else {
|
||||
Log.e(TAG, "Billing error: $e")
|
||||
return JSONObject().put("responseCode", e.errorCode)
|
||||
}
|
||||
} catch (_: CancellationException) {
|
||||
Log.w(TAG, "Billing coroutine canceled")
|
||||
return JSONObject().put("responseCode", ErrorCode.BillingCanceled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSubscriptionPlans(): JSONObject {
|
||||
Log.v(TAG, "Get subscription plans")
|
||||
|
||||
val productDetailsList = getProductDetails()
|
||||
val resultJson = JSONObject().put("responseCode", ErrorCode.NoError)
|
||||
val productArray = JSONArray().also { resultJson.put("products", it) }
|
||||
productDetailsList?.forEach { productDetails ->
|
||||
val product = JSONObject().also { productArray.put(it) }
|
||||
.put("productId", productDetails.productId)
|
||||
.put("name", productDetails.name)
|
||||
val offers = JSONArray().also { product.put("offers", it) }
|
||||
productDetails.subscriptionOfferDetails?.forEach { offerDetails ->
|
||||
val offer = JSONObject().also { offers.put(it) }
|
||||
.put("basePlanId", offerDetails.basePlanId)
|
||||
.put("offerId", offerDetails.offerId)
|
||||
.put("offerToken", offerDetails.offerToken)
|
||||
val pricingPhases = JSONArray().also { offer.put("pricingPhases", it) }
|
||||
offerDetails.pricingPhases.pricingPhaseList.forEach { phase ->
|
||||
val billingMonths = billingPeriodToMonths(phase.billingPeriod)
|
||||
JSONObject().also { pricingPhases.put(it) }
|
||||
.put("billingCycleCount", phase.billingCycleCount)
|
||||
.put("billingPeriod", phase.billingPeriod)
|
||||
.put("formatedPrice", phase.formattedPrice)
|
||||
.put("priceAmountMicros", phase.priceAmountMicros)
|
||||
.put("recurrenceMode", phase.recurrenceMode)
|
||||
.put("subscriptionBillingMonths", billingMonths)
|
||||
.apply {
|
||||
displayPricePerMonth(phase.priceAmountMicros, phase.priceCurrencyCode, billingMonths)?.let {
|
||||
put("displayPricePerMonth", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
val regularPhase = offerDetails.pricingPhases.pricingPhaseList.lastOrNull()
|
||||
Log.v(TAG, "Offer ${offerDetails.basePlanId}: regular price = ${regularPhase?.formattedPrice}")
|
||||
}
|
||||
}
|
||||
return resultJson
|
||||
}
|
||||
|
||||
private suspend fun getProductDetails(): List<ProductDetails>? {
|
||||
Log.v(TAG, "Get product details")
|
||||
|
||||
val productDetailsParams = Product.newBuilder()
|
||||
.setProductId(PRODUCT_ID)
|
||||
.setProductType(ProductType.SUBS)
|
||||
.build()
|
||||
|
||||
val queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
|
||||
.setProductList(listOf(productDetailsParams))
|
||||
.build()
|
||||
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
billingClient.queryProductDetails(queryProductDetailsParams)
|
||||
}
|
||||
|
||||
Log.v(TAG, "Query product details result: ${result.billingResult}")
|
||||
|
||||
if (!result.billingResult.isOk) {
|
||||
Log.e(TAG, "Failed to get product details: ${result.billingResult}")
|
||||
throw BillingException(result.billingResult)
|
||||
}
|
||||
|
||||
return result.productDetailsList
|
||||
}
|
||||
|
||||
suspend fun getCustomerCountryCode(): JSONObject {
|
||||
Log.v(TAG, "Get customer country code")
|
||||
|
||||
val deferred = CompletableDeferred<String>()
|
||||
withContext(Dispatchers.IO) {
|
||||
billingClient.getBillingConfigAsync(GetBillingConfigParams.newBuilder().build(),
|
||||
{ billingResult, billingConfig ->
|
||||
Log.v(TAG, "Billing config: $billingResult, ${billingConfig?.countryCode}")
|
||||
if (billingResult.isOk) {
|
||||
deferred.complete(billingConfig?.countryCode ?: "")
|
||||
} else {
|
||||
deferred.completeExceptionally(BillingException(billingResult))
|
||||
}
|
||||
})
|
||||
}
|
||||
val countryCode = deferred.await()
|
||||
|
||||
return JSONObject()
|
||||
.put("responseCode", ErrorCode.NoError)
|
||||
.put("countryCode", countryCode)
|
||||
}
|
||||
|
||||
suspend fun purchaseSubscription(
|
||||
activity: Activity,
|
||||
offerToken: String,
|
||||
oldPurchaseToken: String? = null
|
||||
): JSONObject {
|
||||
Log.v(TAG, "Purchase subscription")
|
||||
Log.v(TAG, "Offer token: $offerToken")
|
||||
oldPurchaseToken?.let { Log.v(TAG, "Old purchase token: $it") }
|
||||
|
||||
if (offerToken.isBlank()) throw BillingException("offerToken can not be empty")
|
||||
|
||||
val productDetails = getProductDetails()?.let {
|
||||
it.filter { it.productId == PRODUCT_ID }
|
||||
}?.firstOrNull() ?: throw BillingException("Product details not found")
|
||||
|
||||
Log.v(TAG, "Filtered product details:\n$productDetails")
|
||||
|
||||
val productDetail = BillingFlowParams.ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(productDetails)
|
||||
.setOfferToken(offerToken)
|
||||
.build()
|
||||
|
||||
val subscriptionUpdateParams = oldPurchaseToken?.let {
|
||||
BillingFlowParams.SubscriptionUpdateParams.newBuilder()
|
||||
.setOldPurchaseToken(oldPurchaseToken)
|
||||
.setSubscriptionReplacementMode(ReplacementMode.WITHOUT_PRORATION)
|
||||
.build()
|
||||
}
|
||||
|
||||
val billingResult = billingClient.launchBillingFlow(activity, BillingFlowParams.newBuilder()
|
||||
.setProductDetailsParamsList(listOf(productDetail))
|
||||
.apply { subscriptionUpdateParams?.let { setSubscriptionUpdateParams(it) } }
|
||||
.build())
|
||||
|
||||
Log.v(TAG, "Start billing flow result: $billingResult")
|
||||
|
||||
if (billingResult.responseCode == BillingResponseCode.ITEM_ALREADY_OWNED) {
|
||||
Log.w(TAG, "Attempting to purchase already owned product")
|
||||
val purchases = queryPurchases()
|
||||
if (purchases.any { PRODUCT_ID in it.products }) throw BillingException(billingResult)
|
||||
else throw BillingException(billingResult, retryable = true)
|
||||
} else if (billingResult.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
|
||||
Log.w(TAG, "Attempting to replace not owned product")
|
||||
val purchases = queryPurchases()
|
||||
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(billingResult)
|
||||
else throw BillingException(billingResult, retryable = true)
|
||||
} else if (!billingResult.isOk) throw BillingException(billingResult)
|
||||
|
||||
subscriptionPurchases.firstOrNull { it != null }?.let { (billingResult, purchases) ->
|
||||
if (!billingResult.isOk) throw BillingException(billingResult)
|
||||
return JSONObject()
|
||||
.put("responseCode", ErrorCode.NoError)
|
||||
.put("purchases", processPurchases(purchases))
|
||||
} ?: throw BillingException("Purchase failed")
|
||||
}
|
||||
|
||||
private fun processPurchases(purchases: List<Purchase>?): JSONArray {
|
||||
val purchaseArray = JSONArray()
|
||||
purchases?.forEach { purchase ->
|
||||
Log.v(TAG, "processPurchases: purchaseToken=${purchase.purchaseToken} orderId=${purchase.orderId} state=${purchase.purchaseState}")
|
||||
/* val purchaseJson = */ JSONObject().also { purchaseArray.put(it) }
|
||||
.put("purchaseToken", purchase.purchaseToken)
|
||||
.put("purchaseTime", purchase.purchaseTime)
|
||||
.put("purchaseState", purchase.purchaseState)
|
||||
.put("isAcknowledged", purchase.isAcknowledged)
|
||||
.put("isAutoRenewing", purchase.isAutoRenewing)
|
||||
.put("orderId", purchase.orderId)
|
||||
// .put("productIds", JSONArray(purchase.products))
|
||||
|
||||
/* purchase.pendingPurchaseUpdate?.let { purchaseUpdate ->
|
||||
JSONObject()
|
||||
.put("purchaseToken", purchaseUpdate.purchaseToken)
|
||||
// .put("productIds", JSONArray(purchaseUpdate.products))
|
||||
}.also { purchaseJson.put("pendingPurchaseUpdate", it) } */
|
||||
}
|
||||
return purchaseArray
|
||||
}
|
||||
|
||||
suspend fun acknowledge(purchaseToken: String): JSONObject {
|
||||
Log.v(TAG, "Acknowledge purchase: $purchaseToken")
|
||||
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
billingClient.acknowledgePurchase(
|
||||
AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(purchaseToken)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
Log.v(TAG, "Acknowledge purchase result: $result")
|
||||
|
||||
if (result.responseCode == BillingResponseCode.ITEM_NOT_OWNED) {
|
||||
Log.w(TAG, "Attempting to acknowledge not owned product")
|
||||
val purchases = queryPurchases()
|
||||
if (purchases.all { PRODUCT_ID !in it.products }) throw BillingException(result)
|
||||
else throw BillingException(result, retryable = true)
|
||||
} else if (!result.isOk && result.responseCode != BillingResponseCode.ITEM_ALREADY_OWNED) {
|
||||
throw BillingException(result)
|
||||
}
|
||||
|
||||
return JSONObject().put("responseCode", ErrorCode.NoError)
|
||||
}
|
||||
|
||||
suspend fun getPurchases(): JSONObject {
|
||||
Log.v(TAG, "Get purchases")
|
||||
val purchases = queryPurchases()
|
||||
return JSONObject()
|
||||
.put("responseCode", ErrorCode.NoError)
|
||||
.put("purchases", processPurchases(purchases))
|
||||
}
|
||||
|
||||
private suspend fun queryPurchases(): List<Purchase> {
|
||||
Log.v(TAG, "Query purchases")
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
billingClient.queryPurchasesAsync(
|
||||
QueryPurchasesParams.newBuilder().setProductType(ProductType.SUBS).build()
|
||||
)
|
||||
}
|
||||
Log.v(TAG, "Query purchases result: ${result.billingResult}")
|
||||
if (!result.billingResult.isOk) throw BillingException(result.billingResult)
|
||||
return result.purchasesList
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
Log.v(TAG, "Close billing client connection")
|
||||
billingClient.endConnection()
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun withBillingProvider(context: Context, block: suspend BillingProvider.() -> JSONObject): String =
|
||||
BillingProvider(context).use { bp ->
|
||||
bp.handleBillingApiCall {
|
||||
bp.connect()
|
||||
bp.block()
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val BillingResult.isOk: Boolean
|
||||
get() = responseCode == BillingResponseCode.OK
|
||||
@@ -20,6 +20,7 @@ android {
|
||||
namespace = "org.amnezia.vpn"
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
@@ -33,13 +34,56 @@ android {
|
||||
jniLibs.useLegacyPackaging = true
|
||||
}
|
||||
|
||||
val abiList = qtTargetAbiList.split(",")
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.amnezia.vpn"
|
||||
targetSdk = qtTargetSdkVersion.toInt()
|
||||
|
||||
// keeps language resources for only the locales specified below
|
||||
resourceConfigurations += listOf("en", "ru", "b+zh+Hans")
|
||||
ndk.abiFilters += qtTargetAbiList.split(",")
|
||||
// ndk.abiFilters is only used for single-ABI builds; multi-ABI uses splits below
|
||||
if (abiList.size == 1) {
|
||||
ndk.abiFilters += abiList
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
register("release") {
|
||||
storeFile = providers.environmentVariable("QT_ANDROID_KEYSTORE_PATH").orNull?.let { file(it) }
|
||||
storePassword = providers.environmentVariable("QT_ANDROID_KEYSTORE_STORE_PASS").orNull
|
||||
keyAlias = providers.environmentVariable("QT_ANDROID_KEYSTORE_ALIAS").orNull
|
||||
keyPassword = providers.environmentVariable("QT_ANDROID_KEYSTORE_STORE_PASS").orNull
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// exclude coroutine debug resource from release build
|
||||
packaging {
|
||||
resources.excludes += "DebugProbesKt.bin"
|
||||
}
|
||||
signingConfig = signingConfigs["release"]
|
||||
}
|
||||
|
||||
create("fdroid") {
|
||||
initWith(getByName("release"))
|
||||
signingConfig = null
|
||||
matchingFallbacks += "release"
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += "billing"
|
||||
|
||||
productFlavors {
|
||||
create("oss") {
|
||||
dimension = "billing"
|
||||
buildConfigField("boolean", "IS_PLAY_BUILD", "false")
|
||||
}
|
||||
create("play") {
|
||||
dimension = "billing"
|
||||
buildConfigField("boolean", "IS_PLAY_BUILD", "true")
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -51,13 +95,74 @@ android {
|
||||
assets.setSrcDirs(listOf("assets"))
|
||||
jniLibs.setSrcDirs(listOf("libs"))
|
||||
}
|
||||
|
||||
getByName("oss") {
|
||||
java.setSrcDirs(listOf("oss"))
|
||||
}
|
||||
|
||||
getByName("play") {
|
||||
java.setSrcDirs(listOf("play"))
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// exclude coroutine debug resource from release build
|
||||
packaging {
|
||||
resources.excludes += "DebugProbesKt.bin"
|
||||
splits {
|
||||
abi {
|
||||
// splits only make sense for multi-ABI builds; single-ABI uses ndk.abiFilters
|
||||
isEnable = abiList.size > 1
|
||||
reset()
|
||||
include(*abiList.toTypedArray())
|
||||
isUniversalApk = false
|
||||
}
|
||||
}
|
||||
|
||||
// fix for Qt Creator to allow deploying the application to a device
|
||||
// to enable this fix, add the line outputBaseName=android-build to local.properties
|
||||
if (outputBaseName.isNotEmpty()) {
|
||||
applicationVariants.all {
|
||||
outputs.map { it as BaseVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
if (output.outputFileName.endsWith(".apk")) {
|
||||
output.outputFileName = "$outputBaseName-${buildType.name}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// androiddeployqt expects:
|
||||
// APK: build/outputs/apk/{base}-{buildType}[-unsigned].apk (no flavor subdir)
|
||||
// AAB: build/outputs/bundle/{buildType}/{base}-{buildType}.aab (no flavor subdir)
|
||||
// where {base} = outputBaseName (set by Qt Creator) or "android-build" (CI fallback).
|
||||
// Release APK gets -unsigned suffix (Qt cmake signs it); debug does not.
|
||||
// Copy only oss flavor to the flat output dir that androiddeployqt/Qt Creator expect.
|
||||
// Play flavor is built via android_play_apk/android_play_aab cmake targets and uses
|
||||
// its native Gradle output paths directly.
|
||||
applicationVariants.all {
|
||||
val flavorName = productFlavors.firstOrNull()?.name ?: ""
|
||||
val buildTypeName = buildType.name
|
||||
if (flavorName == "oss") {
|
||||
val base = outputBaseName.ifEmpty { "android-build" }
|
||||
val unsignedSuffix = if (buildTypeName == "release") "-unsigned" else ""
|
||||
|
||||
packageApplicationProvider.configure {
|
||||
doLast {
|
||||
val srcDir = layout.buildDirectory.dir("outputs/apk/oss/$buildTypeName").get().asFile
|
||||
val dstDir = layout.buildDirectory.dir("outputs/apk").get().asFile
|
||||
dstDir.mkdirs()
|
||||
srcDir.listFiles()?.filter { it.name.endsWith(".apk") }?.forEach { apk ->
|
||||
apk.copyTo(File(dstDir, "$base-$buildTypeName$unsignedSuffix.apk"), overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("bundle${name.replaceFirstChar { it.uppercase() }}") {
|
||||
doLast {
|
||||
val srcDir = layout.buildDirectory.dir("outputs/bundle/ossRelease").get().asFile
|
||||
val dstDir = layout.buildDirectory.dir("outputs/bundle/$buildTypeName").get().asFile
|
||||
dstDir.mkdirs()
|
||||
srcDir.listFiles()?.filter { it.name.endsWith(".aab") }?.forEach { aab ->
|
||||
aab.copyTo(File(dstDir, "$base-$buildTypeName.aab"), overwrite = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,4 +189,9 @@ dependencies {
|
||||
implementation(libs.google.mlkit)
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.biometric)
|
||||
|
||||
playImplementation(project(":billing"))
|
||||
}
|
||||
|
||||
fun DependencyHandler.playImplementation(dependency: Any): Dependency? =
|
||||
add("playImplementation", dependency)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[versions]
|
||||
agp = "8.6.1"
|
||||
kotlin = "1.9.24"
|
||||
kotlin = "2.1.0"
|
||||
android-billing = "8.0.0"
|
||||
androidx-core = "1.13.1"
|
||||
androidx-activity = "1.9.1"
|
||||
androidx-annotation = "1.8.2"
|
||||
@@ -14,6 +15,7 @@ kotlinx-serialization = "1.6.3"
|
||||
google-mlkit = "17.3.0"
|
||||
|
||||
[libraries]
|
||||
android-billing = { module = "com.android.billingclient:billing-ktx", version.ref = "android-billing" }
|
||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||
androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
|
||||
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
|
||||
class BillingPaymentRepository(@Suppress("UNUSED_PARAMETER") context: Context) : BillingRepository {
|
||||
override suspend fun getCountryCode(): String = ""
|
||||
override suspend fun getSubscriptionPlans(): String = ""
|
||||
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String = ""
|
||||
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String = ""
|
||||
override suspend fun acknowledge(purchaseToken: String): String = ""
|
||||
override suspend fun queryPurchases(): String = ""
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import BillingProvider.Companion.withBillingProvider
|
||||
|
||||
class BillingPaymentRepository(private val context: Context) : BillingRepository {
|
||||
|
||||
override suspend fun getCountryCode(): String = withBillingProvider(context) {
|
||||
getCustomerCountryCode()
|
||||
}
|
||||
|
||||
override suspend fun getSubscriptionPlans(): String = withBillingProvider(context) {
|
||||
getSubscriptionPlans()
|
||||
}
|
||||
|
||||
override suspend fun purchaseSubscription(activity: Activity, offerToken: String): String =
|
||||
withBillingProvider(context) {
|
||||
purchaseSubscription(activity, offerToken)
|
||||
}
|
||||
|
||||
override suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String =
|
||||
withBillingProvider(context) {
|
||||
purchaseSubscription(activity, offerToken, oldPurchaseToken)
|
||||
}
|
||||
|
||||
override suspend fun acknowledge(purchaseToken: String): String = withBillingProvider(context) {
|
||||
acknowledge(purchaseToken)
|
||||
}
|
||||
|
||||
override suspend fun queryPurchases(): String = withBillingProvider(context) {
|
||||
getPurchases()
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ rootProject.buildFileName = "build.gradle.kts"
|
||||
|
||||
include(":qt")
|
||||
include(":utils")
|
||||
include(":billing")
|
||||
include(":protocolApi")
|
||||
include(":wireguard")
|
||||
include(":awg")
|
||||
|
||||
@@ -3,6 +3,13 @@ package org.amnezia.vpn
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.Gravity
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.app.NotificationManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.BroadcastReceiver
|
||||
@@ -56,7 +63,6 @@ import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.amnezia.vpn.protocol.getStatistics
|
||||
import org.amnezia.vpn.protocol.getStatus
|
||||
import org.amnezia.vpn.qt.QtAndroidController
|
||||
@@ -90,6 +96,7 @@ class AmneziaActivity : QtActivity() {
|
||||
private var notificationStateReceiver: BroadcastReceiver? = null
|
||||
private lateinit var vpnServiceMessenger: IpcMessenger
|
||||
private var pfd: ParcelFileDescriptor? = null
|
||||
private lateinit var billingRepository: BillingRepository
|
||||
|
||||
private val actionResultHandlers = mutableMapOf<Int, ActivityResultHandler>()
|
||||
private val permissionRequestHandlers = mutableMapOf<Int, PermissionRequestHandler>()
|
||||
@@ -100,6 +107,8 @@ class AmneziaActivity : QtActivity() {
|
||||
private var pendingOpenFileUri: String? = null
|
||||
private var openFileDeliveryScheduled = false
|
||||
|
||||
private var updateCoverDialog: Dialog? = null
|
||||
|
||||
private val vpnServiceEventHandler: Handler by lazy(NONE) {
|
||||
object : Handler(Looper.getMainLooper()) {
|
||||
override fun handleMessage(msg: Message) {
|
||||
@@ -206,6 +215,7 @@ class AmneziaActivity : QtActivity() {
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
runBlocking { vpnProto = proto.await() }
|
||||
billingRepository = BillingPaymentRepository(applicationContext)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
@@ -487,6 +497,116 @@ class AmneziaActivity : QtActivity() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
fun showUpdateCover() {
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed || updateCoverDialog != null) return@runOnUiThread
|
||||
val dialog = Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
|
||||
dialog.setCancelable(false)
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
setBackgroundColor(0xFF0E0E11.toInt())
|
||||
}
|
||||
dialog.setContentView(root)
|
||||
dialog.show()
|
||||
updateCoverDialog = dialog
|
||||
}
|
||||
}
|
||||
|
||||
fun hideUpdateCover() {
|
||||
runOnUiThread {
|
||||
updateCoverDialog?.dismiss()
|
||||
updateCoverDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
fun showUpdatePrompt(title: String, message: String, updateTitle: String, skipTitle: String, storeUrl: String) {
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed) return@runOnUiThread
|
||||
|
||||
val dialog = updateCoverDialog ?: Dialog(this, android.R.style.Theme_Black_NoTitleBar_Fullscreen).also {
|
||||
it.setCancelable(false)
|
||||
it.show()
|
||||
updateCoverDialog = it
|
||||
}
|
||||
|
||||
val density = resources.displayMetrics.density
|
||||
fun dp(value: Int) = (value * density).toInt()
|
||||
|
||||
val root = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
setBackgroundColor(0xFF0E0E11.toInt())
|
||||
setPadding(dp(32), dp(32), dp(32), dp(32))
|
||||
}
|
||||
|
||||
val titleView = TextView(this).apply {
|
||||
text = title
|
||||
textSize = 22f
|
||||
setTextColor(0xFFFFFFFF.toInt())
|
||||
gravity = Gravity.CENTER
|
||||
typeface = Typeface.create(typeface, Typeface.BOLD)
|
||||
}
|
||||
|
||||
val messageView = TextView(this).apply {
|
||||
text = message
|
||||
textSize = 16f
|
||||
setTextColor(0xFFC7C8CB.toInt())
|
||||
gravity = Gravity.CENTER
|
||||
setPadding(0, dp(16), 0, dp(28))
|
||||
}
|
||||
|
||||
val updateButton = Button(this).apply {
|
||||
text = updateTitle
|
||||
isAllCaps = false
|
||||
textSize = 17f
|
||||
setTextColor(0xFF0E0E11.toInt())
|
||||
stateListAnimator = null
|
||||
background = GradientDrawable().apply {
|
||||
cornerRadius = dp(12).toFloat()
|
||||
setColor(0xFFFBB26A.toInt())
|
||||
}
|
||||
setOnClickListener {
|
||||
try {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(storeUrl)))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.w(TAG, "open store failed: ${e.message}")
|
||||
}
|
||||
hideUpdateCover()
|
||||
}
|
||||
}
|
||||
|
||||
val skipButton = Button(this).apply {
|
||||
text = skipTitle
|
||||
isAllCaps = false
|
||||
textSize = 17f
|
||||
setTextColor(0xFFD7D8DB.toInt())
|
||||
stateListAnimator = null
|
||||
background = GradientDrawable().apply {
|
||||
cornerRadius = dp(12).toFloat()
|
||||
setColor(0x00000000)
|
||||
setStroke(dp(1), 0xFF2C2D30.toInt())
|
||||
}
|
||||
setOnClickListener { hideUpdateCover() }
|
||||
}
|
||||
|
||||
val updateParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, dp(52)
|
||||
).apply { topMargin = dp(8) }
|
||||
|
||||
val skipParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, dp(52)
|
||||
).apply { topMargin = dp(12) }
|
||||
|
||||
root.addView(titleView)
|
||||
root.addView(messageView)
|
||||
root.addView(updateButton, updateParams)
|
||||
root.addView(skipButton, skipParams)
|
||||
|
||||
dialog.setContentView(root)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
Log.d(TAG, "Process activity result, code: ${actionCodeToString(requestCode)}, " +
|
||||
"resultCode: $resultCode, data: $data")
|
||||
@@ -999,15 +1119,9 @@ class AmneziaActivity : QtActivity() {
|
||||
@Suppress("unused")
|
||||
fun getAppList(): String {
|
||||
Log.v(TAG, "Get app list")
|
||||
var appList = ""
|
||||
runBlocking {
|
||||
mainScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
appList = AppListProvider.getAppList(packageManager, packageName)
|
||||
}
|
||||
}.join()
|
||||
return blockingCall(Dispatchers.IO) {
|
||||
AppListProvider.getAppList(packageManager, packageName)
|
||||
}
|
||||
return appList
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
@@ -1176,11 +1290,59 @@ class AmneziaActivity : QtActivity() {
|
||||
return super.dispatchTrackballEvent(ev)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun isPlay(): Boolean = BuildConfig.FLAVOR == "play"
|
||||
|
||||
@Suppress("unused")
|
||||
fun isTestPurchaseEnvironment(): Boolean {
|
||||
if (BuildConfig.DEBUG) return true
|
||||
val appInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||
return (appInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getCountryCode(): String {
|
||||
Log.v(TAG, "Get country code")
|
||||
return blockingCall { billingRepository.getCountryCode() }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getSubscriptionPlans(): String {
|
||||
Log.v(TAG, "Get subscription plans")
|
||||
return blockingCall { billingRepository.getSubscriptionPlans() }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun purchaseSubscription(offerToken: String): String {
|
||||
Log.v(TAG, "Purchase subscription")
|
||||
return blockingCall { billingRepository.purchaseSubscription(this@AmneziaActivity, offerToken) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun upgradeSubscription(offerToken: String, oldPurchaseToken: String): String {
|
||||
Log.v(TAG, "Upgrade subscription")
|
||||
return blockingCall {
|
||||
billingRepository.upgradeSubscription(this@AmneziaActivity, offerToken, oldPurchaseToken)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun acknowledgePurchase(purchaseToken: String): String {
|
||||
Log.v(TAG, "Acknowledge purchase")
|
||||
return blockingCall { billingRepository.acknowledge(purchaseToken) }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun queryPurchases(): String {
|
||||
Log.v(TAG, "Query purchases")
|
||||
return blockingCall { billingRepository.queryPurchases() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Utils methods
|
||||
*/
|
||||
private fun <T> blockingCall(
|
||||
context: CoroutineContext = Dispatchers.Main.immediate,
|
||||
context: CoroutineContext = Dispatchers.Default,
|
||||
block: suspend () -> T
|
||||
) = runBlocking {
|
||||
mainScope.async(context) { block() }.await()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.system.Os
|
||||
import androidx.camera.camera2.Camera2Config
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.CameraXConfig
|
||||
@@ -12,6 +13,9 @@ private const val TAG = "AmneziaApplication"
|
||||
class AmneziaApplication : QtApplication(), CameraXConfig.Provider {
|
||||
|
||||
override fun onCreate() {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Os.setenv("QT_ANDROID_DEBUGGER_MAIN_THREAD_SLEEP_MS", "0", true)
|
||||
}
|
||||
super.onCreate()
|
||||
Prefs.init(this)
|
||||
Log.init(this)
|
||||
|
||||
12
client/android/src/org/amnezia/vpn/BillingRepository.kt
Normal file
12
client/android/src/org/amnezia/vpn/BillingRepository.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package org.amnezia.vpn
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
interface BillingRepository {
|
||||
suspend fun getCountryCode(): String
|
||||
suspend fun getSubscriptionPlans(): String
|
||||
suspend fun purchaseSubscription(activity: Activity, offerToken: String): String
|
||||
suspend fun upgradeSubscription(activity: Activity, offerToken: String, oldPurchaseToken: String): String
|
||||
suspend fun acknowledge(purchaseToken: String): String
|
||||
suspend fun queryPurchases(): String
|
||||
}
|
||||
14
client/android/utils/src/main/kotlin/ErrorCode.kt
Normal file
14
client/android/utils/src/main/kotlin/ErrorCode.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package org.amnezia.vpn.util
|
||||
|
||||
// keep synchronized with client/core/defs.h error_code_ns::ErrorCode
|
||||
object ErrorCode {
|
||||
const val NoError = 0
|
||||
|
||||
const val BillingCanceled = 1300
|
||||
const val BillingError = 1301
|
||||
const val BillingGooglePlayError = 1302
|
||||
const val BillingUnavailable = 1303
|
||||
const val SubscriptionAlreadyOwned = 1304
|
||||
const val SubscriptionUnavailable = 1305
|
||||
const val BillingNetworkError = 1306
|
||||
}
|
||||
@@ -111,7 +111,7 @@ open class Wireguard : Protocol() {
|
||||
configExtensionParameters(configData)
|
||||
}
|
||||
|
||||
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it.toInt()) }
|
||||
configData.optStringOrNull("persistent_keep_alive")?.let { setPersistentKeepalive(it) }
|
||||
configData.getString("client_priv_key").let { setPrivateKeyHex(it.base64ToHex()) }
|
||||
configData.getString("server_pub_key").let { setPublicKeyHex(it.base64ToHex()) }
|
||||
configData.optStringOrNull("psk_key")?.let { setPreSharedKeyHex(it.base64ToHex()) }
|
||||
@@ -134,6 +134,20 @@ open class Wireguard : Protocol() {
|
||||
configData.optStringOrNull("I3")?.let { setI3(it) }
|
||||
configData.optStringOrNull("I4")?.let { setI4(it) }
|
||||
configData.optStringOrNull("I5")?.let { setI5(it) }
|
||||
configData.optStringOrNull("HeaderProtectionKey")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setHeaderProtectionKey(it.base64ToHex()) }
|
||||
configData.optStringOrNull("ContentPaddingAddition")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setContentPaddingAddition(it) }
|
||||
configData.optStringOrNull("RekeyAfterTime")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setRekeyAfterTime(it) }
|
||||
configData.optStringOrNull("RekeyTimeout")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setRekeyTimeout(it) }
|
||||
configData.optStringOrNull("RejectAfterTime")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setRejectAfterTime(it) }
|
||||
configData.optStringOrNull("KeepaliveTimeout")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setKeepaliveTimeout(it) }
|
||||
configData.optStringOrNull("MaxHandshakeAttempts")?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?.let { setMaxHandshakeAttempts(it) }
|
||||
}
|
||||
|
||||
private fun start(
|
||||
|
||||
@@ -10,7 +10,7 @@ private const val WIREGUARD_DEFAULT_MTU = 1280
|
||||
open class WireguardConfig protected constructor(
|
||||
protocolConfigBuilder: ProtocolConfig.Builder,
|
||||
val endpoint: InetEndpoint,
|
||||
val persistentKeepalive: Int,
|
||||
val persistentKeepalive: String?,
|
||||
val publicKeyHex: String,
|
||||
val preSharedKeyHex: String?,
|
||||
val privateKeyHex: String,
|
||||
@@ -31,6 +31,13 @@ open class WireguardConfig protected constructor(
|
||||
var i3: String?,
|
||||
var i4: String?,
|
||||
var i5: String?,
|
||||
val headerProtectionKeyHex: String?,
|
||||
val contentPaddingAddition: String?,
|
||||
val rekeyAfterTime: String?,
|
||||
val rekeyTimeout: String?,
|
||||
val rejectAfterTime: String?,
|
||||
val keepaliveTimeout: String?,
|
||||
val maxHandshakeAttempts: String?,
|
||||
) : ProtocolConfig(protocolConfigBuilder) {
|
||||
|
||||
protected constructor(builder: Builder) : this(
|
||||
@@ -57,6 +64,13 @@ open class WireguardConfig protected constructor(
|
||||
builder.i3,
|
||||
builder.i4,
|
||||
builder.i5,
|
||||
builder.headerProtectionKeyHex,
|
||||
builder.contentPaddingAddition,
|
||||
builder.rekeyAfterTime,
|
||||
builder.rekeyTimeout,
|
||||
builder.rejectAfterTime,
|
||||
builder.keepaliveTimeout,
|
||||
builder.maxHandshakeAttempts,
|
||||
)
|
||||
|
||||
fun toWgUserspaceString(): String = with(StringBuilder()) {
|
||||
@@ -87,6 +101,13 @@ open class WireguardConfig protected constructor(
|
||||
i4?.let { appendLine("i4=$it") }
|
||||
i5?.let { appendLine("i5=$it") }
|
||||
}
|
||||
headerProtectionKeyHex?.takeIf { it.isNotEmpty() }?.let { appendLine("header_protection_key=$it") }
|
||||
contentPaddingAddition?.takeIf { it.isNotEmpty() }?.let { appendLine("content_padding_addition=$it") }
|
||||
rekeyAfterTime?.takeIf { it.isNotEmpty() }?.let { appendLine("rekey_after_time=$it") }
|
||||
rekeyTimeout?.takeIf { it.isNotEmpty() }?.let { appendLine("rekey_timeout=$it") }
|
||||
rejectAfterTime?.takeIf { it.isNotEmpty() }?.let { appendLine("reject_after_time=$it") }
|
||||
keepaliveTimeout?.takeIf { it.isNotEmpty() }?.let { appendLine("keepalive_timeout=$it") }
|
||||
maxHandshakeAttempts?.takeIf { it.isNotEmpty() }?.let { appendLine("max_handshake_attempts=$it") }
|
||||
}
|
||||
|
||||
private fun validateProtocolExtensionParameters() {
|
||||
@@ -107,7 +128,7 @@ open class WireguardConfig protected constructor(
|
||||
appendLine("allowed_ip=${route.inetNetwork}")
|
||||
}
|
||||
appendLine("endpoint=$endpoint")
|
||||
if (persistentKeepalive != 0)
|
||||
if (!persistentKeepalive.isNullOrEmpty() && persistentKeepalive != "0")
|
||||
appendLine("persistent_keepalive_interval=$persistentKeepalive")
|
||||
if (preSharedKeyHex != null)
|
||||
appendLine("preshared_key=$preSharedKeyHex")
|
||||
@@ -117,7 +138,7 @@ open class WireguardConfig protected constructor(
|
||||
internal lateinit var endpoint: InetEndpoint
|
||||
private set
|
||||
|
||||
internal var persistentKeepalive: Int = 0
|
||||
internal var persistentKeepalive: String? = null
|
||||
private set
|
||||
|
||||
internal lateinit var publicKeyHex: String
|
||||
@@ -149,10 +170,17 @@ open class WireguardConfig protected constructor(
|
||||
internal var i3: String? = null
|
||||
internal var i4: String? = null
|
||||
internal var i5: String? = null
|
||||
internal var headerProtectionKeyHex: String? = null
|
||||
internal var contentPaddingAddition: String? = null
|
||||
internal var rekeyAfterTime: String? = null
|
||||
internal var rekeyTimeout: String? = null
|
||||
internal var rejectAfterTime: String? = null
|
||||
internal var keepaliveTimeout: String? = null
|
||||
internal var maxHandshakeAttempts: String? = null
|
||||
|
||||
fun setEndpoint(endpoint: InetEndpoint) = apply { this.endpoint = endpoint }
|
||||
|
||||
fun setPersistentKeepalive(persistentKeepalive: Int) = apply { this.persistentKeepalive = persistentKeepalive }
|
||||
fun setPersistentKeepalive(persistentKeepalive: String) = apply { this.persistentKeepalive = persistentKeepalive }
|
||||
|
||||
fun setPublicKeyHex(publicKeyHex: String) = apply { this.publicKeyHex = publicKeyHex }
|
||||
|
||||
@@ -178,6 +206,13 @@ open class WireguardConfig protected constructor(
|
||||
fun setI3(i3: String) = apply { this.i3 = i3 }
|
||||
fun setI4(i4: String) = apply { this.i4 = i4 }
|
||||
fun setI5(i5: String) = apply { this.i5 = i5 }
|
||||
fun setHeaderProtectionKey(headerProtectionKeyHex: String) = apply { this.headerProtectionKeyHex = headerProtectionKeyHex }
|
||||
fun setContentPaddingAddition(contentPaddingAddition: String) = apply { this.contentPaddingAddition = contentPaddingAddition }
|
||||
fun setRekeyAfterTime(rekeyAfterTime: String) = apply { this.rekeyAfterTime = rekeyAfterTime }
|
||||
fun setRekeyTimeout(rekeyTimeout: String) = apply { this.rekeyTimeout = rekeyTimeout }
|
||||
fun setRejectAfterTime(rejectAfterTime: String) = apply { this.rejectAfterTime = rejectAfterTime }
|
||||
fun setKeepaliveTimeout(keepaliveTimeout: String) = apply { this.keepaliveTimeout = keepaliveTimeout }
|
||||
fun setMaxHandshakeAttempts(maxHandshakeAttempts: String) = apply { this.maxHandshakeAttempts = maxHandshakeAttempts }
|
||||
|
||||
override fun build(): WireguardConfig = configBuild().run { WireguardConfig(this@Builder) }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -53,3 +57,43 @@ file(COPY ${AMNEZIA_LIBXRAY_PATH} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/androi
|
||||
find_package(openvpn-pt-android REQUIRED)
|
||||
set(LIBS ${LIBS} amnezia::openvpn-pt-android)
|
||||
set_property(TARGET ${PROJECT} APPEND PROPERTY QT_ANDROID_EXTRA_LIBS ${OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH})
|
||||
|
||||
if(QT_USE_TARGET_ANDROID_BUILD_DIR)
|
||||
set(_android_build_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build-${PROJECT}")
|
||||
else()
|
||||
set(_android_build_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build")
|
||||
endif()
|
||||
|
||||
add_custom_target(android_gradle_clean
|
||||
COMMAND ./gradlew clean
|
||||
WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Cleaning Android Gradle build cache"
|
||||
)
|
||||
|
||||
# Always-available debug target: build Play Debug APK and copy to standard output path
|
||||
# so Qt Creator's deploy step picks it up automatically
|
||||
add_custom_target(android_play_debug_install
|
||||
COMMAND ./gradlew assemblePlayDebug
|
||||
COMMAND sh -c "cp build/outputs/apk/play/debug/*.apk build/outputs/apk/android-build-${PROJECT}-debug.apk"
|
||||
WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Building Android Play Debug APK and copying to deploy path"
|
||||
DEPENDS ${PROJECT}
|
||||
)
|
||||
|
||||
if(ANDROID_BUILD_PLAY)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(_gradle_suffix "Debug")
|
||||
else()
|
||||
set(_gradle_suffix "Release")
|
||||
endif()
|
||||
add_custom_target(android_play_apk
|
||||
COMMAND ./gradlew assemblePlay${_gradle_suffix} WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Building Android Play APK (assemblePlay${_gradle_suffix})"
|
||||
DEPENDS ${PROJECT}
|
||||
)
|
||||
add_custom_target(android_play_aab
|
||||
COMMAND ./gradlew bundlePlay${_gradle_suffix} WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Building Android Play AAB (bundlePlay${_gradle_suffix})"
|
||||
DEPENDS ${PROJECT}
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -133,6 +133,11 @@ target_sources(${PROJECT} PRIVATE
|
||||
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||
)
|
||||
|
||||
set_source_files_properties(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Images.xcassets
|
||||
PROPERTIES MACOSX_PACKAGE_LOCATION Resources
|
||||
)
|
||||
|
||||
target_sources(${PROJECT} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/macos/app/Images.xcassets
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/ios/app/PrivacyInfo.xcprivacy
|
||||
|
||||
@@ -266,7 +266,7 @@ ProtocolConfig WireguardConfigurator::createConfig(const ServerCredentials &cred
|
||||
clientConfig.presharedKey = connData.pskKey;
|
||||
clientConfig.clientId = connData.clientPubKey;
|
||||
clientConfig.allowedIps = QStringList { "0.0.0.0/0", "::/0" };
|
||||
clientConfig.persistentKeepAlive = "25";
|
||||
clientConfig.persistentKeepAlive = protocols::wireguard::defaultPersistentKeepAlive;
|
||||
clientConfig.mtu = mtu;
|
||||
clientConfig.isObfuscationEnabled = false;
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#include <QFutureWatcher>
|
||||
#include <QtConcurrent>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
@@ -38,8 +42,8 @@ namespace
|
||||
constexpr char amneziaPremium[] = "amnezia-premium";
|
||||
}
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
struct StoreKitPlanQuote {
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE) || defined(Q_OS_ANDROID)
|
||||
struct SubscriptionPlanQuote {
|
||||
QString displayPrice;
|
||||
double priceAmount = 0.0;
|
||||
double subscriptionBillingMonths = 0.0;
|
||||
@@ -76,48 +80,9 @@ namespace
|
||||
return productIds;
|
||||
}
|
||||
|
||||
QHash<QString, StoreKitPlanQuote> buildStoreKitQuoteMap(const QList<QVariantMap> &fetchedProducts)
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
QHash<QString, SubscriptionPlanQuote> buildStoreKitQuoteMap(const QStringList &productIds)
|
||||
{
|
||||
QHash<QString, StoreKitPlanQuote> quotesByProductId;
|
||||
quotesByProductId.reserve(fetchedProducts.size());
|
||||
|
||||
for (const QVariantMap &productInfo : fetchedProducts) {
|
||||
const QString productId = productInfo.value(QStringLiteral("productId")).toString();
|
||||
if (productId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString displayPrice = productInfo.value(QStringLiteral("displayPrice")).toString();
|
||||
if (displayPrice.isEmpty()) {
|
||||
const QString price = productInfo.value(QStringLiteral("price")).toString();
|
||||
const QString currencyCode = productInfo.value(QStringLiteral("currencyCode")).toString();
|
||||
displayPrice = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
||||
}
|
||||
|
||||
StoreKitPlanQuote quote;
|
||||
quote.displayPrice = displayPrice;
|
||||
quote.priceAmount = productInfo.value(QStringLiteral("priceAmount")).toDouble();
|
||||
quote.subscriptionBillingMonths = productInfo.value(QStringLiteral("subscriptionBillingMonths")).toDouble();
|
||||
quote.displayPricePerMonth = productInfo.value(QStringLiteral("displayPricePerMonth")).toString();
|
||||
quotesByProductId.insert(productId, quote);
|
||||
}
|
||||
|
||||
return quotesByProductId;
|
||||
}
|
||||
|
||||
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
||||
{
|
||||
QJsonArray services = data.value(apiDefs::key::services).toArray();
|
||||
if (services.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QStringList productIds = collectPremiumStoreProductIds(services);
|
||||
if (productIds.isEmpty()) {
|
||||
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
|
||||
return;
|
||||
}
|
||||
|
||||
QList<QVariantMap> fetchedProducts;
|
||||
QEventLoop loop;
|
||||
IosController::Instance()->fetchProducts(productIds,
|
||||
@@ -134,8 +99,99 @@ namespace
|
||||
});
|
||||
loop.exec();
|
||||
|
||||
const QHash<QString, StoreKitPlanQuote> quotesByProductId = buildStoreKitQuoteMap(fetchedProducts);
|
||||
QHash<QString, SubscriptionPlanQuote> quotesByProductId;
|
||||
quotesByProductId.reserve(fetchedProducts.size());
|
||||
|
||||
for (const QVariantMap &productInfo : fetchedProducts) {
|
||||
const QString productId = productInfo.value(QStringLiteral("productId")).toString();
|
||||
if (productId.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString displayPrice = productInfo.value(QStringLiteral("displayPrice")).toString();
|
||||
if (displayPrice.isEmpty()) {
|
||||
const QString price = productInfo.value(QStringLiteral("price")).toString();
|
||||
const QString currencyCode = productInfo.value(QStringLiteral("currencyCode")).toString();
|
||||
displayPrice = currencyCode.isEmpty() ? price : (price + QLatin1Char(' ') + currencyCode);
|
||||
}
|
||||
|
||||
SubscriptionPlanQuote quote;
|
||||
quote.displayPrice = displayPrice;
|
||||
quote.priceAmount = productInfo.value(QStringLiteral("priceAmount")).toDouble();
|
||||
quote.subscriptionBillingMonths = productInfo.value(QStringLiteral("subscriptionBillingMonths")).toDouble();
|
||||
quote.displayPricePerMonth = productInfo.value(QStringLiteral("displayPricePerMonth")).toString();
|
||||
quotesByProductId.insert(productId, quote);
|
||||
}
|
||||
|
||||
qInfo().noquote() << "[IAP] Built StoreKit quote map, quotes:" << quotesByProductId.size();
|
||||
return quotesByProductId;
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
QHash<QString, SubscriptionPlanQuote> buildGooglePlayQuoteMap(const QStringList &productIds)
|
||||
{
|
||||
Q_UNUSED(productIds); // Google Play returns every offer for the app's single product in one call
|
||||
|
||||
auto androidController = AndroidController::instance();
|
||||
QFutureWatcher<QJsonObject> watcher;
|
||||
QEventLoop waitLoop;
|
||||
QObject::connect(&watcher, &QFutureWatcher<QJsonObject>::finished, &waitLoop, &QEventLoop::quit);
|
||||
|
||||
QFuture<QJsonObject> future = QtConcurrent::run([androidController]() {
|
||||
return androidController->getSubscriptionPlans();
|
||||
});
|
||||
watcher.setFuture(future);
|
||||
waitLoop.exec();
|
||||
|
||||
QJsonObject plansResult = watcher.result();
|
||||
QHash<QString, SubscriptionPlanQuote> quotesByProductId;
|
||||
if (plansResult.value("responseCode").toInt(-1) != 0) {
|
||||
qWarning() << "[Billing] Failed to get subscription plans for price display, responseCode:"
|
||||
<< plansResult.value("responseCode").toInt(-1);
|
||||
return quotesByProductId;
|
||||
}
|
||||
qInfo() << "[Billing] Fetched subscription plans for price display";
|
||||
|
||||
const QJsonArray products = plansResult.value("products").toArray();
|
||||
for (const QJsonValue &productValue : products) {
|
||||
const QJsonArray offers = productValue.toObject().value("offers").toArray();
|
||||
for (const QJsonValue &offerValue : offers) {
|
||||
const QJsonObject offer = offerValue.toObject();
|
||||
const QString basePlanId = offer.value("basePlanId").toString();
|
||||
if (basePlanId.isEmpty() || quotesByProductId.contains(basePlanId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QJsonArray pricingPhases = offer.value("pricingPhases").toArray();
|
||||
if (pricingPhases.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
// Last phase is always the ongoing recurring price, after any trial/intro phases
|
||||
const QJsonObject regularPhase = pricingPhases.last().toObject();
|
||||
|
||||
SubscriptionPlanQuote quote;
|
||||
quote.displayPrice = regularPhase.value("formatedPrice").toString();
|
||||
if (quote.displayPrice.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
quote.priceAmount = regularPhase.value("priceAmountMicros").toDouble() / 1000000.0;
|
||||
quote.subscriptionBillingMonths = regularPhase.value("subscriptionBillingMonths").toDouble();
|
||||
quote.displayPricePerMonth = regularPhase.value("displayPricePerMonth").toString();
|
||||
quotesByProductId.insert(basePlanId, quote);
|
||||
}
|
||||
}
|
||||
qInfo() << "[Billing] Built Google Play quote map, quotes:" << quotesByProductId.size();
|
||||
return quotesByProductId;
|
||||
}
|
||||
#endif
|
||||
|
||||
void mergeQuotesIntoPremiumPlans(QJsonObject &data, const QHash<QString, SubscriptionPlanQuote> "esByProductId)
|
||||
{
|
||||
QJsonArray services = data.value(apiDefs::key::services).toArray();
|
||||
if (services.isEmpty() || quotesByProductId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int mergedPlanCount = 0;
|
||||
for (int serviceIndex = 0; serviceIndex < services.size(); ++serviceIndex) {
|
||||
QJsonObject serviceObject = services.at(serviceIndex).toObject();
|
||||
if (serviceObject.value(apiDefs::key::serviceType).toString() != serviceType::amneziaPremium) {
|
||||
@@ -166,8 +222,9 @@ namespace
|
||||
}
|
||||
|
||||
const bool isTrialPlan = planObject.value(configKey::isTrial).toBool();
|
||||
const StoreKitPlanQuote "e = *quoteIterator;
|
||||
const SubscriptionPlanQuote "e = *quoteIterator;
|
||||
planObject.insert(configKey::priceLabel, quote.displayPrice);
|
||||
++mergedPlanCount;
|
||||
|
||||
const double months = quote.subscriptionBillingMonths;
|
||||
if (!isTrialPlan && months > oneMonthThreshold && !quote.displayPricePerMonth.isEmpty()) {
|
||||
@@ -200,9 +257,32 @@ namespace
|
||||
serviceObject.insert(configKey::serviceDescription, descriptionObject);
|
||||
services.replace(serviceIndex, serviceObject);
|
||||
}
|
||||
qInfo().noquote() << "[IAP] Merged store quotes into" << mergedPlanCount << "premium plan(s)";
|
||||
data.insert(apiDefs::key::services, services);
|
||||
}
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
void mergeStoreKitPricesIntoPremiumPlans(QJsonObject &data)
|
||||
{
|
||||
const QStringList productIds = collectPremiumStoreProductIds(data.value(apiDefs::key::services).toArray());
|
||||
if (productIds.isEmpty()) {
|
||||
qInfo().noquote() << "[IAP] No store_product_id in premium plans; skip StoreKit merge into services payload";
|
||||
return;
|
||||
}
|
||||
mergeQuotesIntoPremiumPlans(data, buildStoreKitQuoteMap(productIds));
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
void mergeGooglePlayPricesIntoPremiumPlans(QJsonObject &data)
|
||||
{
|
||||
const QStringList productIds = collectPremiumStoreProductIds(data.value(apiDefs::key::services).toArray());
|
||||
if (productIds.isEmpty()) {
|
||||
qInfo().noquote() << "[Billing] No store_product_id in premium plans; skip Google Play merge into services payload";
|
||||
return;
|
||||
}
|
||||
mergeQuotesIntoPremiumPlans(data, buildGooglePlayQuoteMap(productIds));
|
||||
}
|
||||
#endif
|
||||
#endif // Q_OS_IOS || MACOS_NE || Q_OS_ANDROID
|
||||
}
|
||||
|
||||
ServicesCatalogController::ServicesCatalogController(SecureAppSettingsRepository* appSettingsRepository)
|
||||
@@ -220,6 +300,8 @@ ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &services
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
|
||||
qWarning() << "[ServicesCatalog] errorCode:" << static_cast<int>(errorCode)
|
||||
<< "response:" << QString::fromLocal8Bit(responseBody);
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
if (!responseBody.contains(apiDefs::key::services.data())) {
|
||||
errorCode = ErrorCode::ApiServicesMissingError;
|
||||
@@ -234,6 +316,8 @@ ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &services
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
mergeStoreKitPricesIntoPremiumPlans(servicesData);
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
mergeGooglePlayPricesIntoPremiumPlans(servicesData);
|
||||
#endif
|
||||
|
||||
return ErrorCode::NoError;
|
||||
@@ -241,7 +325,10 @@ ErrorCode ServicesCatalogController::fillAvailableServices(QJsonObject &services
|
||||
|
||||
ErrorCode ServicesCatalogController::executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody)
|
||||
{
|
||||
GatewayController gatewayController(m_appSettingsRepository->getGatewayEndpoint(), m_appSettingsRepository->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||
QString gatewayEndpoint = m_appSettingsRepository->getGatewayEndpoint();
|
||||
qWarning() << "[ServicesCatalog] request URL:" << endpoint.arg(gatewayEndpoint)
|
||||
<< "isDevEnv:" << m_appSettingsRepository->isDevGatewayEnv();
|
||||
GatewayController gatewayController(gatewayEndpoint, m_appSettingsRepository->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
|
||||
m_appSettingsRepository->isStrictKillSwitchEnabled(), m_appSettingsRepository);
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#include <AmneziaVPN-Swift.h>
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#include <QtConcurrent>
|
||||
#endif
|
||||
|
||||
using namespace amnezia;
|
||||
@@ -127,6 +130,9 @@ QJsonObject SubscriptionController::GatewayRequestData::toJsonObject() const
|
||||
if (!authData.isEmpty()) {
|
||||
obj[apiDefs::key::authData] = authData;
|
||||
}
|
||||
if (!transactionId.isEmpty()) {
|
||||
obj[apiDefs::key::transactionId] = transactionId;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -153,7 +159,7 @@ void SubscriptionController::appendProtocolDataToApiPayload(const QString &proto
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
|
||||
ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByteArray &apiResponseBody, const QString &protocol,
|
||||
const ProtocolData &protocolData, QJsonObject &serverConfigJson)
|
||||
{
|
||||
QString data = QJsonDocument::fromJson(apiResponseBody).object().value(configKey::config).toString();
|
||||
@@ -205,6 +211,7 @@ ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByt
|
||||
serverProtocolConfig[configKey::specialJunk3] = clientProtocolConfig.value(configKey::specialJunk3);
|
||||
serverProtocolConfig[configKey::specialJunk4] = clientProtocolConfig.value(configKey::specialJunk4);
|
||||
serverProtocolConfig[configKey::specialJunk5] = clientProtocolConfig.value(configKey::specialJunk5);
|
||||
serverProtocolConfig[configKey::headerProtectionKey] = clientProtocolConfig.value(configKey::headerProtectionKey);
|
||||
|
||||
//
|
||||
|
||||
@@ -218,23 +225,23 @@ ErrorCode SubscriptionController::extractServerConfigJsonFromResponse(const QByt
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson, const QString &serviceType,
|
||||
void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &userCountryCode,
|
||||
const QByteArray &apiResponseBody)
|
||||
{
|
||||
QJsonObject apiConfig = serverConfigJson.value(apiDefs::key::apiConfig).toObject();
|
||||
|
||||
|
||||
apiConfig[apiDefs::key::serviceType] = serviceType;
|
||||
apiConfig[apiDefs::key::serviceProtocol] = serviceProtocol;
|
||||
apiConfig[apiDefs::key::userCountryCode] = userCountryCode;
|
||||
|
||||
|
||||
if (serverConfigJson.value(configKey::configVersion).toInt() == serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
QJsonObject responseObj = QJsonDocument::fromJson(apiResponseBody).object();
|
||||
if (responseObj.contains(apiDefs::key::serviceInfo)) {
|
||||
apiConfig.insert(apiDefs::key::serviceInfo, responseObj.value(apiDefs::key::serviceInfo).toObject());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
serverConfigJson[apiDefs::key::apiConfig] = apiConfig;
|
||||
}
|
||||
|
||||
@@ -245,6 +252,26 @@ ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const
|
||||
return gatewayController.post(endpoint, apiPayload, responseBody);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::getSubscriptionInfo(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &purchaseToken,
|
||||
QByteArray &responseBody)
|
||||
{
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
|
||||
m_appSettingsRepository->getInstallationUuid(true),
|
||||
userCountryCode,
|
||||
"",
|
||||
serviceType,
|
||||
serviceProtocol,
|
||||
QJsonObject(),
|
||||
purchaseToken };
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
qWarning() << "[Billing][getSubscriptionInfo] request:" << QJsonDocument(apiPayload).toJson(QJsonDocument::Compact);
|
||||
return executeRequest(QString("%1v1/get_subscription_info"), apiPayload, responseBody, false);
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
CaptchaInfo &captchaInfo)
|
||||
@@ -349,10 +376,10 @@ ErrorCode SubscriptionController::importTrialFromGateway(const QString &userCoun
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
|
||||
ErrorCode SubscriptionController::importServiceFromMarket(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
int *duplicateServerIndex)
|
||||
int *duplicateServerIndex, const QString &endpoint)
|
||||
{
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
@@ -362,14 +389,16 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
|
||||
"",
|
||||
serviceType,
|
||||
serviceProtocol,
|
||||
QJsonObject() };
|
||||
QJsonObject(),
|
||||
transactionId};
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||
apiPayload[apiDefs::key::transactionId] = transactionId;
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
||||
qWarning() << "[Billing][importServiceFromMarket] endpoint:" << endpoint << "isTestPurchase:" << isTestPurchase;
|
||||
ErrorCode errorCode = executeRequest(QString("%1") + endpoint, apiPayload, responseBody, isTestPurchase);
|
||||
qWarning() << "[Billing][importServiceFromMarket] errorCode:" << static_cast<int>(errorCode) << "response:" << responseBody;
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
return errorCode;
|
||||
}
|
||||
@@ -413,7 +442,7 @@ ErrorCode SubscriptionController::importServiceFromAppStore(const QString &userC
|
||||
QJsonObject configObject = QJsonDocument::fromJson(configString).object();
|
||||
|
||||
quint16 crc = qChecksum(QJsonDocument(configObject).toJson());
|
||||
|
||||
|
||||
if (configObject.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
@@ -599,7 +628,7 @@ ErrorCode SubscriptionController::deactivateDevice(const QString &serverId)
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
if (!apiV2->isPremium() && !apiV2->isExternalPremium()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
@@ -636,7 +665,7 @@ ErrorCode SubscriptionController::deactivateExternalDevice(const QString &server
|
||||
if (!apiV2.has_value()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
|
||||
|
||||
if (!apiV2->isPremium() && !apiV2->isExternalPremium()) {
|
||||
return ErrorCode::NoError;
|
||||
}
|
||||
@@ -833,7 +862,7 @@ bool SubscriptionController::isApiKeyExpired(const QString &serverId) const
|
||||
return false;
|
||||
}
|
||||
const QString expiresAt = apiV2->apiConfig.publicKey.expiresAt;
|
||||
|
||||
|
||||
if (expiresAt.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -842,7 +871,7 @@ bool SubscriptionController::isApiKeyExpired(const QString &serverId) const
|
||||
if (expiresAtDateTime < QDateTime::currentDateTimeUtc()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -926,8 +955,133 @@ ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCou
|
||||
bool isTestPurchase = IosController::Instance()->isTestFlight();
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
return importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
originalTransactionId, isTestPurchase, duplicateServerIndex);
|
||||
return importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
originalTransactionId, isTestPurchase, duplicateServerIndex,
|
||||
QStringLiteral("v1/subscriptions"));
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
Q_UNUSED(serviceProtocol);
|
||||
Q_UNUSED(productId);
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
#endif
|
||||
}
|
||||
|
||||
ErrorCode SubscriptionController::processPlayMarketPurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex)
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
auto androidController = AndroidController::instance();
|
||||
QString purchaseToken;
|
||||
bool purchaseOk = false;
|
||||
|
||||
QFutureWatcher<QPair<bool, QString>> watcher;
|
||||
QEventLoop waitLoop;
|
||||
QObject::connect(&watcher, &QFutureWatcher<QPair<bool, QString>>::finished, &waitLoop, &QEventLoop::quit);
|
||||
|
||||
QFuture<QPair<bool, QString>> future = QtConcurrent::run([androidController, productId]() {
|
||||
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;
|
||||
QString fallbackOfferToken;
|
||||
for (const QJsonValue &productValue : products) {
|
||||
QJsonObject product = productValue.toObject();
|
||||
QJsonArray offers = product.value("offers").toArray();
|
||||
for (const QJsonValue &offerValue : offers) {
|
||||
QJsonObject offer = offerValue.toObject();
|
||||
if (offer.value("basePlanId").toString() != productId) continue;
|
||||
|
||||
const QString token = offer.value("offerToken").toString();
|
||||
if (fallbackOfferToken.isEmpty()) fallbackOfferToken = token;
|
||||
|
||||
QJsonArray pricingPhases = offer.value("pricingPhases").toArray();
|
||||
const bool hasFreeTrial = !pricingPhases.isEmpty()
|
||||
&& pricingPhases.first().toObject().value("priceAmountMicros").toDouble() == 0;
|
||||
if (hasFreeTrial) {
|
||||
offerToken = token;
|
||||
qInfo() << "[Billing] Found free trial offer for basePlanId:" << productId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!offerToken.isEmpty()) break;
|
||||
}
|
||||
if (offerToken.isEmpty()) offerToken = fallbackOfferToken;
|
||||
if (offerToken.isEmpty()) {
|
||||
qWarning() << "[Billing] No offer token found for basePlanId:" << productId;
|
||||
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();
|
||||
int purchaseState = purchase.value("purchaseState").toInt(-1);
|
||||
qInfo() << "[Billing] Purchase success. purchaseToken:" << token << "isAcknowledged:" << isAcknowledged << "purchaseState:" << purchaseState;
|
||||
// purchaseState 1 = PURCHASED, 0 = PENDING (user must confirm payment in Google Play)
|
||||
if (purchaseState != 1) {
|
||||
qWarning() << "[Billing] Purchase is in PENDING state, waiting for user to confirm payment";
|
||||
return qMakePair(false, QStringLiteral("pending"));
|
||||
}
|
||||
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) {
|
||||
if (purchaseToken == QStringLiteral("pending")) {
|
||||
return ErrorCode::ApiPurchasePendingError;
|
||||
}
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
if (purchaseToken.isEmpty()) {
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
// First call: determine if this is a test purchase
|
||||
QByteArray checkResponse;
|
||||
ErrorCode checkError = getSubscriptionInfo(userCountryCode, serviceType, serviceProtocol, purchaseToken, checkResponse);
|
||||
qWarning() << "[Billing][processPlayMarketPurchase] getSubscriptionInfo errorCode:" << static_cast<int>(checkError) << "response:" << checkResponse;
|
||||
if (checkError != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[Billing] Initial subscriptions check failed:" << static_cast<int>(checkError);
|
||||
return checkError;
|
||||
}
|
||||
|
||||
QJsonObject checkObject = QJsonDocument::fromJson(checkResponse).object();
|
||||
bool isTestPurchase = checkObject.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||
qInfo().noquote() << "[Billing] Purchase isTestPurchase =" << isTestPurchase;
|
||||
|
||||
// Second call: import service with correct isTestPurchase flag
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
return importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
purchaseToken, isTestPurchase, duplicateServerIndex,
|
||||
QStringLiteral("v1/subscriptions"));
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
@@ -948,6 +1102,9 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
QString restoreError;
|
||||
QEventLoop waitRestore;
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Starting restore. userCountryCode=" << userCountryCode
|
||||
<< "serviceType=" << serviceType << "serviceProtocol=" << serviceProtocol;
|
||||
|
||||
IosController::Instance()->restorePurchases([&](bool success, const QList<QVariantMap> &transactions, const QString &errorString) {
|
||||
restoreSuccess = success;
|
||||
restoredTransactions = transactions;
|
||||
@@ -956,6 +1113,10 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
});
|
||||
waitRestore.exec();
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] restorePurchases result: success=" << restoreSuccess
|
||||
<< "transactions count=" << restoredTransactions.size()
|
||||
<< "error=" << restoreError;
|
||||
|
||||
if (!restoreSuccess) {
|
||||
qWarning().noquote() << "[IAP] Restore failed:" << restoreError;
|
||||
result.errorCode = ErrorCode::ApiPurchaseError;
|
||||
@@ -969,6 +1130,7 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
}
|
||||
|
||||
bool isTestPurchase = IosController::Instance()->isTestFlight();
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] isTestFlight=" << isTestPurchase;
|
||||
QSet<QString> processedTransactions;
|
||||
|
||||
for (const QVariantMap &transaction : restoredTransactions) {
|
||||
@@ -976,6 +1138,9 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
|
||||
const QString transactionProductId = transaction.value(QStringLiteral("productId")).toString();
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Processing transaction: transactionId=" << transactionId
|
||||
<< "originalTransactionId=" << originalTransactionId << "productId=" << transactionProductId;
|
||||
|
||||
if (originalTransactionId.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Skipping restored transaction without originalTransactionId" << transactionId;
|
||||
continue;
|
||||
@@ -983,6 +1148,7 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
|
||||
if (processedTransactions.contains(originalTransactionId)) {
|
||||
result.duplicateCount++;
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Skipping duplicate originalTransactionId=" << originalTransactionId;
|
||||
continue;
|
||||
}
|
||||
processedTransactions.insert(originalTransactionId);
|
||||
@@ -992,9 +1158,13 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
int currentDuplicateServerIndex = -1;
|
||||
ErrorCode errorCode = importServiceFromAppStore(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
ErrorCode errorCode = importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
originalTransactionId, isTestPurchase,
|
||||
¤tDuplicateServerIndex);
|
||||
¤tDuplicateServerIndex,
|
||||
QStringLiteral("v1/restore_subscription"));
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] importServiceFromMarket errorCode=" << static_cast<int>(errorCode)
|
||||
<< "for originalTransactionId=" << originalTransactionId;
|
||||
|
||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
result.duplicateConfigAlreadyPresent = true;
|
||||
@@ -1004,7 +1174,8 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId
|
||||
<< "because subscription config with the same vpn_key already exists";
|
||||
} else if (errorCode != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId;
|
||||
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId
|
||||
<< "errorCode=" << static_cast<int>(errorCode);
|
||||
result.errorCode = errorCode;
|
||||
} else {
|
||||
result.hasInstalledConfig = true;
|
||||
@@ -1015,6 +1186,132 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
result.errorCode = result.duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
qInfo().noquote() << "[IAP][processAppStoreRestore] Done. hasInstalledConfig=" << result.hasInstalledConfig
|
||||
<< "duplicateConfigAlreadyPresent=" << result.duplicateConfigAlreadyPresent
|
||||
<< "duplicateCount=" << result.duplicateCount
|
||||
<< "errorCode=" << static_cast<int>(result.errorCode);
|
||||
|
||||
return result;
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
Q_UNUSED(serviceType);
|
||||
Q_UNUSED(serviceProtocol);
|
||||
result.errorCode = ErrorCode::ApiPurchaseError;
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
SubscriptionController::PlayMarketRestoreResult SubscriptionController::processPlayMarketRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol)
|
||||
{
|
||||
PlayMarketRestoreResult result;
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
auto androidController = AndroidController::instance();
|
||||
|
||||
QJsonObject purchasesResult;
|
||||
{
|
||||
QFutureWatcher<QJsonObject> queryWatcher;
|
||||
QEventLoop queryLoop;
|
||||
QObject::connect(&queryWatcher, &QFutureWatcher<QJsonObject>::finished, &queryLoop, &QEventLoop::quit);
|
||||
QFuture<QJsonObject> queryFuture = QtConcurrent::run([androidController]() {
|
||||
return androidController->queryPurchases();
|
||||
});
|
||||
queryWatcher.setFuture(queryFuture);
|
||||
queryLoop.exec();
|
||||
purchasesResult = queryWatcher.result();
|
||||
}
|
||||
|
||||
int responseCode = purchasesResult.value("responseCode").toInt(-1);
|
||||
if (responseCode != 0) {
|
||||
qWarning().noquote() << "[Billing] queryPurchases failed, responseCode =" << responseCode;
|
||||
result.errorCode = ErrorCode::ApiPurchaseError;
|
||||
return result;
|
||||
}
|
||||
|
||||
QJsonArray purchases = purchasesResult.value("purchases").toArray();
|
||||
if (purchases.isEmpty()) {
|
||||
qInfo().noquote() << "[Billing] Restore completed, but no purchases were found";
|
||||
result.errorCode = ErrorCode::ApiNoPurchasesToRestore;
|
||||
return result;
|
||||
}
|
||||
|
||||
QSet<QString> processedTokens;
|
||||
for (const QJsonValue &purchaseValue : std::as_const(purchases)) {
|
||||
const QJsonObject purchaseObj = purchaseValue.toObject();
|
||||
const QString purchaseToken = purchaseObj.value("purchaseToken").toString();
|
||||
|
||||
if (purchaseToken.isEmpty()) {
|
||||
qWarning().noquote() << "[Billing] Skipping purchase without purchaseToken";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processedTokens.contains(purchaseToken)) {
|
||||
result.duplicateCount++;
|
||||
continue;
|
||||
}
|
||||
processedTokens.insert(purchaseToken);
|
||||
|
||||
qInfo().noquote() << "[Billing] Restoring subscription with purchaseToken =" << purchaseToken;
|
||||
|
||||
{
|
||||
QFutureWatcher<QJsonObject> ackWatcher;
|
||||
QEventLoop ackLoop;
|
||||
QObject::connect(&ackWatcher, &QFutureWatcher<QJsonObject>::finished, &ackLoop, &QEventLoop::quit);
|
||||
QFuture<QJsonObject> ackFuture = QtConcurrent::run([androidController, purchaseToken]() {
|
||||
return androidController->acknowledgePurchase(purchaseToken);
|
||||
});
|
||||
ackWatcher.setFuture(ackFuture);
|
||||
ackLoop.exec();
|
||||
QJsonObject ackResult = ackWatcher.result();
|
||||
int ackCode = ackResult.value("responseCode").toInt(-1);
|
||||
if (ackCode != 0) {
|
||||
qWarning().noquote() << "[Billing] acknowledgePurchase failed, responseCode =" << ackCode;
|
||||
} else {
|
||||
qInfo().noquote() << "[Billing] Purchase acknowledged successfully";
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray checkResponse;
|
||||
ErrorCode checkError = getSubscriptionInfo(userCountryCode, serviceType, serviceProtocol, purchaseToken, checkResponse);
|
||||
qWarning() << "[Billing][processPlayMarketRestore] getSubscriptionInfo errorCode:" << static_cast<int>(checkError) << "response:" << checkResponse;
|
||||
if (checkError != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[Billing] Initial subscriptions check failed:" << static_cast<int>(checkError);
|
||||
result.errorCode = checkError;
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject checkObject = QJsonDocument::fromJson(checkResponse).object();
|
||||
bool isTestPurchase = checkObject.value(apiDefs::key::isTestPurchase).toBool(false);
|
||||
qInfo().noquote() << "[Billing] Purchase isTestPurchase =" << isTestPurchase;
|
||||
|
||||
ProtocolData protocolData = generateProtocolData(serviceProtocol);
|
||||
int currentDuplicateServerIndex = -1;
|
||||
ErrorCode errorCode = importServiceFromMarket(userCountryCode, serviceType, serviceProtocol, protocolData,
|
||||
purchaseToken, isTestPurchase,
|
||||
¤tDuplicateServerIndex,
|
||||
QStringLiteral("v1/restore_subscription"));
|
||||
|
||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
result.duplicateConfigAlreadyPresent = true;
|
||||
if (result.duplicateServerIndex < 0) {
|
||||
result.duplicateServerIndex = currentDuplicateServerIndex;
|
||||
}
|
||||
qInfo().noquote() << "[Billing] Skipping purchase" << purchaseToken
|
||||
<< "because subscription config with the same vpn_key already exists";
|
||||
} else if (errorCode != ErrorCode::NoError) {
|
||||
qWarning().noquote() << "[Billing] Failed to process restored subscription for purchaseToken =" << purchaseToken
|
||||
<< "errorCode =" << static_cast<int>(errorCode);
|
||||
result.errorCode = errorCode;
|
||||
} else {
|
||||
result.hasInstalledConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.hasInstalledConfig) {
|
||||
result.errorCode = result.duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiNoPurchasesToRestore;
|
||||
}
|
||||
|
||||
return result;
|
||||
#else
|
||||
Q_UNUSED(userCountryCode);
|
||||
@@ -1027,12 +1324,14 @@ SubscriptionController::AppStoreRestoreResult SubscriptionController::processApp
|
||||
|
||||
ErrorCode SubscriptionController::getAccountInfo(const QString &serverId, QJsonObject &accountInfo)
|
||||
{
|
||||
auto apiV2 = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2.has_value()) {
|
||||
auto apiV2Opt = m_serversRepository->apiV2Config(serverId);
|
||||
if (!apiV2Opt.has_value()) {
|
||||
return ErrorCode::InternalError;
|
||||
}
|
||||
|
||||
const ApiV2ServerConfig* apiV2 = &apiV2Opt.value();
|
||||
bool isTestPurchase = apiV2->apiConfig.isTestPurchase;
|
||||
|
||||
|
||||
QJsonObject authDataJson = apiV2->authData.toJson();
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
|
||||
@@ -38,6 +38,7 @@ public:
|
||||
QString serviceType;
|
||||
QString serviceProtocol;
|
||||
QJsonObject authData;
|
||||
QString transactionId;
|
||||
|
||||
QJsonObject toJsonObject() const;
|
||||
};
|
||||
@@ -61,10 +62,15 @@ public:
|
||||
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &email);
|
||||
|
||||
ErrorCode importServiceFromAppStore(const QString &userCountryCode, const QString &serviceType,
|
||||
ErrorCode importServiceFromMarket(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||
const QString &transactionId, bool isTestPurchase,
|
||||
int *duplicateServerIndex = nullptr);
|
||||
int *duplicateServerIndex,
|
||||
const QString &endpoint);
|
||||
|
||||
ErrorCode getSubscriptionInfo(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &purchaseToken,
|
||||
QByteArray &responseBody);
|
||||
|
||||
ErrorCode updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, bool isConnectEvent,
|
||||
CaptchaInfo *captchaInfoOut = nullptr, ProtocolData *usedProtocolDataOut = nullptr);
|
||||
@@ -107,10 +113,23 @@ public:
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
};
|
||||
|
||||
struct PlayMarketRestoreResult
|
||||
{
|
||||
bool hasInstalledConfig = false;
|
||||
bool duplicateConfigAlreadyPresent = false;
|
||||
int duplicateCount = 0;
|
||||
int duplicateServerIndex = -1;
|
||||
ErrorCode errorCode = ErrorCode::NoError;
|
||||
};
|
||||
|
||||
ErrorCode processAppStorePurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex = nullptr);
|
||||
|
||||
ErrorCode processPlayMarketPurchase(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol, const QString &productId,
|
||||
int *duplicateServerIndex = nullptr);
|
||||
|
||||
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol);
|
||||
|
||||
@@ -119,6 +138,9 @@ public:
|
||||
const QString &captchaId, const QString &captchaSolution,
|
||||
CaptchaInfo *retryCaptchaOut = nullptr);
|
||||
|
||||
PlayMarketRestoreResult processPlayMarketRestore(const QString &userCountryCode, const QString &serviceType,
|
||||
const QString &serviceProtocol);
|
||||
|
||||
private:
|
||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||
bool isApiKeyExpired(const QString &serverId) const;
|
||||
|
||||
@@ -31,7 +31,6 @@ ConnectionController::ConnectionController(SecureServersRepository* serversRepos
|
||||
connect(m_vpnConnection, &VpnConnection::connectionStateChanged, this, &ConnectionController::connectionStateChanged);
|
||||
connect(this, &ConnectionController::openConnectionRequested, m_vpnConnection, &VpnConnection::connectToVpn, Qt::QueuedConnection);
|
||||
connect(this, &ConnectionController::closeConnectionRequested, m_vpnConnection, &VpnConnection::disconnectFromVpn, Qt::QueuedConnection);
|
||||
connect(this, &ConnectionController::setConnectionStateRequested, m_vpnConnection, &VpnConnection::setConnectionState, Qt::QueuedConnection);
|
||||
connect(this, &ConnectionController::killSwitchModeChangedRequested, m_vpnConnection, &VpnConnection::onKillSwitchModeChanged, Qt::QueuedConnection);
|
||||
#ifdef Q_OS_ANDROID
|
||||
connect(this, &ConnectionController::restoreConnectionRequested, m_vpnConnection, &VpnConnection::restoreConnection, Qt::QueuedConnection);
|
||||
@@ -45,9 +44,7 @@ bool ConnectionController::isConnected() const
|
||||
|
||||
void ConnectionController::setConnectionState(Vpn::ConnectionState state)
|
||||
{
|
||||
if (m_vpnConnection) {
|
||||
emit setConnectionStateRequested(state);
|
||||
}
|
||||
emit connectionStateChanged(state);
|
||||
}
|
||||
|
||||
ErrorCode ConnectionController::defaultContainerForServer(const QString &serverId, DockerContainer &container) const
|
||||
|
||||
@@ -67,7 +67,6 @@ signals:
|
||||
void connectionStateChanged(Vpn::ConnectionState state);
|
||||
void openConnectionRequested(const QString &serverId, DockerContainer container, const QJsonObject &vpnConfiguration);
|
||||
void closeConnectionRequested();
|
||||
void setConnectionStateRequested(Vpn::ConnectionState state);
|
||||
void killSwitchModeChangedRequested(bool enabled);
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
|
||||
@@ -285,6 +285,10 @@ void CoreController::initSignalHandlers()
|
||||
if (m_serversUiController->hasServersFromGatewayApi()) {
|
||||
m_apiNewsUiController->fetchNews(false);
|
||||
}
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
m_updateController->checkForUpdates();
|
||||
#endif
|
||||
}
|
||||
|
||||
void CoreController::updateTranslator(const QLocale &locale)
|
||||
|
||||
@@ -440,9 +440,6 @@ void CoreSignalHandlers::initNotificationHandler()
|
||||
void CoreSignalHandlers::initUpdateFoundHandler()
|
||||
{
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
|
||||
connect(m_coreController->m_apiNewsUiController, &ApiNewsUiController::fetchNewsFinished, m_coreController->m_updateUiController,
|
||||
&UpdateUiController::checkForUpdates);
|
||||
|
||||
connect(m_coreController->m_updateUiController, &UpdateUiController::updateFound, this, [this]() {
|
||||
const QString version = m_coreController->m_updateUiController->getVersion();
|
||||
const QString updateId = version.isEmpty() ? QStringLiteral("update") : QStringLiteral("update-%1").arg(version);
|
||||
|
||||
@@ -74,6 +74,28 @@ QJsonObject AwgServerConfig::toJson() const
|
||||
obj[configKey::specialJunk4] = specialJunk4;
|
||||
obj[configKey::specialJunk5] = specialJunk5;
|
||||
|
||||
if (!headerProtectionKey.isEmpty()) {
|
||||
obj[configKey::headerProtectionKey] = headerProtectionKey;
|
||||
}
|
||||
if (!contentPaddingAddition.isEmpty()) {
|
||||
obj[configKey::contentPaddingAddition] = contentPaddingAddition;
|
||||
}
|
||||
if (!rekeyAfterTime.isEmpty()) {
|
||||
obj[configKey::rekeyAfterTime] = rekeyAfterTime;
|
||||
}
|
||||
if (!rekeyTimeout.isEmpty()) {
|
||||
obj[configKey::rekeyTimeout] = rekeyTimeout;
|
||||
}
|
||||
if (!rejectAfterTime.isEmpty()) {
|
||||
obj[configKey::rejectAfterTime] = rejectAfterTime;
|
||||
}
|
||||
if (!keepaliveTimeout.isEmpty()) {
|
||||
obj[configKey::keepaliveTimeout] = keepaliveTimeout;
|
||||
}
|
||||
if (!maxHandshakeAttempts.isEmpty()) {
|
||||
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
|
||||
}
|
||||
|
||||
if (isThirdPartyConfig) {
|
||||
obj[configKey::isThirdPartyConfig] = isThirdPartyConfig;
|
||||
}
|
||||
@@ -109,6 +131,14 @@ AwgServerConfig AwgServerConfig::fromJson(const QJsonObject& json)
|
||||
config.specialJunk3 = json.value(configKey::specialJunk3).toString();
|
||||
config.specialJunk4 = json.value(configKey::specialJunk4).toString();
|
||||
config.specialJunk5 = json.value(configKey::specialJunk5).toString();
|
||||
|
||||
config.headerProtectionKey = json.value(configKey::headerProtectionKey).toString();
|
||||
config.contentPaddingAddition = json.value(configKey::contentPaddingAddition).toString();
|
||||
config.rekeyAfterTime = json.value(configKey::rekeyAfterTime).toString();
|
||||
config.rekeyTimeout = json.value(configKey::rekeyTimeout).toString();
|
||||
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
|
||||
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
|
||||
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
|
||||
|
||||
config.isThirdPartyConfig = json.value(configKey::isThirdPartyConfig).toBool(false);
|
||||
|
||||
@@ -211,6 +241,28 @@ QJsonObject AwgClientConfig::toJson() const
|
||||
if (!specialJunk5.isEmpty()) {
|
||||
obj[configKey::specialJunk5] = specialJunk5;
|
||||
}
|
||||
|
||||
if (!headerProtectionKey.isEmpty()) {
|
||||
obj[configKey::headerProtectionKey] = headerProtectionKey;
|
||||
}
|
||||
if (!contentPaddingAddition.isEmpty()) {
|
||||
obj[configKey::contentPaddingAddition] = contentPaddingAddition;
|
||||
}
|
||||
if (!rekeyAfterTime.isEmpty()) {
|
||||
obj[configKey::rekeyAfterTime] = rekeyAfterTime;
|
||||
}
|
||||
if (!rekeyTimeout.isEmpty()) {
|
||||
obj[configKey::rekeyTimeout] = rekeyTimeout;
|
||||
}
|
||||
if (!rejectAfterTime.isEmpty()) {
|
||||
obj[configKey::rejectAfterTime] = rejectAfterTime;
|
||||
}
|
||||
if (!keepaliveTimeout.isEmpty()) {
|
||||
obj[configKey::keepaliveTimeout] = keepaliveTimeout;
|
||||
}
|
||||
if (!maxHandshakeAttempts.isEmpty()) {
|
||||
obj[configKey::maxHandshakeAttempts] = maxHandshakeAttempts;
|
||||
}
|
||||
|
||||
if (isObfuscationEnabled) {
|
||||
obj[configKey::isObfuscationEnabled] = isObfuscationEnabled;
|
||||
@@ -258,6 +310,14 @@ AwgClientConfig AwgClientConfig::fromJson(const QJsonObject& json)
|
||||
config.specialJunk3 = json.value(configKey::specialJunk3).toString();
|
||||
config.specialJunk4 = json.value(configKey::specialJunk4).toString();
|
||||
config.specialJunk5 = json.value(configKey::specialJunk5).toString();
|
||||
|
||||
config.headerProtectionKey = json.value(configKey::headerProtectionKey).toString();
|
||||
config.contentPaddingAddition = json.value(configKey::contentPaddingAddition).toString();
|
||||
config.rekeyAfterTime = json.value(configKey::rekeyAfterTime).toString();
|
||||
config.rekeyTimeout = json.value(configKey::rekeyTimeout).toString();
|
||||
config.rejectAfterTime = json.value(configKey::rejectAfterTime).toString();
|
||||
config.keepaliveTimeout = json.value(configKey::keepaliveTimeout).toString();
|
||||
config.maxHandshakeAttempts = json.value(configKey::maxHandshakeAttempts).toString();
|
||||
|
||||
config.isObfuscationEnabled = json.value(configKey::isObfuscationEnabled).toBool(false);
|
||||
|
||||
@@ -320,7 +380,8 @@ bool AwgServerConfig::hasEqualServerSettings(const AwgServerConfig& other) const
|
||||
transportPacketMagicHeader != other.transportPacketMagicHeader ||
|
||||
specialJunk1 != other.specialJunk1 || specialJunk2 != other.specialJunk2 ||
|
||||
specialJunk3 != other.specialJunk3 || specialJunk4 != other.specialJunk4 ||
|
||||
specialJunk5 != other.specialJunk5) {
|
||||
specialJunk5 != other.specialJunk5 ||
|
||||
headerProtectionKey != other.headerProtectionKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,13 @@ struct AwgServerConfig {
|
||||
QString specialJunk3;
|
||||
QString specialJunk4;
|
||||
QString specialJunk5;
|
||||
QString headerProtectionKey;
|
||||
QString contentPaddingAddition;
|
||||
QString rekeyAfterTime;
|
||||
QString rekeyTimeout;
|
||||
QString rejectAfterTime;
|
||||
QString keepaliveTimeout;
|
||||
QString maxHandshakeAttempts;
|
||||
bool isThirdPartyConfig = false;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
@@ -76,6 +83,13 @@ struct AwgClientConfig {
|
||||
QString specialJunk3;
|
||||
QString specialJunk4;
|
||||
QString specialJunk5;
|
||||
QString headerProtectionKey;
|
||||
QString contentPaddingAddition;
|
||||
QString rekeyAfterTime;
|
||||
QString rekeyTimeout;
|
||||
QString rejectAfterTime;
|
||||
QString keepaliveTimeout;
|
||||
QString maxHandshakeAttempts;
|
||||
bool isObfuscationEnabled = false;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
|
||||
@@ -90,6 +90,14 @@ namespace amnezia
|
||||
constexpr QLatin1String specialJunk4("I4");
|
||||
constexpr QLatin1String specialJunk5("I5");
|
||||
|
||||
constexpr QLatin1String headerProtectionKey("HeaderProtectionKey");
|
||||
constexpr QLatin1String contentPaddingAddition("ContentPaddingAddition");
|
||||
constexpr QLatin1String rekeyAfterTime("RekeyAfterTime");
|
||||
constexpr QLatin1String rekeyTimeout("RekeyTimeout");
|
||||
constexpr QLatin1String rejectAfterTime("RejectAfterTime");
|
||||
constexpr QLatin1String keepaliveTimeout("KeepaliveTimeout");
|
||||
constexpr QLatin1String maxHandshakeAttempts("MaxHandshakeAttempts");
|
||||
|
||||
constexpr QLatin1String protocolVersion("protocol_version");
|
||||
|
||||
constexpr QLatin1String openvpn("openvpn");
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace amnezia
|
||||
constexpr char defaultFlow[] = "xtls-rprx-vision";
|
||||
constexpr char defaultTransport[] = "raw";
|
||||
constexpr char defaultFingerprint[] = "chrome";
|
||||
constexpr char defaultSni[] = "cdn.example.com";
|
||||
constexpr char defaultSni[] = "www.googletagmanager.com";
|
||||
constexpr char defaultAlpn[] = "HTTP/2";
|
||||
|
||||
constexpr char defaultXhttpMode[] = "Auto";
|
||||
@@ -147,6 +147,7 @@ namespace amnezia
|
||||
constexpr char defaultSubnetCidr[] = "24";
|
||||
|
||||
constexpr char defaultPort[] = "51820";
|
||||
constexpr char defaultPersistentKeepAlive[] = "25";
|
||||
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
constexpr char defaultMtu[] = "1280";
|
||||
|
||||
@@ -108,6 +108,8 @@ namespace amnezia
|
||||
ApiCaptchaInvalidError = 1118,
|
||||
ApiCaptchaRefreshError = 1119,
|
||||
ApiRateLimitError = 1120,
|
||||
ApiNoPurchasesToRestore = 1121,
|
||||
ApiPurchasePendingError = 1122,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
@@ -115,7 +117,16 @@ namespace amnezia
|
||||
PermissionsError = 1202,
|
||||
UnspecifiedError = 1203,
|
||||
FatalError = 1204,
|
||||
AbortError = 1205
|
||||
AbortError = 1205,
|
||||
|
||||
// Billing errors
|
||||
BillingCanceled = 1300,
|
||||
BillingError = 1301,
|
||||
BillingGooglePlayError = 1302,
|
||||
BillingUnavailable = 1303,
|
||||
SubscriptionAlreadyOwned = 1304,
|
||||
SubscriptionUnavailable = 1305,
|
||||
BillingNetworkError = 1306,
|
||||
};
|
||||
Q_ENUM_NS(ErrorCode)
|
||||
}
|
||||
|
||||
@@ -100,6 +100,16 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ApiCaptchaInvalidError): errorMessage = QObject::tr("CAPTCHA was incorrect. Please try again"); break;
|
||||
case (ErrorCode::ApiCaptchaRefreshError): errorMessage = QObject::tr("CAPTCHA refreshed. Please try again"); break;
|
||||
case (ErrorCode::ApiRateLimitError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
|
||||
case (ErrorCode::ApiPurchasePendingError): errorMessage = QObject::tr("Your payment is pending confirmation in Google Play. Please complete the payment and then restore your subscription."); 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;
|
||||
@@ -109,6 +119,15 @@ QString errorString(ErrorCode code) {
|
||||
case(ErrorCode::FatalError): errorMessage = QObject::tr("QFile error: A fatal error occurred"); break;
|
||||
case(ErrorCode::AbortError): errorMessage = QObject::tr("QFile error: The operation was aborted"); break;
|
||||
|
||||
// Billing errors
|
||||
case(ErrorCode::BillingCanceled): errorMessage = QObject::tr("Transaction was canceled by the user"); break;
|
||||
case(ErrorCode::BillingError): errorMessage = QObject::tr("Billing error"); break;
|
||||
case(ErrorCode::BillingGooglePlayError): errorMessage = QObject::tr("Internal Google Play error, please try again later"); break;
|
||||
case(ErrorCode::BillingUnavailable): errorMessage = QObject::tr("Billing is unavailable, please try again later"); break;
|
||||
case(ErrorCode::SubscriptionAlreadyOwned): errorMessage = QObject::tr("You already own this subscription"); break;
|
||||
case(ErrorCode::SubscriptionUnavailable): errorMessage = QObject::tr("The requested subscription is not available for purchase"); break;
|
||||
case(ErrorCode::BillingNetworkError): errorMessage = QObject::tr("A network error occurred during the operation, please check the Internet connection"); break;
|
||||
|
||||
case(ErrorCode::InternalError):
|
||||
default:
|
||||
errorMessage = QObject::tr("Internal error"); break;
|
||||
|
||||
@@ -286,7 +286,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
return { resGateway, QNetworkInterface::interfaceFromIndex(resIndex) };
|
||||
#endif
|
||||
#ifdef Q_OS_LINUX
|
||||
constexpr int BUFFER_SIZE = 100;
|
||||
constexpr int BUFFER_SIZE = 8192;
|
||||
int received_bytes = 0, msg_len = 0, route_attribute_len = 0;
|
||||
int sock = -1, msgseq = 0;
|
||||
struct nlmsghdr *nlh, *nlmsg;
|
||||
@@ -294,7 +294,7 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
// This struct contain route attributes (route type)
|
||||
struct rtattr *route_attribute;
|
||||
char gateway_address[INET_ADDRSTRLEN], interface[IF_NAMESIZE];
|
||||
char msgbuf[BUFFER_SIZE], buffer[BUFFER_SIZE];
|
||||
char msgbuf[100], buffer[BUFFER_SIZE];
|
||||
char *ptr = buffer;
|
||||
struct timeval tv;
|
||||
|
||||
@@ -339,8 +339,8 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
nlh = (struct nlmsghdr *) ptr;
|
||||
|
||||
/* Check if the header is valid */
|
||||
if((NLMSG_OK(nlmsg, received_bytes) == 0) ||
|
||||
(nlmsg->nlmsg_type == NLMSG_ERROR))
|
||||
if((NLMSG_OK(nlh, received_bytes) == 0) ||
|
||||
(nlh->nlmsg_type == NLMSG_ERROR))
|
||||
{
|
||||
perror("Error in received packet");
|
||||
return {};
|
||||
@@ -355,13 +355,15 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
}
|
||||
|
||||
/* Break if its not a multi part message */
|
||||
if ((nlmsg->nlmsg_flags & NLM_F_MULTI) == 0)
|
||||
if ((nlh->nlmsg_flags & NLM_F_MULTI) == 0)
|
||||
break;
|
||||
}
|
||||
while ((nlmsg->nlmsg_seq != msgseq) || (nlmsg->nlmsg_pid != getpid()));
|
||||
while ((nlh->nlmsg_seq != msgseq) || (nlh->nlmsg_pid != getpid()));
|
||||
|
||||
/* parse response */
|
||||
for ( ; NLMSG_OK(nlh, received_bytes); nlh = NLMSG_NEXT(nlh, received_bytes))
|
||||
int remaining = msg_len + received_bytes;
|
||||
nlh = (struct nlmsghdr *) buffer;
|
||||
for ( ; NLMSG_OK(nlh, remaining); nlh = NLMSG_NEXT(nlh, remaining))
|
||||
{
|
||||
/* Get the route data */
|
||||
route_entry = (struct rtmsg *) NLMSG_DATA(nlh);
|
||||
@@ -370,6 +372,10 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
if (route_entry->rtm_table != RT_TABLE_MAIN)
|
||||
continue;
|
||||
|
||||
/* Reset per-route to avoid cross-route state pollution */
|
||||
memset(gateway_address, 0, sizeof(gateway_address));
|
||||
memset(interface, 0, sizeof(interface));
|
||||
|
||||
route_attribute = (struct rtattr *) RTM_RTA(route_entry);
|
||||
route_attribute_len = RTM_PAYLOAD(nlh);
|
||||
|
||||
@@ -395,6 +401,8 @@ QPair<QString, QNetworkInterface> NetworkUtilities::getGatewayAndIface()
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!(*gateway_address) || !(*interface))
|
||||
qDebug() << "getGatewayAndIface: no gateway found";
|
||||
close(sock);
|
||||
return { gateway_address, QNetworkInterface::interfaceFromName(interface) };
|
||||
#endif
|
||||
|
||||
@@ -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) \
|
||||
{ \
|
||||
|
||||
@@ -265,6 +265,8 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
|
||||
#endif
|
||||
}
|
||||
|
||||
config.m_persistentKeepalive = obj.value("persistentKeepalive").toString();
|
||||
|
||||
config.m_deviceIpv4Address = obj.value("deviceIpv4Address").toString();
|
||||
config.m_deviceIpv6Address = obj.value("deviceIpv6Address").toString();
|
||||
if (config.m_deviceIpv4Address.isNull() &&
|
||||
@@ -441,6 +443,28 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
|
||||
config.m_specialJunk["I5"] = i5.toString();
|
||||
}
|
||||
|
||||
if (const auto headerProtectionKey = obj.value("HeaderProtectionKey"); !headerProtectionKey.isUndefined()) {
|
||||
config.m_headerProtectionKey = headerProtectionKey.toString();
|
||||
}
|
||||
if (const auto contentPaddingAddition = obj.value("ContentPaddingAddition"); !contentPaddingAddition.isUndefined()) {
|
||||
config.m_contentPaddingAddition = contentPaddingAddition.toString();
|
||||
}
|
||||
if (const auto rekeyAfterTime = obj.value("RekeyAfterTime"); !rekeyAfterTime.isUndefined()) {
|
||||
config.m_rekeyAfterTime = rekeyAfterTime.toString();
|
||||
}
|
||||
if (const auto rekeyTimeout = obj.value("RekeyTimeout"); !rekeyTimeout.isUndefined()) {
|
||||
config.m_rekeyTimeout = rekeyTimeout.toString();
|
||||
}
|
||||
if (const auto rejectAfterTime = obj.value("RejectAfterTime"); !rejectAfterTime.isUndefined()) {
|
||||
config.m_rejectAfterTime = rejectAfterTime.toString();
|
||||
}
|
||||
if (const auto keepaliveTimeout = obj.value("KeepaliveTimeout"); !keepaliveTimeout.isUndefined()) {
|
||||
config.m_keepaliveTimeout = keepaliveTimeout.toString();
|
||||
}
|
||||
if (const auto maxHandshakeAttempts = obj.value("MaxHandshakeAttempts"); !maxHandshakeAttempts.isUndefined()) {
|
||||
config.m_maxHandshakeAttempts = maxHandshakeAttempts.toString();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ QJsonObject InterfaceConfig::toJson() const {
|
||||
json.insert("serverIpv6AddrIn", QJsonValue(m_serverIpv6AddrIn));
|
||||
json.insert("serverPort", QJsonValue((double)m_serverPort));
|
||||
json.insert("deviceMTU", QJsonValue(m_deviceMTU));
|
||||
if (!m_persistentKeepalive.isEmpty()) {
|
||||
json.insert("persistentKeepalive", QJsonValue(m_persistentKeepalive));
|
||||
}
|
||||
if ((m_hopType == InterfaceConfig::MultiHopExit) ||
|
||||
(m_hopType == InterfaceConfig::SingleHop)) {
|
||||
json.insert("serverIpv4Gateway", QJsonValue(m_serverIpv4Gateway));
|
||||
@@ -153,6 +156,28 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
|
||||
out << key << " = " << m_specialJunk[key] << "\n";
|
||||
}
|
||||
|
||||
if (!m_headerProtectionKey.isEmpty()) {
|
||||
out << "HeaderProtectionKey = " << m_headerProtectionKey << "\n";
|
||||
}
|
||||
if (!m_contentPaddingAddition.isEmpty()) {
|
||||
out << "ContentPaddingAddition = " << m_contentPaddingAddition << "\n";
|
||||
}
|
||||
if (!m_rekeyAfterTime.isEmpty()) {
|
||||
out << "RekeyAfterTime = " << m_rekeyAfterTime << "\n";
|
||||
}
|
||||
if (!m_rekeyTimeout.isEmpty()) {
|
||||
out << "RekeyTimeout = " << m_rekeyTimeout << "\n";
|
||||
}
|
||||
if (!m_rejectAfterTime.isEmpty()) {
|
||||
out << "RejectAfterTime = " << m_rejectAfterTime << "\n";
|
||||
}
|
||||
if (!m_keepaliveTimeout.isEmpty()) {
|
||||
out << "KeepaliveTimeout = " << m_keepaliveTimeout << "\n";
|
||||
}
|
||||
if (!m_maxHandshakeAttempts.isEmpty()) {
|
||||
out << "MaxHandshakeAttempts = " << m_maxHandshakeAttempts << "\n";
|
||||
}
|
||||
|
||||
// If any extra config was provided, append it now.
|
||||
for (const QString& key : extra.keys()) {
|
||||
out << key << " = " << extra[key] << "\n";
|
||||
@@ -173,6 +198,9 @@ QString InterfaceConfig::toWgConf(const QMap<QString, QString>& extra) const {
|
||||
ranges.append(ip.toString());
|
||||
}
|
||||
out << "AllowedIPs = " << ranges.join(", ") << "\n";
|
||||
if (!m_persistentKeepalive.isEmpty()) {
|
||||
out << "PersistentKeepalive = " << m_persistentKeepalive << "\n";
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ class InterfaceConfig {
|
||||
QString m_secondaryDnsServer;
|
||||
int m_serverPort = 0;
|
||||
int m_deviceMTU = 1420;
|
||||
QString m_persistentKeepalive;
|
||||
QList<IPAddress> m_allowedIPAddressRanges;
|
||||
QStringList m_excludedAddresses;
|
||||
QStringList m_vpnDisabledApps;
|
||||
@@ -58,6 +59,14 @@ class InterfaceConfig {
|
||||
QString m_transportPacketMagicHeader;
|
||||
QMap<QString, QString> m_specialJunk;
|
||||
|
||||
QString m_headerProtectionKey;
|
||||
QString m_contentPaddingAddition;
|
||||
QString m_rekeyAfterTime;
|
||||
QString m_rekeyTimeout;
|
||||
QString m_rejectAfterTime;
|
||||
QString m_keepaliveTimeout;
|
||||
QString m_maxHandshakeAttempts;
|
||||
|
||||
QJsonObject toJson() const;
|
||||
QString toWgConf(
|
||||
const QMap<QString, QString>& extra = QMap<QString, QString>()) const;
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
constexpr const char* WG_INTERFACE = "amn0";
|
||||
|
||||
constexpr uint16_t WG_KEEPALIVE_PERIOD = 60;
|
||||
|
||||
class WireguardUtils : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
|
||||
@@ -44,8 +44,20 @@
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIcons</key>
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>CFBundlePrimaryIcon</key>
|
||||
<dict>
|
||||
<key>CFBundleIconFiles</key>
|
||||
<array>
|
||||
<string>AppIcon</string>
|
||||
</array>
|
||||
<key>CFBundleIconName</key>
|
||||
<string>AppIcon</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -159,6 +159,11 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
json.insert("serverIpv4Gateway", wgConfig.value(amnezia::configKey::hostName));
|
||||
// json.insert("serverIpv6Gateway", QJsonValue(hop.m_server.ipv6Gateway()));
|
||||
|
||||
if (wgConfig.contains(amnezia::configKey::persistentKeepAlive)) {
|
||||
json.insert("persistentKeepalive",
|
||||
wgConfig.value(amnezia::configKey::persistentKeepAlive).toString());
|
||||
}
|
||||
|
||||
json.insert("primaryDnsServer", rawConfig.value(amnezia::configKey::dns1));
|
||||
|
||||
// We don't use secondary DNS if primary DNS is AmneziaDNS
|
||||
@@ -263,6 +268,13 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
json.insert(amnezia::configKey::specialJunk3, wgConfig.value(amnezia::configKey::specialJunk3));
|
||||
json.insert(amnezia::configKey::specialJunk4, wgConfig.value(amnezia::configKey::specialJunk4));
|
||||
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
|
||||
json.insert(amnezia::configKey::headerProtectionKey, wgConfig.value(amnezia::configKey::headerProtectionKey));
|
||||
json.insert(amnezia::configKey::contentPaddingAddition, wgConfig.value(amnezia::configKey::contentPaddingAddition));
|
||||
json.insert(amnezia::configKey::rekeyAfterTime, wgConfig.value(amnezia::configKey::rekeyAfterTime));
|
||||
json.insert(amnezia::configKey::rekeyTimeout, wgConfig.value(amnezia::configKey::rekeyTimeout));
|
||||
json.insert(amnezia::configKey::rejectAfterTime, wgConfig.value(amnezia::configKey::rejectAfterTime));
|
||||
json.insert(amnezia::configKey::keepaliveTimeout, wgConfig.value(amnezia::configKey::keepaliveTimeout));
|
||||
json.insert(amnezia::configKey::maxHandshakeAttempts, wgConfig.value(amnezia::configKey::maxHandshakeAttempts));
|
||||
} else if (!wgConfig.value(amnezia::configKey::junkPacketCount).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::junkPacketMinSize).isUndefined()
|
||||
&& !wgConfig.value(amnezia::configKey::junkPacketMaxSize).isUndefined()
|
||||
@@ -290,6 +302,13 @@ void LocalSocketController::activate(const QJsonObject &rawConfig) {
|
||||
json.insert(amnezia::configKey::specialJunk3, wgConfig.value(amnezia::configKey::specialJunk3));
|
||||
json.insert(amnezia::configKey::specialJunk4, wgConfig.value(amnezia::configKey::specialJunk4));
|
||||
json.insert(amnezia::configKey::specialJunk5, wgConfig.value(amnezia::configKey::specialJunk5));
|
||||
json.insert(amnezia::configKey::headerProtectionKey, wgConfig.value(amnezia::configKey::headerProtectionKey));
|
||||
json.insert(amnezia::configKey::contentPaddingAddition, wgConfig.value(amnezia::configKey::contentPaddingAddition));
|
||||
json.insert(amnezia::configKey::rekeyAfterTime, wgConfig.value(amnezia::configKey::rekeyAfterTime));
|
||||
json.insert(amnezia::configKey::rekeyTimeout, wgConfig.value(amnezia::configKey::rekeyTimeout));
|
||||
json.insert(amnezia::configKey::rejectAfterTime, wgConfig.value(amnezia::configKey::rejectAfterTime));
|
||||
json.insert(amnezia::configKey::keepaliveTimeout, wgConfig.value(amnezia::configKey::keepaliveTimeout));
|
||||
json.insert(amnezia::configKey::maxHandshakeAttempts, wgConfig.value(amnezia::configKey::maxHandshakeAttempts));
|
||||
}
|
||||
|
||||
write(json);
|
||||
|
||||
@@ -154,6 +154,28 @@ void AndroidController::resetLastServer(int serverIndex)
|
||||
callActivityMethod("resetLastServer", "(I)V", serverIndex);
|
||||
}
|
||||
|
||||
void AndroidController::showUpdateCover()
|
||||
{
|
||||
callActivityMethod("showUpdateCover", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::hideUpdateCover()
|
||||
{
|
||||
callActivityMethod("hideUpdateCover", "()V");
|
||||
}
|
||||
|
||||
void AndroidController::showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
|
||||
const QString &skipTitle, const QString &storeUrl)
|
||||
{
|
||||
callActivityMethod("showUpdatePrompt",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V",
|
||||
QJniObject::fromString(title).object<jstring>(),
|
||||
QJniObject::fromString(message).object<jstring>(),
|
||||
QJniObject::fromString(updateTitle).object<jstring>(),
|
||||
QJniObject::fromString(skipTitle).object<jstring>(),
|
||||
QJniObject::fromString(storeUrl).object<jstring>());
|
||||
}
|
||||
|
||||
void AndroidController::saveFile(const QString &fileName, const QString &data)
|
||||
{
|
||||
callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V",
|
||||
@@ -328,6 +350,57 @@ void AndroidController::sendTouch(float x, float y)
|
||||
callActivityMethod("sendTouch", "(FF)V", x, y);
|
||||
}
|
||||
|
||||
|
||||
bool AndroidController::isPlay()
|
||||
{
|
||||
return callActivityMethod<jboolean>("isPlay", "()Z");
|
||||
}
|
||||
|
||||
bool AndroidController::isTestPurchaseEnvironment()
|
||||
{
|
||||
return callActivityMethod<jboolean>("isTestPurchaseEnvironment", "()Z");
|
||||
}
|
||||
|
||||
QJsonObject AndroidController::getSubscriptionPlans()
|
||||
{
|
||||
QJniObject subscriptionPlans = callActivityMethod<jstring>("getSubscriptionPlans", "()Ljava/lang/String;");
|
||||
QJsonObject json = QJsonDocument::fromJson(subscriptionPlans.toString().toUtf8()).object();
|
||||
return json;
|
||||
}
|
||||
|
||||
QJsonObject AndroidController::purchaseSubscription(const QString &offerToken)
|
||||
{
|
||||
QJniObject result = callActivityMethod<jstring, jstring>("purchaseSubscription", "(Ljava/lang/String;)Ljava/lang/String;",
|
||||
QJniObject::fromString(offerToken).object<jstring>());
|
||||
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||
return json;
|
||||
}
|
||||
|
||||
QJsonObject AndroidController::upgradeSubscription(const QString &offerToken, const QString &oldPurchaseToken)
|
||||
{
|
||||
QJniObject result = callActivityMethod<jstring, jstring, jstring>("upgradeSubscription",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
|
||||
QJniObject::fromString(offerToken).object<jstring>(),
|
||||
QJniObject::fromString(oldPurchaseToken).object<jstring>());
|
||||
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||
return json;
|
||||
}
|
||||
|
||||
QJsonObject AndroidController::acknowledgePurchase(const QString &purchaseToken)
|
||||
{
|
||||
QJniObject result = callActivityMethod<jstring, jstring>("acknowledgePurchase", "(Ljava/lang/String;)Ljava/lang/String;",
|
||||
QJniObject::fromString(purchaseToken).object<jstring>());
|
||||
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||
return json;
|
||||
}
|
||||
|
||||
QJsonObject AndroidController::queryPurchases()
|
||||
{
|
||||
QJniObject result = callActivityMethod<jstring>("queryPurchases", "()Ljava/lang/String;");
|
||||
QJsonObject json = QJsonDocument::fromJson(result.toString().toUtf8()).object();
|
||||
return json;
|
||||
}
|
||||
|
||||
// Moving log processing to the Android side
|
||||
jclass AndroidController::log;
|
||||
jmethodID AndroidController::logDebug;
|
||||
|
||||
@@ -55,6 +55,18 @@ public:
|
||||
void requestNotificationPermission();
|
||||
bool requestAuthentication();
|
||||
void sendTouch(float x, float y);
|
||||
bool isPlay();
|
||||
bool isTestPurchaseEnvironment();
|
||||
QJsonObject getSubscriptionPlans();
|
||||
QJsonObject purchaseSubscription(const QString &offerToken);
|
||||
QJsonObject upgradeSubscription(const QString &offerToken, const QString &oldPurchaseToken);
|
||||
QJsonObject acknowledgePurchase(const QString &purchaseToken);
|
||||
QJsonObject queryPurchases();
|
||||
|
||||
void showUpdateCover();
|
||||
void hideUpdateCover();
|
||||
void showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
|
||||
const QString &skipTitle, const QString &storeUrl);
|
||||
|
||||
static bool initLogging();
|
||||
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||
|
||||
@@ -6,6 +6,8 @@ struct WGConfig: Decodable {
|
||||
let junkPacketCount, junkPacketMinSize, junkPacketMaxSize: String?
|
||||
let initPacketJunkSize, responsePacketJunkSize, cookieReplyPacketJunkSize, transportPacketJunkSize: String?
|
||||
let specialJunk1, specialJunk2, specialJunk3, specialJunk4, specialJunk5: String?
|
||||
let headerProtectionKey: String?
|
||||
let contentPaddingAddition, rekeyAfterTime, rekeyTimeout, rejectAfterTime, keepaliveTimeout, maxHandshakeAttempts: String?
|
||||
let dns1: String
|
||||
let dns2: String
|
||||
let mtu: String
|
||||
@@ -16,7 +18,7 @@ struct WGConfig: Decodable {
|
||||
let serverPublicKey: String
|
||||
let presharedKey: String?
|
||||
var allowedIPs: [String]
|
||||
var persistentKeepAlive: String
|
||||
var persistentKeepAlive: String?
|
||||
let splitTunnelType: Int
|
||||
let splitTunnelSites: [String]
|
||||
|
||||
@@ -26,6 +28,11 @@ struct WGConfig: Decodable {
|
||||
case junkPacketCount = "Jc", junkPacketMinSize = "Jmin", junkPacketMaxSize = "Jmax"
|
||||
case initPacketJunkSize = "S1", responsePacketJunkSize = "S2", cookieReplyPacketJunkSize = "S3", transportPacketJunkSize = "S4"
|
||||
case specialJunk1 = "I1", specialJunk2 = "I2", specialJunk3 = "I3", specialJunk4 = "I4", specialJunk5 = "I5"
|
||||
case headerProtectionKey = "HeaderProtectionKey"
|
||||
case contentPaddingAddition = "ContentPaddingAddition"
|
||||
case rekeyAfterTime = "RekeyAfterTime", rekeyTimeout = "RekeyTimeout"
|
||||
case rejectAfterTime = "RejectAfterTime", keepaliveTimeout = "KeepaliveTimeout"
|
||||
case maxHandshakeAttempts = "MaxHandshakeAttempts"
|
||||
case dns1
|
||||
case dns2
|
||||
case mtu
|
||||
@@ -100,6 +107,28 @@ struct WGConfig: Decodable {
|
||||
settingsLines.append("I5 = \(i5)")
|
||||
}
|
||||
|
||||
if let headerProtectionKey = trimmed(headerProtectionKey) {
|
||||
settingsLines.append("HeaderProtectionKey = \(headerProtectionKey)")
|
||||
}
|
||||
if let contentPaddingAddition = trimmed(contentPaddingAddition) {
|
||||
settingsLines.append("ContentPaddingAddition = \(contentPaddingAddition)")
|
||||
}
|
||||
if let rekeyAfterTime = trimmed(rekeyAfterTime) {
|
||||
settingsLines.append("RekeyAfterTime = \(rekeyAfterTime)")
|
||||
}
|
||||
if let rekeyTimeout = trimmed(rekeyTimeout) {
|
||||
settingsLines.append("RekeyTimeout = \(rekeyTimeout)")
|
||||
}
|
||||
if let rejectAfterTime = trimmed(rejectAfterTime) {
|
||||
settingsLines.append("RejectAfterTime = \(rejectAfterTime)")
|
||||
}
|
||||
if let keepaliveTimeout = trimmed(keepaliveTimeout) {
|
||||
settingsLines.append("KeepaliveTimeout = \(keepaliveTimeout)")
|
||||
}
|
||||
if let maxHandshakeAttempts = trimmed(maxHandshakeAttempts) {
|
||||
settingsLines.append("MaxHandshakeAttempts = \(maxHandshakeAttempts)")
|
||||
}
|
||||
|
||||
return settingsLines.joined(separator: "\n")
|
||||
}
|
||||
|
||||
@@ -116,7 +145,7 @@ struct WGConfig: Decodable {
|
||||
\(presharedKey == nil ? "" : "PresharedKey = \(presharedKey!)")
|
||||
AllowedIPs = \(allowedIPs.joined(separator: ", "))
|
||||
Endpoint = \(hostName):\(port)
|
||||
PersistentKeepalive = \(persistentKeepAlive)
|
||||
\(persistentKeepAlive == nil ? "" : "PersistentKeepalive = \(persistentKeepAlive!)")
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -133,7 +162,7 @@ struct WGConfig: Decodable {
|
||||
PresharedKey = ***
|
||||
AllowedIPs = \(allowedIPs.joined(separator: ", "))
|
||||
Endpoint = \(hostName):\(port)
|
||||
PersistentKeepalive = \(persistentKeepAlive)
|
||||
\(persistentKeepAlive == nil ? "" : "PersistentKeepalive = \(persistentKeepAlive!)")
|
||||
|
||||
SplitTunnelType = \(splitTunnelType)
|
||||
SplitTunnelSites = \(splitTunnelSites.joined(separator: ", "))
|
||||
|
||||
@@ -80,6 +80,11 @@ public:
|
||||
|
||||
void requestInetAccess();
|
||||
bool isTestFlight();
|
||||
|
||||
void showUpdateCover();
|
||||
void hideUpdateCover();
|
||||
void showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
|
||||
const QString &skipTitle, const QString &storeUrl);
|
||||
signals:
|
||||
void connectionStateChanged(Vpn::ConnectionState state);
|
||||
void bytesChanged(quint64 receivedBytes, quint64 sentBytes);
|
||||
|
||||
@@ -591,8 +591,6 @@ bool IosController::setupWireGuard()
|
||||
|
||||
if (config.contains(configKey::persistentKeepAlive)) {
|
||||
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
|
||||
} else {
|
||||
wgConfig.insert(configKey::persistentKeepAlive, "25");
|
||||
}
|
||||
|
||||
if (config.contains(configKey::isObfuscationEnabled) && config.value(configKey::isObfuscationEnabled).toBool()) {
|
||||
@@ -609,6 +607,14 @@ bool IosController::setupWireGuard()
|
||||
wgConfig.insert(configKey::junkPacketCount, config[configKey::junkPacketCount]);
|
||||
wgConfig.insert(configKey::junkPacketMinSize, config[configKey::junkPacketMinSize]);
|
||||
wgConfig.insert(configKey::junkPacketMaxSize, config[configKey::junkPacketMaxSize]);
|
||||
|
||||
wgConfig.insert(configKey::headerProtectionKey, config[configKey::headerProtectionKey]);
|
||||
wgConfig.insert(configKey::contentPaddingAddition, config[configKey::contentPaddingAddition]);
|
||||
wgConfig.insert(configKey::rekeyAfterTime, config[configKey::rekeyAfterTime]);
|
||||
wgConfig.insert(configKey::rekeyTimeout, config[configKey::rekeyTimeout]);
|
||||
wgConfig.insert(configKey::rejectAfterTime, config[configKey::rejectAfterTime]);
|
||||
wgConfig.insert(configKey::keepaliveTimeout, config[configKey::keepaliveTimeout]);
|
||||
wgConfig.insert(configKey::maxHandshakeAttempts, config[configKey::maxHandshakeAttempts]);
|
||||
}
|
||||
|
||||
QJsonDocument wgConfigDoc(wgConfig);
|
||||
@@ -697,8 +703,6 @@ bool IosController::setupAwg()
|
||||
|
||||
if (config.contains(configKey::persistentKeepAlive)) {
|
||||
wgConfig.insert(configKey::persistentKeepAlive, config[configKey::persistentKeepAlive]);
|
||||
} else {
|
||||
wgConfig.insert(configKey::persistentKeepAlive, "25");
|
||||
}
|
||||
|
||||
wgConfig.insert(configKey::initPacketMagicHeader, config[configKey::initPacketMagicHeader]);
|
||||
@@ -721,6 +725,14 @@ bool IosController::setupAwg()
|
||||
wgConfig.insert(configKey::specialJunk4, config[configKey::specialJunk4]);
|
||||
wgConfig.insert(configKey::specialJunk5, config[configKey::specialJunk5]);
|
||||
|
||||
wgConfig.insert(configKey::headerProtectionKey, config[configKey::headerProtectionKey]);
|
||||
wgConfig.insert(configKey::contentPaddingAddition, config[configKey::contentPaddingAddition]);
|
||||
wgConfig.insert(configKey::rekeyAfterTime, config[configKey::rekeyAfterTime]);
|
||||
wgConfig.insert(configKey::rekeyTimeout, config[configKey::rekeyTimeout]);
|
||||
wgConfig.insert(configKey::rejectAfterTime, config[configKey::rejectAfterTime]);
|
||||
wgConfig.insert(configKey::keepaliveTimeout, config[configKey::keepaliveTimeout]);
|
||||
wgConfig.insert(configKey::maxHandshakeAttempts, config[configKey::maxHandshakeAttempts]);
|
||||
|
||||
QJsonDocument wgConfigDoc(wgConfig);
|
||||
QString wgConfigDocStr(wgConfigDoc.toJson(QJsonDocument::Compact));
|
||||
|
||||
@@ -1200,3 +1212,138 @@ bool IosController::isTestFlight() {
|
||||
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
|
||||
return receiptURL && [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"];
|
||||
}
|
||||
|
||||
#if !MACOS_NE
|
||||
static UIWindow *s_updateCoverWindow = nil;
|
||||
|
||||
static UIWindowScene *activeWindowScene() {
|
||||
UIWindowScene *fallback = nil;
|
||||
for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) {
|
||||
if (![scene isKindOfClass:[UIWindowScene class]]) {
|
||||
continue;
|
||||
}
|
||||
fallback = (UIWindowScene *)scene;
|
||||
if (scene.activationState == UISceneActivationStateForegroundActive) {
|
||||
return (UIWindowScene *)scene;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
#endif
|
||||
|
||||
void IosController::showUpdateCover() {
|
||||
#if !MACOS_NE
|
||||
void (^build)(void) = ^{
|
||||
if (s_updateCoverWindow) {
|
||||
return;
|
||||
}
|
||||
UIWindowScene *scene = activeWindowScene();
|
||||
if (!scene) {
|
||||
return;
|
||||
}
|
||||
UIWindow *win = [[UIWindow alloc] initWithWindowScene:scene];
|
||||
win.windowLevel = UIWindowLevelAlert + 1;
|
||||
UIViewController *vc = [[[UIViewController alloc] init] autorelease];
|
||||
vc.view.backgroundColor = [UIColor colorWithRed:0.055 green:0.055 blue:0.063 alpha:1.0];
|
||||
win.rootViewController = vc;
|
||||
[win makeKeyAndVisible];
|
||||
s_updateCoverWindow = win;
|
||||
};
|
||||
|
||||
if ([NSThread isMainThread]) {
|
||||
build();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), build);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void IosController::hideUpdateCover() {
|
||||
#if !MACOS_NE
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (!s_updateCoverWindow) {
|
||||
return;
|
||||
}
|
||||
s_updateCoverWindow.hidden = YES;
|
||||
[s_updateCoverWindow release];
|
||||
s_updateCoverWindow = nil;
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void IosController::showUpdatePrompt(const QString &title, const QString &message, const QString &updateTitle,
|
||||
const QString &skipTitle, const QString &storeUrl) {
|
||||
#if !MACOS_NE
|
||||
NSString *nsTitle = title.toNSString();
|
||||
NSString *nsMessage = message.toNSString();
|
||||
NSString *nsUpdate = updateTitle.toNSString();
|
||||
NSString *nsSkip = skipTitle.toNSString();
|
||||
NSString *nsUrl = storeUrl.toNSString();
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (!s_updateCoverWindow) {
|
||||
return;
|
||||
}
|
||||
UIViewController *vc = s_updateCoverWindow.rootViewController;
|
||||
|
||||
void (^dismissCover)(void) = ^{
|
||||
s_updateCoverWindow.hidden = YES;
|
||||
[s_updateCoverWindow release];
|
||||
s_updateCoverWindow = nil;
|
||||
};
|
||||
|
||||
UILabel *titleLabel = [[[UILabel alloc] init] autorelease];
|
||||
titleLabel.text = nsTitle;
|
||||
titleLabel.font = [UIFont boldSystemFontOfSize:22];
|
||||
titleLabel.textColor = [UIColor whiteColor];
|
||||
titleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
titleLabel.numberOfLines = 0;
|
||||
|
||||
UILabel *messageLabel = [[[UILabel alloc] init] autorelease];
|
||||
messageLabel.text = nsMessage;
|
||||
messageLabel.font = [UIFont systemFontOfSize:16];
|
||||
messageLabel.textColor = [UIColor colorWithWhite:0.78 alpha:1.0];
|
||||
messageLabel.textAlignment = NSTextAlignmentCenter;
|
||||
messageLabel.numberOfLines = 0;
|
||||
|
||||
UIButton *updateButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[updateButton setTitle:nsUpdate forState:UIControlStateNormal];
|
||||
[updateButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
|
||||
updateButton.backgroundColor = [UIColor colorWithRed:1.0 green:0.6 blue:0.0 alpha:1.0];
|
||||
updateButton.titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
|
||||
updateButton.layer.cornerRadius = 12;
|
||||
[updateButton.heightAnchor constraintEqualToConstant:52].active = YES;
|
||||
[updateButton addAction:[UIAction actionWithHandler:^(__kindof UIAction *action) {
|
||||
NSURL *url = [NSURL URLWithString:nsUrl];
|
||||
if (url) {
|
||||
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
|
||||
}
|
||||
dismissCover();
|
||||
}] forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
UIButton *skipButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[skipButton setTitle:nsSkip forState:UIControlStateNormal];
|
||||
[skipButton setTitleColor:[UIColor colorWithWhite:0.7 alpha:1.0] forState:UIControlStateNormal];
|
||||
skipButton.titleLabel.font = [UIFont systemFontOfSize:17];
|
||||
[skipButton.heightAnchor constraintEqualToConstant:44].active = YES;
|
||||
[skipButton addAction:[UIAction actionWithHandler:^(__kindof UIAction *action) {
|
||||
dismissCover();
|
||||
}] forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
UIStackView *stack = [[[UIStackView alloc] initWithArrangedSubviews:@[titleLabel, messageLabel, updateButton, skipButton]] autorelease];
|
||||
stack.axis = UILayoutConstraintAxisVertical;
|
||||
stack.spacing = 16;
|
||||
stack.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[stack setCustomSpacing:28 afterView:messageLabel];
|
||||
[vc.view addSubview:stack];
|
||||
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[stack.centerYAnchor constraintEqualToAnchor:vc.view.centerYAnchor],
|
||||
[stack.leadingAnchor constraintEqualToAnchor:vc.view.leadingAnchor constant:32],
|
||||
[stack.trailingAnchor constraintEqualToAnchor:vc.view.trailingAnchor constant:-32]
|
||||
]];
|
||||
});
|
||||
#else
|
||||
Q_UNUSED(title) Q_UNUSED(message) Q_UNUSED(updateTitle) Q_UNUSED(skipTitle) Q_UNUSED(storeUrl)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -144,6 +144,30 @@ bool WireguardUtilsLinux::addInterface(const InterfaceConfig& config) {
|
||||
out << key.toLower() << "=" << config.m_specialJunk.value(key) << "\n";
|
||||
}
|
||||
|
||||
if (!config.m_headerProtectionKey.isEmpty()) {
|
||||
QByteArray headerProtectionKey =
|
||||
QByteArray::fromBase64(config.m_headerProtectionKey.toUtf8());
|
||||
out << "header_protection_key=" << QString(headerProtectionKey.toHex()) << "\n";
|
||||
}
|
||||
if (!config.m_contentPaddingAddition.isEmpty()) {
|
||||
out << "content_padding_addition=" << config.m_contentPaddingAddition << "\n";
|
||||
}
|
||||
if (!config.m_rekeyAfterTime.isEmpty()) {
|
||||
out << "rekey_after_time=" << config.m_rekeyAfterTime << "\n";
|
||||
}
|
||||
if (!config.m_rekeyTimeout.isEmpty()) {
|
||||
out << "rekey_timeout=" << config.m_rekeyTimeout << "\n";
|
||||
}
|
||||
if (!config.m_rejectAfterTime.isEmpty()) {
|
||||
out << "reject_after_time=" << config.m_rejectAfterTime << "\n";
|
||||
}
|
||||
if (!config.m_keepaliveTimeout.isEmpty()) {
|
||||
out << "keepalive_timeout=" << config.m_keepaliveTimeout << "\n";
|
||||
}
|
||||
if (!config.m_maxHandshakeAttempts.isEmpty()) {
|
||||
out << "max_handshake_attempts=" << config.m_maxHandshakeAttempts << "\n";
|
||||
}
|
||||
|
||||
int err = uapiErrno(uapiCommand(message));
|
||||
if (err != 0) {
|
||||
logger.error() << "Interface configuration failed:" << strerror(err);
|
||||
@@ -229,7 +253,9 @@ bool WireguardUtilsLinux::updatePeer(const InterfaceConfig& config) {
|
||||
out << config.m_serverPort << "\n";
|
||||
|
||||
out << "replace_allowed_ips=true\n";
|
||||
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
if (!config.m_persistentKeepalive.isEmpty()) {
|
||||
out << "persistent_keepalive_interval=" << config.m_persistentKeepalive << "\n";
|
||||
}
|
||||
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
|
||||
out << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
@@ -142,6 +142,30 @@ bool WireguardUtilsMacos::addInterface(const InterfaceConfig& config) {
|
||||
out << key.toLower() << "=" << config.m_specialJunk.value(key) << "\n";
|
||||
}
|
||||
|
||||
if (!config.m_headerProtectionKey.isEmpty()) {
|
||||
QByteArray headerProtectionKey =
|
||||
QByteArray::fromBase64(config.m_headerProtectionKey.toUtf8());
|
||||
out << "header_protection_key=" << QString(headerProtectionKey.toHex()) << "\n";
|
||||
}
|
||||
if (!config.m_contentPaddingAddition.isEmpty()) {
|
||||
out << "content_padding_addition=" << config.m_contentPaddingAddition << "\n";
|
||||
}
|
||||
if (!config.m_rekeyAfterTime.isEmpty()) {
|
||||
out << "rekey_after_time=" << config.m_rekeyAfterTime << "\n";
|
||||
}
|
||||
if (!config.m_rekeyTimeout.isEmpty()) {
|
||||
out << "rekey_timeout=" << config.m_rekeyTimeout << "\n";
|
||||
}
|
||||
if (!config.m_rejectAfterTime.isEmpty()) {
|
||||
out << "reject_after_time=" << config.m_rejectAfterTime << "\n";
|
||||
}
|
||||
if (!config.m_keepaliveTimeout.isEmpty()) {
|
||||
out << "keepalive_timeout=" << config.m_keepaliveTimeout << "\n";
|
||||
}
|
||||
if (!config.m_maxHandshakeAttempts.isEmpty()) {
|
||||
out << "max_handshake_attempts=" << config.m_maxHandshakeAttempts << "\n";
|
||||
}
|
||||
|
||||
int err = uapiErrno(uapiCommand(message));
|
||||
if (err != 0) {
|
||||
logger.error() << "Interface configuration failed:" << strerror(err);
|
||||
@@ -229,7 +253,9 @@ bool WireguardUtilsMacos::updatePeer(const InterfaceConfig& config) {
|
||||
out << config.m_serverPort << "\n";
|
||||
|
||||
out << "replace_allowed_ips=true\n";
|
||||
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
if (!config.m_persistentKeepalive.isEmpty()) {
|
||||
out << "persistent_keepalive_interval=" << config.m_persistentKeepalive << "\n";
|
||||
}
|
||||
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
|
||||
out << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
@@ -180,7 +180,9 @@ bool WireguardUtilsWindows::updatePeer(const InterfaceConfig& config) {
|
||||
out << config.m_serverPort << "\n";
|
||||
|
||||
out << "replace_allowed_ips=true\n";
|
||||
out << "persistent_keepalive_interval=" << WG_KEEPALIVE_PERIOD << "\n";
|
||||
if (!config.m_persistentKeepalive.isEmpty()) {
|
||||
out << "persistent_keepalive_interval=" << config.m_persistentKeepalive << "\n";
|
||||
}
|
||||
for (const IPAddress& ip : config.m_allowedIPAddressRanges) {
|
||||
out << "allowed_ip=" << ip.toString() << "\n";
|
||||
}
|
||||
|
||||
@@ -227,6 +227,36 @@ bool SubscriptionUiController::importPremiumFromAppStore(const QString &storePro
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::importPremiumFromPlayMarket(const QString &storeProductId)
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
QString productId = storeProductId.trimmed();
|
||||
if (productId.isEmpty()) {
|
||||
productId = QStringLiteral("premium");
|
||||
}
|
||||
|
||||
int duplicateServerIndex = -1;
|
||||
ErrorCode errorCode = m_subscriptionController->processPlayMarketPurchase(
|
||||
m_apiServicesModel->getCountryCode(),
|
||||
m_apiServicesModel->getSelectedServiceType(),
|
||||
m_apiServicesModel->getSelectedServiceProtocol(),
|
||||
productId,
|
||||
&duplicateServerIndex);
|
||||
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
emit installServerFromApiFinished(tr("This subscription has already been added"), duplicateServerIndex);
|
||||
return true;
|
||||
}
|
||||
emit errorOccurred(errorCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit installServerFromApiFinished(tr("%1 has been added to the app").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::restoreServiceFromAppStore()
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
@@ -281,6 +311,59 @@ bool SubscriptionUiController::restoreServiceFromAppStore()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::restoreServiceFromPlayMarket()
|
||||
{
|
||||
#if defined(Q_OS_ANDROID)
|
||||
const QString premiumServiceType = QStringLiteral("amnezia-premium");
|
||||
|
||||
if (!fillAvailableServices()) {
|
||||
qWarning().noquote() << "[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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
SubscriptionController::PlayMarketRestoreResult result = m_subscriptionController->processPlayMarketRestore(
|
||||
m_apiServicesModel->getCountryCode(),
|
||||
m_apiServicesModel->getSelectedServiceType(),
|
||||
m_apiServicesModel->getSelectedServiceProtocol());
|
||||
|
||||
if (!result.hasInstalledConfig) {
|
||||
if (result.duplicateConfigAlreadyPresent) {
|
||||
emit installServerFromApiFinished(tr("This subscription has already been added"), result.duplicateServerIndex);
|
||||
return true;
|
||||
}
|
||||
emit errorOccurred(result.errorCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit installServerFromApiFinished(tr("Subscription restored successfully."));
|
||||
if (result.duplicateCount > 0) {
|
||||
qInfo().noquote() << "[Billing] Skipped" << result.duplicateCount
|
||||
<< "duplicate restored purchases for tokens already processed";
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SubscriptionUiController::importFreeFromGateway()
|
||||
{
|
||||
QString userCountryCode = m_apiServicesModel->getCountryCode();
|
||||
|
||||
@@ -45,8 +45,10 @@ public slots:
|
||||
|
||||
bool fillAvailableServices();
|
||||
bool importPremiumFromAppStore(const QString &storeProductId);
|
||||
bool importPremiumFromPlayMarket(const QString &storeProductId);
|
||||
bool importFreeFromGateway();
|
||||
bool restoreServiceFromAppStore();
|
||||
bool restoreServiceFromPlayMarket();
|
||||
bool importTrialFromGateway(const QString &email);
|
||||
bool updateServiceFromGateway(const QString &serverId, const QString &newCountryCode, const QString &newCountryName,
|
||||
bool reloadServiceConfig = false);
|
||||
|
||||
@@ -29,6 +29,7 @@ void ConnectionUiController::openConnection()
|
||||
{
|
||||
const QString serverId = m_serversController->getDefaultServerId();
|
||||
if (serverId.isEmpty()) {
|
||||
m_connectionController->setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
164
client/ui/controllers/marketplaceUpdateController.cpp
Normal file
164
client/ui/controllers/marketplaceUpdateController.cpp
Normal file
@@ -0,0 +1,164 @@
|
||||
#include "marketplaceUpdateController.h"
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include "version.h"
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLocale>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QRegularExpression>
|
||||
#include <QVersionNumber>
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_IOS)
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_ANDROID)
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
|
||||
namespace
|
||||
{
|
||||
#if defined(Q_OS_IOS)
|
||||
constexpr auto kIosBundleId = "org.amnezia.AmneziaVPN";
|
||||
constexpr auto kIosStoreUrlFallback = "itms-apps://itunes.apple.com/app/id1600529900";
|
||||
#else
|
||||
constexpr auto kAndroidPackage = "org.amnezia.vpn";
|
||||
constexpr auto kAndroidStoreUrl = "https://play.google.com/store/apps/details?id=org.amnezia.vpn";
|
||||
#endif
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
MarketplaceUpdateController::MarketplaceUpdateController(QObject *parent) : QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void MarketplaceUpdateController::start()
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
|
||||
// Cover the app immediately so the store UI is not visible before the check
|
||||
// resolves (avoids a flash of the main window before the update screen).
|
||||
showCover();
|
||||
|
||||
const QUrl url = versionSourceUrl();
|
||||
if (!url.isValid()) {
|
||||
hideCover();
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setTransferTimeout(1000);
|
||||
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, QByteArrayLiteral("AmneziaVPN"));
|
||||
|
||||
QNetworkReply *reply = m_nam.get(request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "[MarketplaceUpdate] network error:" << reply->errorString();
|
||||
hideCover();
|
||||
return;
|
||||
}
|
||||
|
||||
QString version;
|
||||
QString storeUrl;
|
||||
if (!parseStoreVersion(reply->readAll(), version, storeUrl) || version.isEmpty()) {
|
||||
qWarning() << "[MarketplaceUpdate] could not determine store version";
|
||||
hideCover();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto current = QVersionNumber::fromString(QString(APP_VERSION)).normalized();
|
||||
const auto store = QVersionNumber::fromString(version).normalized();
|
||||
qInfo() << "[MarketplaceUpdate] current:" << current.toString() << "store:" << store.toString();
|
||||
|
||||
if (store > current) {
|
||||
showUpdatePrompt(storeUrl);
|
||||
} else {
|
||||
hideCover();
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
|
||||
QUrl MarketplaceUpdateController::versionSourceUrl() const
|
||||
{
|
||||
#if defined(Q_OS_IOS)
|
||||
const QString country = QLocale::system().name().section('_', 1, 1).toLower();
|
||||
QString url = QStringLiteral("https://itunes.apple.com/lookup?bundleId=%1").arg(kIosBundleId);
|
||||
if (!country.isEmpty()) {
|
||||
url += QStringLiteral("&country=%1").arg(country);
|
||||
}
|
||||
return QUrl(url);
|
||||
#else
|
||||
return QUrl(QStringLiteral("https://play.google.com/store/apps/details?id=%1&hl=en&gl=US").arg(kAndroidPackage));
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MarketplaceUpdateController::parseStoreVersion(const QByteArray &body, QString &version, QString &storeUrl)
|
||||
{
|
||||
#if defined(Q_OS_IOS)
|
||||
const auto results = QJsonDocument::fromJson(body).object().value("results").toArray();
|
||||
if (results.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const auto first = results.first().toObject();
|
||||
version = first.value("version").toString();
|
||||
storeUrl = first.value("trackViewUrl").toString();
|
||||
if (storeUrl.isEmpty()) {
|
||||
storeUrl = QString::fromLatin1(kIosStoreUrlFallback);
|
||||
}
|
||||
return !version.isEmpty();
|
||||
#else
|
||||
const QString html = QString::fromUtf8(body);
|
||||
static const QRegularExpression re(QStringLiteral("\\[\\[\\[\"(\\d+\\.\\d+(?:\\.\\d+){0,2})\"\\]\\]"));
|
||||
const auto match = re.match(html);
|
||||
if (match.hasMatch()) {
|
||||
version = match.captured(1);
|
||||
}
|
||||
storeUrl = QString::fromLatin1(kAndroidStoreUrl);
|
||||
return !version.isEmpty();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MarketplaceUpdateController::showCover()
|
||||
{
|
||||
#if defined(Q_OS_IOS)
|
||||
IosController::Instance()->showUpdateCover();
|
||||
#else
|
||||
AndroidController::instance()->showUpdateCover();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MarketplaceUpdateController::hideCover()
|
||||
{
|
||||
#if defined(Q_OS_IOS)
|
||||
IosController::Instance()->hideUpdateCover();
|
||||
#else
|
||||
AndroidController::instance()->hideUpdateCover();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MarketplaceUpdateController::showUpdatePrompt(const QString &storeUrl)
|
||||
{
|
||||
const QString title = tr("Update available");
|
||||
const QString message = tr("A new version of AmneziaVPN is available.");
|
||||
const QString updateTitle = tr("Update");
|
||||
const QString skipTitle = tr("Skip");
|
||||
|
||||
#if defined(Q_OS_IOS)
|
||||
IosController::Instance()->showUpdatePrompt(title, message, updateTitle, skipTitle, storeUrl);
|
||||
#else
|
||||
AndroidController::instance()->showUpdatePrompt(title, message, updateTitle, skipTitle, storeUrl);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
30
client/ui/controllers/marketplaceUpdateController.h
Normal file
30
client/ui/controllers/marketplaceUpdateController.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef MARKETPLACEUPDATECONTROLLER_H
|
||||
#define MARKETPLACEUPDATECONTROLLER_H
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
class MarketplaceUpdateController : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MarketplaceUpdateController(QObject *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void start();
|
||||
|
||||
private:
|
||||
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
|
||||
QUrl versionSourceUrl() const;
|
||||
bool parseStoreVersion(const QByteArray &body, QString &version, QString &storeUrl);
|
||||
void showCover();
|
||||
void hideCover();
|
||||
void showUpdatePrompt(const QString &storeUrl);
|
||||
|
||||
QNetworkAccessManager m_nam;
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // MARKETPLACEUPDATECONTROLLER_H
|
||||
@@ -635,6 +635,7 @@ void InstallUiController::validateConfig()
|
||||
{
|
||||
const QString serverId = m_serversController->getDefaultServerId();
|
||||
if (serverId.isEmpty()) {
|
||||
emit configValidated(false);
|
||||
return;
|
||||
}
|
||||
m_installController->validateConfig(serverId);
|
||||
|
||||
@@ -4,7 +4,7 @@ import QtQuick.Controls
|
||||
Menu {
|
||||
property var textObj
|
||||
|
||||
popupType: Qt.platform.os === "ios" ? Popup.Item : Popup.Native
|
||||
popupType: Popup.Native
|
||||
|
||||
property Item inputBlocker: null
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ Rectangle {
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
ContextMenu.menu: Qt.platform.os === "ios" ? null : contextMenu
|
||||
ContextMenu.menu: contextMenu
|
||||
|
||||
ContextMenuType {
|
||||
id: contextMenu
|
||||
|
||||
@@ -79,7 +79,7 @@ Rectangle {
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
|
||||
ContextMenu.menu: Qt.platform.os === "ios" ? null : contextMenu
|
||||
ContextMenu.menu: contextMenu
|
||||
|
||||
ContextMenuType {
|
||||
id: contextMenu
|
||||
|
||||
@@ -144,7 +144,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
ContextMenu.menu: Qt.platform.os === "ios" ? null : contextMenu
|
||||
ContextMenu.menu: contextMenu
|
||||
|
||||
ContextMenuType {
|
||||
id: contextMenu
|
||||
|
||||
@@ -69,9 +69,7 @@ PageType {
|
||||
rightImageSource: "qrc:/images/controls/chevron-right.svg"
|
||||
|
||||
clickedFunction: function() {
|
||||
if (!isUpdate) {
|
||||
NewsModel.markAsRead(index)
|
||||
}
|
||||
NewsModel.markAsRead(index)
|
||||
NewsModel.processedIndex = index
|
||||
PageController.goToPage(PageEnum.PageSettingsNewsDetail)
|
||||
}
|
||||
|
||||
@@ -187,6 +187,13 @@ PageType {
|
||||
PageController.showBusyIndicator(false)
|
||||
return
|
||||
}
|
||||
if (Qt.platform.os === "android") {
|
||||
PageController.showBusyIndicator(true)
|
||||
var androidStoreId = plan.storeProductId !== undefined ? String(plan.storeProductId) : ""
|
||||
SubscriptionUiController.importPremiumFromPlayMarket(androidStoreId)
|
||||
PageController.showBusyIndicator(false)
|
||||
return
|
||||
}
|
||||
if (plan.checkoutUrl) {
|
||||
Qt.openUrlExternally(plan.checkoutUrl)
|
||||
PageController.closePage()
|
||||
|
||||
@@ -377,10 +377,14 @@ 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 || IsPlayBuild
|
||||
property var handler: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
SubscriptionUiController.restoreServiceFromAppStore()
|
||||
if (Qt.platform.os === "android") {
|
||||
SubscriptionUiController.restoreServiceFromPlayMarket()
|
||||
} else {
|
||||
SubscriptionUiController.restoreServiceFromAppStore()
|
||||
}
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,11 +408,10 @@ void VpnConnection::appendSplitTunnelingConfig()
|
||||
for (auto &line : nativeConfigLines) {
|
||||
if (line.contains("PersistentKeepalive")) {
|
||||
auto persistentKeepaliveString = line.split(" = ");
|
||||
if (persistentKeepaliveString.size() < 1) {
|
||||
break;
|
||||
if (persistentKeepaliveString.size() > 1) {
|
||||
configData.insert(configKey::persistentKeepAlive, persistentKeepaliveString.at(1));
|
||||
m_vpnConfiguration.insert(protocolName + "_config_data", configData);
|
||||
}
|
||||
configData.insert(configKey::persistentKeepAlive, persistentKeepaliveString.at(1));
|
||||
m_vpnConfiguration.insert(protocolName + "_config_data", configData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
12
conanfile.py
12
conanfile.py
@@ -19,26 +19,26 @@ class AmneziaVPN(ConanFile):
|
||||
|
||||
if has_service:
|
||||
if os == "Windows":
|
||||
self.requires("awg-windows/0.1.9")
|
||||
self.requires("awg-windows/3.0.2")
|
||||
self.requires("tap-windows6/9.27.0")
|
||||
self.requires("win-split-tunnel/1.2.5.0")
|
||||
self.requires("wintun/0.14.1")
|
||||
else:
|
||||
self.requires("awg-go/0.2.18")
|
||||
self.requires("awg-go/3.0.1")
|
||||
|
||||
self.requires("amnezia-xray-bindings/1.1.0")
|
||||
self.requires("amnezia-xray-bindings/1.3.0")
|
||||
self.requires("tun2socks/2.6.0")
|
||||
self.requires("openvpn/2.7.0")
|
||||
self.requires("v2ray-rules-dat/202603162227")
|
||||
|
||||
if has_ne:
|
||||
self.requires("awg-apple/2.0.2")
|
||||
self.requires("awg-apple/3.0.1")
|
||||
self.requires("hev-socks5-tunnel/2.15.0", options={"as_framework": True})
|
||||
self.requires("openvpnadapter/1.0.0")
|
||||
|
||||
if os == "Android":
|
||||
self.requires("amnezia-libxray/1.0.0")
|
||||
self.requires("awg-android/2.0.1")
|
||||
self.requires("amnezia-libxray/1.0.2")
|
||||
self.requires("awg-android/3.0.1")
|
||||
self.requires("openvpn-pt-android/1.0.0")
|
||||
|
||||
# expicitly use libssh@amnezia to prevent it from being downloaded from conan-center
|
||||
|
||||
@@ -33,6 +33,8 @@ while [[ $# -gt 0 ]]; do
|
||||
--abi) abis+=("$2"); shift 2 ;;
|
||||
--sign) : ${SIGN:=true}; shift ;;
|
||||
--aab) : ${BUILD_AAB=true}; shift ;;
|
||||
--apk) : ${BUILD_APK=true}; shift ;;
|
||||
--play) : ${BUILD_PLAY=true}; shift ;;
|
||||
--help|-h|?)
|
||||
echo "Usage: $0 [options]"
|
||||
echo " Options:"
|
||||
@@ -45,6 +47,8 @@ while [[ $# -gt 0 ]]; do
|
||||
echo " --abi - specify Android ABIs for target to build for. all by default"
|
||||
echo " --sign - whether to sign the resulting files. only appicable to Android"
|
||||
echo " --aab - whether to build AAB. only applicable to Android"
|
||||
echo " --apk - whether to build APK. use with --play. only applicable to Android"
|
||||
echo " --play - build Play flavor (Google Play Billing). use with --aab or --apk. only applicable to Android"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown arg \"$1\". Use $0 -h to get help"; exit 1 ;;
|
||||
@@ -201,6 +205,7 @@ args=()
|
||||
[[ -n "$QT_ANDROID_SIGN_AAB" ]] && args+=("-DQT_ANDROID_SIGN_AAB=$QT_ANDROID_SIGN_AAB")
|
||||
[[ -n "$QT_ANDROID_ABIS" ]] && args+=("-DQT_ANDROID_ABIS=$QT_ANDROID_ABIS")
|
||||
[[ -n "$QT_ANDROID_BUILD_ALL_ABIS" ]] && args+=("-DQT_ANDROID_BUILD_ALL_ABIS=$QT_ANDROID_BUILD_ALL_ABIS")
|
||||
[[ -n "$BUILD_PLAY" ]] && args+=("-DANDROID_BUILD_PLAY=ON")
|
||||
|
||||
if [[ -n "$FORCE" ]]; then
|
||||
run_traced rm -rf "$BUILD_PATH"
|
||||
@@ -209,7 +214,17 @@ fi
|
||||
run_traced cmake -S "$SOURCE_PATH" -B "$BUILD_PATH" "${args[@]}"
|
||||
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE"
|
||||
|
||||
[[ -n "$BUILD_AAB" ]] && run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "aab"
|
||||
if [[ -n "$BUILD_AAB" ]]; then
|
||||
if [[ -n "$BUILD_PLAY" ]]; then
|
||||
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "android_play_aab"
|
||||
else
|
||||
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "aab"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$BUILD_APK" ]] && [[ -n "$BUILD_PLAY" ]]; then
|
||||
run_traced cmake --build "$BUILD_PATH" --config "$CMAKE_BUILD_TYPE" -t "android_play_apk"
|
||||
fi
|
||||
|
||||
if [ -z "$no_installers" ]; then
|
||||
for installer in $INSTALLERS; do
|
||||
|
||||
3
recipes/amnezia-libxray/conandata.yml
Normal file
3
recipes/amnezia-libxray/conandata.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
patches:
|
||||
"1.0.1":
|
||||
- patch_file: "patches/0001-add-16kb-page-support-amnezia-libxray.patch"
|
||||
@@ -1,5 +1,5 @@
|
||||
from conan import ConanFile
|
||||
from conan.tools.files import get, copy
|
||||
from conan.tools.files import get, copy, apply_conandata_patches, export_conandata_patches
|
||||
from conan.tools.layout import basic_layout
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
from conan.tools.env import Environment
|
||||
@@ -11,9 +11,12 @@ from pathlib import Path
|
||||
|
||||
class AmneziaLibxray(ConanFile):
|
||||
name = "amnezia-libxray"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
settings = "os", "arch", "compiler"
|
||||
|
||||
def export_sources(self):
|
||||
export_conandata_patches(self)
|
||||
|
||||
def configure(self):
|
||||
self.settings.rm_safe("compiler.libcxx")
|
||||
self.settings.rm_safe("compiler.cppstd")
|
||||
@@ -29,8 +32,8 @@ class AmneziaLibxray(ConanFile):
|
||||
raise ConanInvalidConfiguration(f"{self.name} v{self.version} does not support {self.settings.os}")
|
||||
|
||||
def source(self):
|
||||
get(self, "https://github.com/amnezia-vpn/amnezia-libxray/archive/refs/tags/v1.0.0.zip",
|
||||
sha256="0c50c5acd5063a9fc3cfbb5b3e11481d30cfa3762b3cb1d72130248ff498e9df", strip_root=True
|
||||
get(self, f"https://github.com/amnezia-vpn/amnezia-libxray/archive/refs/tags/v{self.version}.zip",
|
||||
sha256="a434a84574b021c4bcd52d9edc906e5ec03eaaab98c192f683f84294b3e4e79c", strip_root=True
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
@@ -44,18 +47,22 @@ class AmneziaLibxray(ConanFile):
|
||||
env.vars(self).save_script("conan_provide_androidhome")
|
||||
|
||||
def _patch_sources(self):
|
||||
apply_conandata_patches(self)
|
||||
build_path = os.path.join(self.build_folder, "build.sh")
|
||||
build_stat = os.stat(build_path)
|
||||
os.chmod(build_path, build_stat.st_mode | stat.S_IEXEC)
|
||||
|
||||
def build(self):
|
||||
self._patch_sources()
|
||||
self.run("./build.sh android")
|
||||
if self.settings_build.os == "Windows":
|
||||
self.run("bash build.sh android")
|
||||
else:
|
||||
self.run("./build.sh android")
|
||||
|
||||
def package(self):
|
||||
copy(self, "libxray.aar", src=self.build_folder, dst=os.path.join(self.package_folder, "aar"))
|
||||
|
||||
def package_info(self):
|
||||
self.cpp_info.set_property("cmake_extra_variables", {
|
||||
"AMNEZIA_LIBXRAY_PATH": os.path.join(self.package_folder, "aar", "libxray.aar"),
|
||||
"AMNEZIA_LIBXRAY_PATH": Path(self.package_folder, "aar", "libxray.aar").as_posix(),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
From 0531cd00cd580524ec21a663442264136dbd368f Mon Sep 17 00:00:00 2001
|
||||
From: NickVs2015 <nv@amnezia.org>
|
||||
Date: Thu, 23 Jul 2026 11:23:07 +0300
|
||||
Subject: [PATCH] fix: add 16kb page size support for android build
|
||||
|
||||
---
|
||||
build.sh | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/build.sh b/build.sh
|
||||
index 7969505..02c5b73 100755
|
||||
--- a/build.sh
|
||||
+++ b/build.sh
|
||||
@@ -28,7 +28,7 @@ build_android() {
|
||||
rm -fr assets
|
||||
mkdir -p assets/geo
|
||||
mv dat/* assets/geo
|
||||
- gomobile bind -target android -androidapi 24 -javapkg=org.amnezia.vpn.protocol.xray -o libxray.aar -ldflags="-w -s -buildid= -checklinkname=0" -trimpath
|
||||
+ gomobile bind -target android -androidapi 24 -javapkg=org.amnezia.vpn.protocol.xray -o libxray.aar -ldflags="-w -s -buildid= -checklinkname=0 -extldflags=-Wl,-z,max-page-size=16384" -trimpath
|
||||
}
|
||||
|
||||
download_geo() {
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
@@ -13,7 +13,7 @@ import shlex
|
||||
|
||||
class AmneziaXrayBindings(ConanFile):
|
||||
name = "amnezia-xray-bindings"
|
||||
version = "1.1.0"
|
||||
version = "1.3.0"
|
||||
settings = "os", "arch", "compiler"
|
||||
|
||||
_arch_map = {
|
||||
@@ -76,8 +76,8 @@ class AmneziaXrayBindings(ConanFile):
|
||||
)
|
||||
|
||||
def source(self):
|
||||
get(self, "https://github.com/amnezia-vpn/amnezia-xray-bindings/archive/v1.1.0.zip",
|
||||
sha256="6ea768ec7002cedd422a39aea17704b888acaf794432aa5937cfc92fb6d80eb5", strip_root=True)
|
||||
get(self, f"https://github.com/amnezia-vpn/amnezia-xray-bindings/archive/refs/tags/v{self.version}.zip",
|
||||
sha256="97233926c91e0bed61603fddb9909607b97a65f8ff0841a628f96268637ade5c", strip_root=True)
|
||||
|
||||
def generate(self):
|
||||
tc = AutotoolsToolchain(self)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from conan import ConanFile
|
||||
from conan.tools.cmake import cmake_layout, CMake, CMakeToolchain
|
||||
from conan.tools.files import copy, replace_in_file
|
||||
from conan.tools.env import VirtualBuildEnv, Environment
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
from conan.tools.scm import Git
|
||||
|
||||
import os
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
class AwgAndroid(ConanFile):
|
||||
name = "awg-android"
|
||||
version = "2.0.1"
|
||||
version = "3.0.1"
|
||||
settings = "os", "arch", "build_type", "compiler"
|
||||
|
||||
def configure(self):
|
||||
@@ -21,6 +23,11 @@ class AwgAndroid(ConanFile):
|
||||
|
||||
def build_requirements(self):
|
||||
self.tool_requires("cmake/[>=3.4.1 <4]")
|
||||
if platform.system() == "Windows":
|
||||
self.tool_requires("ninja/[*]")
|
||||
self.tool_requires("go/[*]")
|
||||
if not self.conf.get("tools.microsoft.bash:path", check_type=str):
|
||||
self.tool_requires("msys2/cci.latest")
|
||||
|
||||
def validate(self):
|
||||
if self.settings.os != "Android":
|
||||
@@ -35,9 +42,11 @@ class AwgAndroid(ConanFile):
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
VirtualBuildEnv(self).generate()
|
||||
|
||||
tc = CMakeToolchain(self)
|
||||
tc.variables["GRADLE_USER_HOME"] = os.path.join(self.build_folder, "gradle_user_home")
|
||||
tc.variables["CMAKE_LIBRARY_OUTPUT_DIRECTORY"] = os.path.join(self.build_folder, "out")
|
||||
tc.variables["GRADLE_USER_HOME"] = Path(os.path.join(self.build_folder, "gradle_user_home")).as_posix()
|
||||
tc.variables["CMAKE_LIBRARY_OUTPUT_DIRECTORY"] = Path(os.path.join(self.build_folder, "out")).as_posix()
|
||||
# not to warn in case of strtok() usage
|
||||
tc.extra_cflags = ["-Wno-deprecated-declarations"]
|
||||
tc.generate()
|
||||
@@ -64,6 +73,31 @@ class AwgAndroid(ConanFile):
|
||||
'sha256sum -c',
|
||||
'shasum -a 256 -c'
|
||||
)
|
||||
elif platform.system() == 'Windows':
|
||||
# elf-cleaner uses sys/mman.h (POSIX only) and cannot be built on Windows;
|
||||
# skip it — DT_FLAGS_1 warnings only affect Android < 6.0
|
||||
replace_in_file(self,
|
||||
os.path.join(self.source_folder, "tunnel", "tools", "CMakeLists.txt"),
|
||||
'# Strip unwanted ELF sections to prevent DT_FLAGS_1 warnings on old Android versions\n'
|
||||
'file(GLOB ELF_CLEANER_SOURCES elf-cleaner/*.c elf-cleaner/*.cpp)\n'
|
||||
'add_custom_target(elf-cleaner COMMENT "Building elf-cleaner" VERBATIM COMMAND cc\n'
|
||||
' -O2 -DPACKAGE_NAME="elf-cleaner" -DPACKAGE_VERSION="" -DCOPYRIGHT=""\n'
|
||||
' -o "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner" ${ELF_CLEANER_SOURCES}\n'
|
||||
')\n'
|
||||
'add_custom_command(TARGET libwg.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"\n'
|
||||
' --api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg.so>")\n'
|
||||
'add_dependencies(libwg.so elf-cleaner)\n'
|
||||
'add_custom_command(TARGET libwg-quick.so POST_BUILD VERBATIM COMMAND "${CMAKE_CURRENT_BINARY_DIR}/elf-cleaner"\n'
|
||||
' --api-level "${ANDROID_NATIVE_API_LEVEL}" "$<TARGET_FILE:libwg-quick.so>")\n'
|
||||
'add_dependencies(libwg-quick.so elf-cleaner)',
|
||||
'',
|
||||
)
|
||||
# patch Makefile: skip Go download, use 'go' already in PATH from tool_requires
|
||||
replace_in_file(self,
|
||||
os.path.join(self.source_folder, "tunnel", "tools", "libwg-go", "Makefile"),
|
||||
'$(DESTDIR)/libwg-go.so: export PATH := $(BUILDDIR)/go-$(GO_VERSION)/bin/:$(PATH)\n$(DESTDIR)/libwg-go.so: $(BUILDDIR)/go-$(GO_VERSION)/.prepared go.mod',
|
||||
'$(DESTDIR)/libwg-go.so: go.mod',
|
||||
)
|
||||
|
||||
def build(self):
|
||||
self._patch_sources()
|
||||
@@ -81,6 +115,6 @@ class AwgAndroid(ConanFile):
|
||||
self.cpp_info.set_property("cmake_target_name", "amnezia::awg-android")
|
||||
self.cpp_info.libs = [ "wg-go" ]
|
||||
self.cpp_info.set_property("cmake_extra_variables", {
|
||||
"AMNEZIA_ANDROID_LIBWG_PATH": os.path.join(self.package_folder, "bin", "libwg.so"),
|
||||
"AMNEZIA_ANDROID_LIBWG_QUICK_PATH": os.path.join(self.package_folder, "bin", "libwg-quick.so"),
|
||||
"AMNEZIA_ANDROID_LIBWG_PATH": Path(os.path.join(self.package_folder, "bin", "libwg.so")).as_posix(),
|
||||
"AMNEZIA_ANDROID_LIBWG_QUICK_PATH": Path(os.path.join(self.package_folder, "bin", "libwg-quick.so")).as_posix(),
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import os
|
||||
|
||||
class AwgApple(ConanFile):
|
||||
name = "awg-apple"
|
||||
version = "2.0.2"
|
||||
version = "3.0.1"
|
||||
settings = "os", "arch", "compiler"
|
||||
|
||||
@property
|
||||
@@ -39,7 +39,7 @@ class AwgApple(ConanFile):
|
||||
|
||||
def source(self):
|
||||
get(self, f"https://github.com/amnezia-vpn/amneziawg-apple/archive/refs/tags/v{self.version}.zip",
|
||||
sha256="a04f49eac9f82bbf5dd9031bab188d44de2b3482efde1b6e970821de1d5a3c5d", strip_root=True
|
||||
sha256="8efd086e3c51637270be105336692ca89b396b2ea9b71ea30ce28292c61c76e8", strip_root=True
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
|
||||
@@ -14,7 +14,7 @@ import shlex
|
||||
|
||||
class AwgGo(ConanFile):
|
||||
name = "awg-go"
|
||||
version = "0.2.18"
|
||||
version = "3.0.1"
|
||||
package_type = "application"
|
||||
settings = "os", "arch"
|
||||
|
||||
@@ -61,7 +61,7 @@ class AwgGo(ConanFile):
|
||||
|
||||
def source(self):
|
||||
get(self, f"https://github.com/amnezia-vpn/amneziawg-go/archive/refs/tags/v{self.version}.zip",
|
||||
sha256="58eefbd012e79bd1525f0e02d748979e9480acc1a339df8ceb3b9ffafcedb1ba", strip_root=True
|
||||
sha256="3ab9655b894c6fe43dfcba6d1e3cbfff20ca1f1928eae6242875541e82488247", strip_root=True
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
|
||||
@@ -8,7 +8,7 @@ import os
|
||||
|
||||
class AwgWindows(ConanFile):
|
||||
name = "awg-windows"
|
||||
version = "0.1.9"
|
||||
version = "3.0.2"
|
||||
settings = "os", "arch"
|
||||
|
||||
@property
|
||||
@@ -63,7 +63,7 @@ class AwgWindows(ConanFile):
|
||||
|
||||
def source(self):
|
||||
get(self, f"https://github.com/amnezia-vpn/amneziawg-windows/archive/refs/tags/v{self.version}.zip",
|
||||
sha256="5c29a75cb2beae291cc51b64840a39f838da5f300b9e956f7964813a687ec74c", strip_root=True)
|
||||
sha256="e5755ef1e19fd8408881cab49684d37ee4a0822d706960bbabe89770f7c436f1", strip_root=True)
|
||||
|
||||
def generate(self):
|
||||
tc = AutotoolsToolchain(self)
|
||||
|
||||
@@ -5,6 +5,7 @@ from conan.tools.scm import Git
|
||||
from conan.errors import ConanInvalidConfiguration
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
class OpenvpnPtAndroid(ConanFile):
|
||||
name = "openvpn-pt-android"
|
||||
@@ -53,5 +54,5 @@ class OpenvpnPtAndroid(ConanFile):
|
||||
self.cpp_info.set_property("cmake_target_name", "amnezia::openvpn-pt-android")
|
||||
self.cpp_info.libs = [ "ovpn3", "ovpnutil", "rsapss" ]
|
||||
self.cpp_info.set_property("cmake_extra_variables", {
|
||||
"OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH": os.path.join(self.package_folder, "lib", "libck-ovpn-plugin.so")
|
||||
"OPENVPN_PT_ANDROID_LIBCK_OVPN_PLUGIN_PATH": Path(self.package_folder, "lib", "libck-ovpn-plugin.so").as_posix()
|
||||
})
|
||||
|
||||
@@ -333,6 +333,12 @@ if(APPLE)
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
COMPONENT AmneziaVPN
|
||||
)
|
||||
add_custom_command(TARGET ${PROJECT} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E $<IF:$<CONFIG:Debug>,copy_directory,true>
|
||||
${CMAKE_SOURCE_DIR}/deploy/data/macos/pf
|
||||
"$<TARGET_FILE_DIR:${PROJECT}>/pf"
|
||||
COMMAND_EXPAND_LISTS
|
||||
)
|
||||
endif()
|
||||
|
||||
# install non-linked dependencies
|
||||
|
||||
Reference in New Issue
Block a user