mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-30 23:31:14 +03:00
Compare commits
18 Commits
feat/proxy
...
feature/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c168bc9ed | ||
|
|
0d7f9381c1 | ||
|
|
fe99cdeb85 | ||
|
|
4c03463344 | ||
|
|
d4f6add807 | ||
|
|
545c766732 | ||
|
|
494e93d4ab | ||
|
|
4b6ec29761 | ||
|
|
daa44a2672 | ||
|
|
5e23eed2bc | ||
|
|
012135aea6 | ||
|
|
58acf71858 | ||
|
|
02be6dc5f9 | ||
|
|
bfcf7f0305 | ||
|
|
2bce595ade | ||
|
|
cd1e561fd4 | ||
|
|
9bd1e6a0f5 | ||
|
|
5058c9aa6f |
64
.github/workflows/deploy.yml
vendored
64
.github/workflows/deploy.yml
vendored
@@ -660,15 +660,57 @@ jobs:
|
||||
ANDROID_KEYSTORE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
|
||||
ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
shell: bash
|
||||
run: ./deploy/build_android.sh --aab --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
|
||||
run: ./deploy/build_android.sh --aab --play --apk all --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
|
||||
|
||||
- name: 'Build OSS AAB (in-app purchase)'
|
||||
env:
|
||||
ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
QT_HOST_PATH: ${{ runner.temp }}/Qt/${{ env.QT_VERSION }}/gcc_64
|
||||
ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/android.keystore
|
||||
ANDROID_KEYSTORE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_ALIAS }}
|
||||
ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_RELEASE_KEYSTORE_KEY_PASS }}
|
||||
shell: bash
|
||||
run: ./deploy/build_android.sh --aab --build-platform ${{ env.ANDROID_BUILD_PLATFORM }}
|
||||
|
||||
- name: 'Upload OSS x86_64 apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-x86_64
|
||||
path: deploy/build/AmneziaVPN-oss-x86_64-release.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload OSS x86 apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-x86
|
||||
path: deploy/build/AmneziaVPN-oss-x86-release.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload OSS arm64-v8a apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-arm64-v8a
|
||||
path: deploy/build/AmneziaVPN-oss-arm64-v8a-release.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload OSS armeabi-v7a apk'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-armeabi-v7a
|
||||
path: deploy/build/AmneziaVPN-oss-armeabi-v7a-release.apk
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Rename Android APKs'
|
||||
run: |
|
||||
cd deploy/build
|
||||
mv AmneziaVPN-x86_64-release.apk AmneziaVPN_${VERSION}_android9+_x86_64.apk
|
||||
mv AmneziaVPN-x86-release.apk AmneziaVPN_${VERSION}_android9+_x86.apk
|
||||
mv AmneziaVPN-arm64-v8a-release.apk AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
|
||||
mv AmneziaVPN-armeabi-v7a-release.apk AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
|
||||
mv AmneziaVPN-oss-x86_64-release.apk AmneziaVPN_${VERSION}_android9+_x86_64.apk
|
||||
mv AmneziaVPN-oss-x86-release.apk AmneziaVPN_${VERSION}_android9+_x86.apk
|
||||
mv AmneziaVPN-oss-arm64-v8a-release.apk AmneziaVPN_${VERSION}_android9+_arm64-v8a.apk
|
||||
mv AmneziaVPN-oss-armeabi-v7a-release.apk AmneziaVPN_${VERSION}_android9+_armeabi-v7a.apk
|
||||
cd ../..
|
||||
|
||||
- name: 'Upload x86_64 apk'
|
||||
@@ -703,11 +745,19 @@ jobs:
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload aab'
|
||||
- name: 'Upload Play AAB'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android
|
||||
path: deploy/build/AmneziaVPN-release.aab
|
||||
path: deploy/build/AmneziaVPN-play-release.aab
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
- name: 'Upload OSS AAB (in-app purchase)'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AmneziaVPN-android-oss-aab
|
||||
path: deploy/build/AmneziaVPN-oss-release.aab
|
||||
compression-level: 0
|
||||
retention-days: 7
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||
|
||||
set(PROJECT AmneziaVPN)
|
||||
set(AMNEZIAVPN_VERSION 4.8.13.0)
|
||||
set(AMNEZIAVPN_VERSION 4.8.13.1)
|
||||
|
||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||
DESCRIPTION "AmneziaVPN"
|
||||
@@ -12,7 +12,9 @@ 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 2106)
|
||||
|
||||
set(APP_ANDROID_VERSION_CODE 2107)
|
||||
|
||||
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(MZ_PLATFORM_NAME "linux")
|
||||
@@ -61,6 +63,7 @@ if(WIN32 AND NOT IOS AND NOT ANDROID AND NOT MACOS_NE)
|
||||
set(CPACK_PACKAGE_VENDOR "AmneziaVPN")
|
||||
set(CPACK_PACKAGE_VERSION ${AMNEZIAVPN_VERSION})
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "AmneziaVPN client")
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "AmneziaVPN")
|
||||
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||
set(CPACK_PACKAGE_EXECUTABLES "AmneziaVPN" "AmneziaVPN")
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
320
client/android/billing/src/main/kotlin/BillingProvider.kt
Normal file
320
client/android/billing/src/main/kotlin/BillingProvider.kt
Normal file
@@ -0,0 +1,320 @@
|
||||
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
|
||||
|
||||
private const val TAG = "BillingProvider"
|
||||
private const val PRODUCT_ID = "premium"
|
||||
|
||||
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 ->
|
||||
JSONObject().also { pricingPhases.put(it) }
|
||||
.put("billingCycleCount", phase.billingCycleCount)
|
||||
.put("billingPeriod", phase.billingPeriod)
|
||||
.put("formatedPrice", phase.formattedPrice)
|
||||
.put("recurrenceMode", phase.recurrenceMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 ->
|
||||
/* 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
|
||||
}
|
||||
|
||||
@@ -41,17 +42,6 @@ android {
|
||||
resourceConfigurations += listOf("en", "ru", "b+zh+Hans")
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
java.setSrcDirs(listOf("src"))
|
||||
res.setSrcDirs(listOf("res"))
|
||||
// androyddeployqt creates the folders below
|
||||
assets.setSrcDirs(listOf("assets"))
|
||||
jniLibs.setSrcDirs(listOf("libs"))
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
register("release") {
|
||||
storeFile = providers.environmentVariable("ANDROID_KEYSTORE_PATH").orNull?.let { file(it) }
|
||||
@@ -77,6 +67,36 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += "billing"
|
||||
|
||||
productFlavors {
|
||||
create("oss") {
|
||||
dimension = "billing"
|
||||
}
|
||||
create("play") {
|
||||
dimension = "billing"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
java.setSrcDirs(listOf("src"))
|
||||
res.setSrcDirs(listOf("res"))
|
||||
// androyddeployqt creates the folders below
|
||||
assets.setSrcDirs(listOf("assets"))
|
||||
jniLibs.setSrcDirs(listOf("libs"))
|
||||
}
|
||||
|
||||
getByName("oss") {
|
||||
java.setSrcDirs(listOf("oss"))
|
||||
}
|
||||
|
||||
getByName("play") {
|
||||
java.setSrcDirs(listOf("play"))
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
isEnable = true
|
||||
@@ -122,4 +142,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.5.2"
|
||||
kotlin = "1.9.24"
|
||||
android-billing = "7.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()
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ rootProject.buildFileName = "build.gradle.kts"
|
||||
|
||||
include(":qt")
|
||||
include(":utils")
|
||||
include(":billing")
|
||||
include(":protocolApi")
|
||||
include(":wireguard")
|
||||
include(":awg")
|
||||
|
||||
@@ -55,7 +55,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
|
||||
@@ -87,6 +86,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>()
|
||||
@@ -199,6 +199,7 @@ class AmneziaActivity : QtActivity() {
|
||||
registerBroadcastReceivers()
|
||||
intent?.let(::processIntent)
|
||||
runBlocking { vpnProto = proto.await() }
|
||||
billingRepository = BillingPaymentRepository(applicationContext)
|
||||
}
|
||||
|
||||
private fun loadLibs() {
|
||||
@@ -932,15 +933,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")
|
||||
@@ -1111,11 +1106,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
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
message("Client android ${CMAKE_ANDROID_ARCH_ABI} build")
|
||||
|
||||
# Option to build Play variant (with Google Play Billing) instead of OSS
|
||||
# When ON, adds target android_play_apk: cmake --build . --target android_play_apk
|
||||
option(ANDROID_BUILD_PLAY "Add android_play_apk target for Google Play Billing build" OFF)
|
||||
|
||||
set(APP_ANDROID_MIN_SDK 28)
|
||||
set(ANDROID_PLATFORM "android-${APP_ANDROID_MIN_SDK}" CACHE STRING
|
||||
"The minimum API level supported by the application or library" FORCE)
|
||||
@@ -57,3 +61,22 @@ endforeach()
|
||||
|
||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/3rd-prebuilt/3rd-prebuilt/xray/android/libxray.aar
|
||||
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/android/xray/libXray)
|
||||
|
||||
# Custom target to build Play variant (with Google Play Billing)
|
||||
# Enable with: cmake -DANDROID_BUILD_PLAY=ON ...
|
||||
# Then run: cmake --build <build_dir> --target android_play_apk
|
||||
# Note: Do a normal build first so androiddeployqt creates the android-build folder
|
||||
if(ANDROID_BUILD_PLAY)
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
set(_gradle_suffix "Debug")
|
||||
else()
|
||||
set(_gradle_suffix "Release")
|
||||
endif()
|
||||
set(_android_build_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build-${PROJECT}")
|
||||
add_custom_target(android_play_apk
|
||||
COMMAND ./gradlew assemblePlay${_gradle_suffix} -DexplicitRun=1
|
||||
WORKING_DIRECTORY "${_android_build_dir}"
|
||||
COMMENT "Building Android Play variant (assemblePlay${_gradle_suffix})"
|
||||
DEPENDS ${PROJECT}
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -123,6 +123,7 @@ namespace amnezia
|
||||
ApiUpdateRequestError = 1111,
|
||||
ApiSubscriptionExpiredError = 1112,
|
||||
ApiPurchaseError = 1113,
|
||||
ApiNoPurchasesToRestore = 1114,
|
||||
|
||||
// QFile errors
|
||||
OpenError = 1200,
|
||||
@@ -130,7 +131,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)
|
||||
}
|
||||
|
||||
@@ -80,6 +80,15 @@ QString errorString(ErrorCode code) {
|
||||
case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break;
|
||||
case (ErrorCode::ApiSubscriptionExpiredError): errorMessage = QObject::tr("Your Amnezia Premium subscription has expired.\n Please check your email for renewal instructions.\n If you haven't received an email, please contact our support."); break;
|
||||
case (ErrorCode::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break;
|
||||
case (ErrorCode::ApiNoPurchasesToRestore):
|
||||
#if defined(Q_OS_ANDROID)
|
||||
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same Google account used for the purchase.");
|
||||
#elif defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same Apple ID used for the purchase.");
|
||||
#else
|
||||
errorMessage = QObject::tr("No purchases to restore. If you have an active subscription, make sure you're signed in with the same account used for the purchase.");
|
||||
#endif
|
||||
break;
|
||||
|
||||
// QFile errors
|
||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||
@@ -89,6 +98,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;
|
||||
|
||||
@@ -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) \
|
||||
{ \
|
||||
|
||||
@@ -72,9 +72,9 @@ void NetworkWatcher::initialize() {
|
||||
connect(m_impl, &NetworkWatcherImpl::unsecuredNetwork, this,
|
||||
&NetworkWatcher::unsecuredNetwork);
|
||||
connect(m_impl, &NetworkWatcherImpl::networkChanged, this,
|
||||
&NetworkWatcher::networkChange);
|
||||
connect(m_impl, &NetworkWatcherImpl::sleepMode, this,
|
||||
&NetworkWatcher::onSleepMode);
|
||||
&NetworkWatcher::networkChanged);
|
||||
connect(m_impl, &NetworkWatcherImpl::wakeup, this,
|
||||
&NetworkWatcher::wakeup);
|
||||
m_impl->initialize();
|
||||
|
||||
// Enable sleep/wake monitoring for VPN auto-reconnection
|
||||
@@ -97,12 +97,6 @@ void NetworkWatcher::settingsChanged() {
|
||||
logger.debug() << "NetworkWatcher settings changed - keeping sleep monitoring active";
|
||||
}
|
||||
|
||||
void NetworkWatcher::onSleepMode()
|
||||
{
|
||||
logger.debug() << "Resumed from sleep mode";
|
||||
emit sleepMode();
|
||||
}
|
||||
|
||||
void NetworkWatcher::unsecuredNetwork(const QString& networkName,
|
||||
const QString& networkId) {
|
||||
logger.debug() << "Unsecured network:" << logger.sensitive(networkName)
|
||||
|
||||
@@ -29,13 +29,11 @@ public:
|
||||
// false to restore.
|
||||
void simulateDisconnection(bool simulatedDisconnection);
|
||||
|
||||
void onSleepMode();
|
||||
|
||||
QNetworkInformation::Reachability getReachability();
|
||||
|
||||
signals:
|
||||
void networkChange();
|
||||
void sleepMode();
|
||||
void networkChanged();
|
||||
void wakeup();
|
||||
|
||||
private:
|
||||
void settingsChanged();
|
||||
|
||||
@@ -41,7 +41,7 @@ signals:
|
||||
// TODO: Only windows-networkwatcher has this, the other plattforms should
|
||||
// too.
|
||||
void networkChanged(QString newBSSID);
|
||||
void sleepMode();
|
||||
void wakeup();
|
||||
|
||||
|
||||
private:
|
||||
|
||||
@@ -326,6 +326,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,13 @@ 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();
|
||||
|
||||
static bool initLogging();
|
||||
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
|
||||
|
||||
@@ -41,8 +41,8 @@ void LinuxNetworkWatcher::initialize() {
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::unsecuredNetwork, this,
|
||||
&LinuxNetworkWatcher::unsecuredNetwork);
|
||||
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::sleepMode, this,
|
||||
&NetworkWatcherImpl::sleepMode);
|
||||
connect(m_worker, &LinuxNetworkWatcherWorker::wakeup, this,
|
||||
&NetworkWatcherImpl::wakeup);
|
||||
|
||||
// Let's wait a few seconds to allow the UI to be fully loaded and shown.
|
||||
// This is not strictly needed, but it's better for user experience because
|
||||
|
||||
@@ -200,7 +200,7 @@ void LinuxNetworkWatcherWorker::checkDevices() {
|
||||
void LinuxNetworkWatcherWorker::NMStateChanged(quint32 state)
|
||||
{
|
||||
if (state == NM_STATE_ASLEEP) {
|
||||
emit sleepMode();
|
||||
emit wakeup();
|
||||
}
|
||||
|
||||
logger.debug() << "NMStateChanged " << state;
|
||||
|
||||
@@ -23,7 +23,7 @@ class LinuxNetworkWatcherWorker final : public QObject {
|
||||
|
||||
signals:
|
||||
void unsecuredNetwork(const QString& networkName, const QString& networkId);
|
||||
void sleepMode();
|
||||
void wakeup();
|
||||
|
||||
public slots:
|
||||
void initialize();
|
||||
|
||||
@@ -173,10 +173,10 @@ void PowerNotificationsListener::sleepWakeupCallBack(void *refParam, io_service_
|
||||
|
||||
case kIOMessageSystemHasPoweredOn:
|
||||
/* Announces that the system and its devices have woken up. */
|
||||
logger.debug() << "System has powered on - emitting sleepMode signal from dedicated CFRunLoop thread";
|
||||
logger.debug() << "System has powered on - emitting wakeup signal from dedicated CFRunLoop thread";
|
||||
if (listener->m_watcher) {
|
||||
// Use QMetaObject::invokeMethod for thread-safe signal emission
|
||||
QMetaObject::invokeMethod(listener->m_watcher, "sleepMode", Qt::QueuedConnection);
|
||||
QMetaObject::invokeMethod(listener->m_watcher, "wakeup", Qt::QueuedConnection);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ LRESULT WindowsNetworkWatcher::PowerWndProcCallback(HWND hwnd, UINT uMsg, WPARAM
|
||||
switch (uMsg) {
|
||||
case WM_POWERBROADCAST:
|
||||
if (wParam == PBT_APMRESUMESUSPEND) {
|
||||
emit obj->sleepMode();
|
||||
emit obj->wakeup();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -15,7 +15,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *
|
||||
m_impl.reset(new LocalSocketController());
|
||||
connect(m_impl.get(), &ControllerImpl::connected, this,
|
||||
[this](const QString &pubkey, const QDateTime &connectionTimestamp) {
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Connected);
|
||||
setConnectionState(Vpn::ConnectionState::Connected);
|
||||
});
|
||||
connect(m_impl.get(), &ControllerImpl::statusUpdated, this,
|
||||
[this](const QString& serverIpv4Gateway,
|
||||
@@ -38,7 +38,7 @@ WireguardProtocol::WireguardProtocol(const QJsonObject &configuration, QObject *
|
||||
});
|
||||
|
||||
connect(m_impl.get(), &ControllerImpl::disconnected, this,
|
||||
[this]() { emit connectionStateChanged(Vpn::ConnectionState::Disconnected); });
|
||||
[this]() { setConnectionState(Vpn::ConnectionState::Disconnected); });
|
||||
m_impl->initialize(nullptr, nullptr);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ XrayProtocol::~XrayProtocol()
|
||||
ErrorCode XrayProtocol::start()
|
||||
{
|
||||
qDebug() << "XrayProtocol::start()";
|
||||
setConnectionState(Vpn::ConnectionState::Connecting);
|
||||
|
||||
return IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto xrayStart = iface->xrayStart(QJsonDocument(m_xrayConfig).toJson());
|
||||
@@ -69,7 +68,6 @@ ErrorCode XrayProtocol::start()
|
||||
void XrayProtocol::stop()
|
||||
{
|
||||
qDebug() << "XrayProtocol::stop()";
|
||||
setConnectionState(Vpn::ConnectionState::Disconnecting);
|
||||
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto disableKillSwitch = iface->disableKillSwitch();
|
||||
|
||||
@@ -11,9 +11,14 @@
|
||||
#include <QClipboard>
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QFutureWatcher>
|
||||
#include <QSet>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "platforms/ios/ios_controller.h"
|
||||
#ifdef Q_OS_ANDROID
|
||||
#include "platforms/android/android_controller.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -370,6 +375,7 @@ bool ApiConfigsController::fillAvailableServices()
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/services"), apiPayload, responseBody);
|
||||
qDebug().noquote() << "[Billing] gateway response v1/services responseBody:" << responseBody;
|
||||
if (errorCode == ErrorCode::NoError) {
|
||||
if (!responseBody.contains("services")) {
|
||||
errorCode = ErrorCode::ApiServicesMissingError;
|
||||
@@ -425,6 +431,97 @@ bool ApiConfigsController::fillAvailableServices()
|
||||
}
|
||||
}
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
// Get price from Google Play Billing
|
||||
auto androidController = AndroidController::instance();
|
||||
QJsonObject plansResult = androidController->getSubscriptionPlans();
|
||||
int responseCode = plansResult.value("responseCode").toInt(-1);
|
||||
qDebug().noquote() << "[Billing] getSubscriptionPlans plansResult:" << QJsonDocument(plansResult).toJson(QJsonDocument::Compact);
|
||||
qDebug() << "[Billing] getSubscriptionPlans responseCode:" << responseCode;
|
||||
|
||||
if (responseCode == 0) {
|
||||
QJsonArray products = plansResult.value("products").toArray();
|
||||
QString formattedPrice;
|
||||
int billingPeriodDays = 180;
|
||||
for (const QJsonValue &productValue : products) {
|
||||
QJsonObject product = productValue.toObject();
|
||||
const QString productId = product.value("productId").toString();
|
||||
const bool isPremium = (productId == "premium") || productId.contains("premium");
|
||||
if (isPremium) {
|
||||
QJsonArray offers = product.value("offers").toArray();
|
||||
if (!offers.isEmpty()) {
|
||||
QJsonObject firstOffer = offers.at(0).toObject();
|
||||
QJsonArray pricingPhases = firstOffer.value("pricingPhases").toArray();
|
||||
if (!pricingPhases.isEmpty()) {
|
||||
QJsonObject pricingPhase = pricingPhases.at(0).toObject();
|
||||
formattedPrice = pricingPhase.value("formatedPrice").toString();
|
||||
if (formattedPrice.isEmpty()) {
|
||||
formattedPrice = pricingPhase.value("formattedPrice").toString();
|
||||
}
|
||||
QString billingPeriod = pricingPhase.value("billingPeriod").toString();
|
||||
if (billingPeriod.contains("Y")) {
|
||||
int idx = billingPeriod.indexOf("Y");
|
||||
int years = billingPeriod.mid(1, idx - 1).toInt();
|
||||
if (years > 0) billingPeriodDays = years * 365;
|
||||
} else if (billingPeriod.contains("M")) {
|
||||
int idx = billingPeriod.indexOf("M");
|
||||
int months = billingPeriod.mid(1, idx - 1).toInt();
|
||||
if (months > 0) billingPeriodDays = months * 30;
|
||||
} else if (billingPeriod.contains("D")) {
|
||||
int idx = billingPeriod.indexOf("D");
|
||||
billingPeriodDays = billingPeriod.mid(1, idx - 1).toInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!formattedPrice.isEmpty()) {
|
||||
QJsonArray services = data.value("services").toArray();
|
||||
bool premiumFound = false;
|
||||
for (int i = 0; i < services.size(); ++i) {
|
||||
QJsonObject service = services[i].toObject();
|
||||
if (service.value(configKey::serviceType).toString() == serviceType::amneziaPremium) {
|
||||
QJsonObject serviceInfo = service.value(configKey::serviceInfo).toObject();
|
||||
serviceInfo["price"] = formattedPrice;
|
||||
service[configKey::serviceInfo] = serviceInfo;
|
||||
services[i] = service;
|
||||
data["services"] = services;
|
||||
premiumFound = true;
|
||||
qInfo() << "[Billing] Updated premium service price in data:" << formattedPrice;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* if (!premiumFound) {
|
||||
// Gateway did not return premium; add it from billing data
|
||||
QString region = data.value(configKey::userCountryCode).toString();
|
||||
QJsonObject serviceInfo;
|
||||
serviceInfo["name"] = tr("Amnezia Premium");
|
||||
serviceInfo["price"] = formattedPrice;
|
||||
serviceInfo["region"] = region;
|
||||
serviceInfo["speed"] = "200";
|
||||
serviceInfo["timelimit"] = QString::number(billingPeriodDays);
|
||||
QJsonObject serviceDescription;
|
||||
serviceDescription["card_description"] = tr("Amnezia Premium is classic VPN for seamless work, downloading large files, and watching videos.");
|
||||
serviceDescription["description"] = serviceDescription["card_description"];
|
||||
serviceDescription["features"] = "";
|
||||
QJsonObject premiumService;
|
||||
premiumService[configKey::serviceType] = serviceType::amneziaPremium;
|
||||
premiumService[configKey::serviceProtocol] = "amnezia-premium";
|
||||
premiumService[configKey::serviceInfo] = serviceInfo;
|
||||
premiumService["service_description"] = serviceDescription;
|
||||
premiumService["available_countries"] = QJsonArray();
|
||||
premiumService["is_available"] = true;
|
||||
premiumService["store_endpoint"] = "";
|
||||
premiumService["subscription"] = QJsonObject();
|
||||
services.prepend(premiumService);
|
||||
data["services"] = services;
|
||||
qInfo() << "[Billing] Added premium service from billing (gateway did not return it)";
|
||||
}*/
|
||||
}
|
||||
} else {
|
||||
qWarning() << "[Billing] Failed to fetch product price, responseCode:" << responseCode;
|
||||
}
|
||||
#endif
|
||||
|
||||
m_apiServicesModel->updateModel(data);
|
||||
@@ -436,25 +533,19 @@ bool ApiConfigsController::fillAvailableServices()
|
||||
|
||||
bool ApiConfigsController::importService()
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
bool isIosOrMacOsNe = true;
|
||||
#else
|
||||
bool isIosOrMacOsNe = false;
|
||||
#endif
|
||||
|
||||
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
|
||||
if (isIosOrMacOsNe) {
|
||||
importSerivceFromAppStore();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
importServiceFromGateway();
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE) || defined(Q_OS_ANDROID)
|
||||
importServiceFromPaymentMarket();
|
||||
return true;
|
||||
#else
|
||||
return false; // premium only via App Store / Play
|
||||
#endif
|
||||
}
|
||||
return false;
|
||||
importServiceFromGateway();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApiConfigsController::importSerivceFromAppStore()
|
||||
bool ApiConfigsController::importServiceFromPaymentMarket()
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
bool purchaseOk = false;
|
||||
@@ -511,12 +602,116 @@ bool ApiConfigsController::importSerivceFromAppStore()
|
||||
return false;
|
||||
}
|
||||
|
||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
auto androidController = AndroidController::instance();
|
||||
QString purchaseToken;
|
||||
bool purchaseOk = false;
|
||||
|
||||
QFutureWatcher<QPair<bool, QString>> watcher;
|
||||
QEventLoop waitLoop;
|
||||
connect(&watcher, &QFutureWatcher<QPair<bool, QString>>::finished, &waitLoop, &QEventLoop::quit);
|
||||
|
||||
QFuture<QPair<bool, QString>> future = QtConcurrent::run([androidController]() {
|
||||
QJsonObject plansResult = androidController->getSubscriptionPlans();
|
||||
int responseCode = plansResult.value("responseCode").toInt(-1);
|
||||
qDebug().noquote() << "[Billing] importService getSubscriptionPlans plansResult:" << QJsonDocument(plansResult).toJson(QJsonDocument::Compact);
|
||||
qDebug() << "[Billing] importService getSubscriptionPlans responseCode:" << responseCode;
|
||||
if (responseCode != 0) {
|
||||
qWarning() << "[Billing] Failed to get subscription plans, responseCode:" << responseCode;
|
||||
return qMakePair(false, QString());
|
||||
}
|
||||
QJsonArray products = plansResult.value("products").toArray();
|
||||
QString offerToken;
|
||||
for (const QJsonValue &productValue : products) {
|
||||
QJsonObject product = productValue.toObject();
|
||||
const QString productId = product.value("productId").toString();
|
||||
const bool isPremium = (productId == "premium") || productId.contains("premium");
|
||||
if (isPremium) {
|
||||
QJsonArray offers = product.value("offers").toArray();
|
||||
if (!offers.isEmpty()) {
|
||||
QJsonObject firstOffer = offers.at(0).toObject();
|
||||
offerToken = firstOffer.value("offerToken").toString();
|
||||
qInfo() << "[Billing] Found offer token:" << offerToken;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (offerToken.isEmpty()) {
|
||||
qWarning() << "[Billing] No offer token found for premium subscription";
|
||||
return qMakePair(false, QString());
|
||||
}
|
||||
QJsonObject purchaseResult = androidController->purchaseSubscription(offerToken);
|
||||
responseCode = purchaseResult.value("responseCode").toInt(-1);
|
||||
if (responseCode != 0) {
|
||||
qWarning() << "[Billing] Purchase failed, responseCode:" << responseCode;
|
||||
return qMakePair(false, QString());
|
||||
}
|
||||
QJsonArray purchases = purchaseResult.value("purchases").toArray();
|
||||
if (purchases.isEmpty()) {
|
||||
qWarning() << "[Billing] Purchase succeeded but no purchases returned";
|
||||
return qMakePair(false, QString());
|
||||
}
|
||||
QJsonObject purchase = purchases.at(0).toObject();
|
||||
QString token = purchase.value("purchaseToken").toString();
|
||||
bool isAcknowledged = purchase.value("isAcknowledged").toBool();
|
||||
qInfo() << "[Billing] Purchase success. purchaseToken:" << token << "isAcknowledged:" << isAcknowledged;
|
||||
if (!isAcknowledged) {
|
||||
QJsonObject ackResult = androidController->acknowledgePurchase(token);
|
||||
if (ackResult.value("responseCode").toInt(-1) != 0) {
|
||||
qWarning() << "[Billing] Acknowledge failed";
|
||||
} else {
|
||||
qInfo() << "[Billing] Purchase acknowledged successfully";
|
||||
}
|
||||
}
|
||||
return qMakePair(true, token);
|
||||
});
|
||||
|
||||
watcher.setFuture(future);
|
||||
waitLoop.exec();
|
||||
|
||||
purchaseOk = watcher.result().first;
|
||||
purchaseToken = watcher.result().second;
|
||||
|
||||
if (!purchaseOk || purchaseToken.isEmpty()) {
|
||||
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
||||
return false;
|
||||
}
|
||||
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_settings->getAppLanguage().name().split("_").first(),
|
||||
m_settings->getInstallationUuid(true),
|
||||
m_apiServicesModel->getCountryCode(),
|
||||
"",
|
||||
m_apiServicesModel->getSelectedServiceType(),
|
||||
m_apiServicesModel->getSelectedServiceProtocol(),
|
||||
QJsonObject() };
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
apiPayload[apiDefs::key::transactionId] = purchaseToken;
|
||||
bool isTestPurchase = m_settings->isDevGatewayEnv(false) || androidController->isTestPurchaseEnvironment();
|
||||
|
||||
ErrorCode errorCode;
|
||||
QByteArray responseBody;
|
||||
errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit errorOccurred(errorCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
errorCode = importServiceFromBilling(responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
emit errorOccurred(errorCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ApiConfigsController::restoreSerivceFromAppStore()
|
||||
bool ApiConfigsController::restoreServiceFromPaymentMarket()
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
const QString premiumServiceType = QStringLiteral("amnezia-premium");
|
||||
@@ -639,6 +834,131 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
|
||||
qInfo().noquote() << "[IAP] Skipped" << duplicateCount
|
||||
<< "duplicate restored transactions for original transaction IDs already processed";
|
||||
}
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
// Android Google Play Billing restore implementation
|
||||
const QString premiumServiceType = QStringLiteral("amnezia-premium");
|
||||
|
||||
if (!fillAvailableServices()) {
|
||||
qWarning() << "[Billing] Unable to fetch services list before restore";
|
||||
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_apiServicesModel->rowCount() <= 0) {
|
||||
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure we have a valid premium selection for gateway requests
|
||||
bool premiumSelected = false;
|
||||
for (int i = 0; i < m_apiServicesModel->rowCount(); ++i) {
|
||||
m_apiServicesModel->setServiceIndex(i);
|
||||
if (m_apiServicesModel->getSelectedServiceType() == premiumServiceType) {
|
||||
premiumSelected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!premiumSelected) {
|
||||
emit errorOccurred(ErrorCode::ApiServicesMissingError);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto androidController = AndroidController::instance();
|
||||
|
||||
// Query existing purchases
|
||||
QJsonObject purchasesResult = androidController->queryPurchases();
|
||||
int responseCode = purchasesResult.value("responseCode").toInt(-1);
|
||||
|
||||
if (responseCode != 0) {
|
||||
qWarning() << "[Billing] Failed to query purchases, responseCode:" << responseCode;
|
||||
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonArray purchases = purchasesResult.value("purchases").toArray();
|
||||
|
||||
if (purchases.isEmpty()) {
|
||||
qInfo() << "[Billing] No purchases found to restore";
|
||||
emit errorOccurred(ErrorCode::ApiNoPurchasesToRestore);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasInstalledConfig = false;
|
||||
bool duplicateConfigAlreadyPresent = false;
|
||||
QSet<QString> processedTokens;
|
||||
|
||||
for (const QJsonValue &purchaseValue : purchases) {
|
||||
QJsonObject purchase = purchaseValue.toObject();
|
||||
QString purchaseToken = purchase.value("purchaseToken").toString();
|
||||
bool isAcknowledged = purchase.value("isAcknowledged").toBool();
|
||||
|
||||
if (purchaseToken.isEmpty()) {
|
||||
qWarning() << "[Billing] Skipping purchase without token";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processedTokens.contains(purchaseToken)) {
|
||||
continue;
|
||||
}
|
||||
processedTokens.insert(purchaseToken);
|
||||
|
||||
qInfo() << "[Billing] Restoring purchase. purchaseToken:" << purchaseToken
|
||||
<< "isAcknowledged:" << isAcknowledged;
|
||||
|
||||
// Acknowledge purchase if needed
|
||||
if (!isAcknowledged) {
|
||||
QJsonObject ackResult = androidController->acknowledgePurchase(purchaseToken);
|
||||
int ackResponseCode = ackResult.value("responseCode").toInt(-1);
|
||||
if (ackResponseCode != 0) {
|
||||
qWarning() << "[Billing] Acknowledge failed, responseCode:" << ackResponseCode;
|
||||
} else {
|
||||
qInfo() << "[Billing] Purchase acknowledged successfully";
|
||||
}
|
||||
}
|
||||
|
||||
// Send purchase token to gateway
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_settings->getAppLanguage().name().split("_").first(),
|
||||
m_settings->getInstallationUuid(true),
|
||||
m_apiServicesModel->getCountryCode(),
|
||||
"",
|
||||
m_apiServicesModel->getSelectedServiceType(),
|
||||
m_apiServicesModel->getSelectedServiceProtocol(),
|
||||
QJsonObject() };
|
||||
|
||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||
apiPayload[apiDefs::key::transactionId] = purchaseToken;
|
||||
bool isTestPurchase = m_settings->isDevGatewayEnv(false) || androidController->isTestPurchaseEnvironment();
|
||||
|
||||
QByteArray responseBody;
|
||||
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
||||
if (errorCode != ErrorCode::NoError) {
|
||||
qWarning() << "[Billing] Failed to restore purchase" << purchaseToken
|
||||
<< "errorCode =" << static_cast<int>(errorCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
ErrorCode installError = importServiceFromBilling(responseBody, isTestPurchase);
|
||||
if (installError == ErrorCode::ApiConfigAlreadyAdded) {
|
||||
duplicateConfigAlreadyPresent = true;
|
||||
qInfo() << "[Billing] Skipping restored purchase" << purchaseToken
|
||||
<< "because subscription config with the same vpn_key already exists";
|
||||
} else if (installError != ErrorCode::NoError) {
|
||||
qWarning() << "[Billing] Failed to process restored subscription response for purchase" << purchaseToken;
|
||||
} else {
|
||||
hasInstalledConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasInstalledConfig) {
|
||||
const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
|
||||
emit errorOccurred(restoreError);
|
||||
return false;
|
||||
}
|
||||
|
||||
emit installServerFromApiFinished(tr("Subscription restored successfully."));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
@@ -944,16 +1264,16 @@ QString ApiConfigsController::getVpnKey()
|
||||
|
||||
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase)
|
||||
{
|
||||
#ifdef Q_OS_IOS
|
||||
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
|
||||
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
||||
QString key = responseObject.value(QStringLiteral("key")).toString();
|
||||
if (key.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Subscription response does not contain a key field";
|
||||
qWarning().noquote() << "[IAP/Billing] Subscription response does not contain a key field";
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
if (m_serversModel->hasServerWithVpnKey(key)) {
|
||||
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
||||
qInfo().noquote() << "[IAP/Billing] Subscription config with the same vpn_key already exists";
|
||||
return ErrorCode::ApiConfigAlreadyAdded;
|
||||
}
|
||||
|
||||
@@ -967,7 +1287,7 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
|
||||
}
|
||||
|
||||
if (configString.isEmpty()) {
|
||||
qWarning().noquote() << "[IAP] Subscription response config payload is empty";
|
||||
qWarning().noquote() << "[IAP/Billing] Subscription response config payload is empty";
|
||||
return ErrorCode::ApiPurchaseError;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ public slots:
|
||||
|
||||
bool fillAvailableServices();
|
||||
bool importService();
|
||||
bool importSerivceFromAppStore();
|
||||
bool restoreSerivceFromAppStore();
|
||||
bool importServiceFromPaymentMarket();
|
||||
bool restoreServiceFromPaymentMarket();
|
||||
bool importServiceFromGateway();
|
||||
bool updateServiceFromGateway(const int serverIndex, const QString &newCountryCode, const QString &newCountryName,
|
||||
bool reloadServiceConfig = false);
|
||||
|
||||
@@ -114,6 +114,8 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
return tr("%1 $").arg(price);
|
||||
#elif defined(Q_OS_ANDROID)
|
||||
return price;
|
||||
#else
|
||||
return tr("%1 $/month").arg(price);
|
||||
#endif
|
||||
|
||||
@@ -396,9 +396,7 @@ PageType {
|
||||
PageController.showNotificationMessage(qsTr("Cannot remove server during active connection"))
|
||||
} else {
|
||||
PageController.showBusyIndicator(true)
|
||||
if (ApiConfigsController.deactivateDevice(true)) {
|
||||
InstallController.removeProcessedServer()
|
||||
}
|
||||
InstallController.removeProcessedServer()
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,226 +1,237 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (this.activeFocus) {
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
headerText: ApiServicesModel.getSelectedServiceData("name")
|
||||
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
|
||||
}
|
||||
}
|
||||
|
||||
model: inputFields
|
||||
spacing: 0
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
LabelWithImageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
imageSource: imagePath
|
||||
leftText: lText
|
||||
rightText: rText
|
||||
|
||||
visible: isVisible
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
spacing: 0
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
textFormat: Text.RichText
|
||||
text: {
|
||||
var text = ApiServicesModel.getSelectedServiceData("features")
|
||||
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.PlainText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.bottomMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
|
||||
|
||||
clickedFunc: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
var result = ApiConfigsController.importService()
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
if (!result) {
|
||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
||||
Qt.openUrlExternally(endpoint)
|
||||
PageController.closePage()
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
visible: (Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium"
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: {
|
||||
var termsUrl = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/"
|
||||
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
|
||||
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
|
||||
}
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property list<QtObject> inputFields: [
|
||||
region,
|
||||
price,
|
||||
timeLimit,
|
||||
speed,
|
||||
features
|
||||
]
|
||||
|
||||
QtObject {
|
||||
id: region
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
|
||||
readonly property string lText: qsTr("For the region")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: price
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/tag.svg"
|
||||
readonly property string lText: qsTr("Price")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: timeLimit
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/history.svg"
|
||||
readonly property string lText: qsTr("Work period")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
|
||||
property bool isVisible: rText !== ""
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: speed
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
|
||||
readonly property string lText: qsTr("Speed")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: features
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/info.svg"
|
||||
readonly property string lText: qsTr("Features")
|
||||
readonly property string rText: ""
|
||||
property bool isVisible: true
|
||||
}
|
||||
}
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
|
||||
import PageEnum 1.0
|
||||
import Style 1.0
|
||||
|
||||
import "./"
|
||||
import "../Controls2"
|
||||
import "../Controls2/TextTypes"
|
||||
import "../Config"
|
||||
import "../Components"
|
||||
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
BackButtonType {
|
||||
id: backButton
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 20 + SettingsController.safeAreaTopMargin
|
||||
|
||||
onFocusChanged: {
|
||||
if (this.activeFocus) {
|
||||
listView.positionViewAtBeginning()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: listView
|
||||
|
||||
anchors.top: backButton.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
|
||||
header: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
BaseHeaderType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
headerText: ApiServicesModel.getSelectedServiceData("name")
|
||||
descriptionText: ApiServicesModel.getSelectedServiceData("serviceDescription")
|
||||
}
|
||||
}
|
||||
|
||||
model: inputFields
|
||||
spacing: 0
|
||||
|
||||
delegate: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
LabelWithImageType {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 16
|
||||
|
||||
imageSource: imagePath
|
||||
leftText: lText
|
||||
rightText: rText
|
||||
|
||||
visible: isVisible
|
||||
}
|
||||
}
|
||||
|
||||
footer: ColumnLayout {
|
||||
width: listView.width
|
||||
|
||||
spacing: 0
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
textFormat: Text.RichText
|
||||
text: {
|
||||
var text = ApiServicesModel.getSelectedServiceData("features")
|
||||
return text.replace("%1", LanguageModel.getCurrentSiteUrl("free")).replace("/free", "") // todo link should come from gateway
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
visible: ((Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium") ||
|
||||
(Qt.platform.os === "android" && ApiServicesModel.getSelectedServiceType() === "amnezia-premium")
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.PlainText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: {
|
||||
if (Qt.platform.os === "ios" || IsMacOsNeBuild) {
|
||||
return qsTr("Charged to your Apple ID at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Apple ID settings.")
|
||||
} else if (Qt.platform.os === "android") {
|
||||
return qsTr("Charged to your Google Play account at confirmation. Renews automatically unless auto-renew is turned off at least 24 hours before period end. Manage in Google Play settings.")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
BasicButtonType {
|
||||
id: continueButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 32
|
||||
Layout.bottomMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
text: ApiServicesModel.getSelectedServiceType() === "amnezia-premium" ? qsTr("Subscribe Now") : qsTr("Connect")
|
||||
|
||||
clickedFunc: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
var result = ApiConfigsController.importService()
|
||||
PageController.showBusyIndicator(false)
|
||||
|
||||
if (!result) {
|
||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
||||
Qt.openUrlExternally(endpoint)
|
||||
PageController.closePage()
|
||||
PageController.closePage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParagraphTextType {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
Layout.bottomMargin: 32
|
||||
|
||||
visible: ((Qt.platform.os === "ios" || IsMacOsNeBuild) && ApiServicesModel.getSelectedServiceType() === "amnezia-premium") ||
|
||||
(Qt.platform.os === "android" && ApiServicesModel.getSelectedServiceType() === "amnezia-premium")
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
textFormat: Text.RichText
|
||||
color: AmneziaStyle.color.mutedGray
|
||||
font.pixelSize: 12
|
||||
|
||||
text: {
|
||||
var termsUrl = Qt.platform.os === "ios" || IsMacOsNeBuild ?
|
||||
"https://www.apple.com/legal/internet-services/itunes/dev/stdeula/" :
|
||||
"https://play.google.com/intl/en_us/about/play-terms/"
|
||||
var privacyUrl = LanguageModel.getCurrentSiteUrl("policy")
|
||||
return qsTr("By continuing, you agree to the <a href=\"%1\" style=\"color: #FBB26A;\">Terms of Use</a> and <a href=\"%2\" style=\"color: #FBB26A;\">Privacy Policy</a>").arg(termsUrl).arg(privacyUrl)
|
||||
}
|
||||
|
||||
onLinkActivated: function(link) {
|
||||
Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property list<QtObject> inputFields: [
|
||||
region,
|
||||
price,
|
||||
timeLimit,
|
||||
speed,
|
||||
features
|
||||
]
|
||||
|
||||
QtObject {
|
||||
id: region
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/map-pin.svg"
|
||||
readonly property string lText: qsTr("For the region")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("region")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: price
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/tag.svg"
|
||||
readonly property string lText: qsTr("Price")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("price")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: timeLimit
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/history.svg"
|
||||
readonly property string lText: qsTr("Work period")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("timeLimit")
|
||||
property bool isVisible: rText !== ""
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: speed
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/gauge.svg"
|
||||
readonly property string lText: qsTr("Speed")
|
||||
readonly property string rText: ApiServicesModel.getSelectedServiceData("speed")
|
||||
property bool isVisible: true
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: features
|
||||
|
||||
readonly property string imagePath: "qrc:/images/controls/info.svg"
|
||||
readonly property string lText: qsTr("Features")
|
||||
readonly property string rText: ""
|
||||
property bool isVisible: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,10 +358,10 @@ PageType {
|
||||
property string title: qsTr("Restore purchases")
|
||||
property string description: qsTr("")
|
||||
property string imageSource: "qrc:/images/controls/refresh-cw.svg"
|
||||
property bool isVisible: Qt.platform.os === "ios" || IsMacOsNeBuild
|
||||
property bool isVisible: Qt.platform.os === "ios" || IsMacOsNeBuild || Qt.platform.os === "android"
|
||||
property var handler: function() {
|
||||
PageController.showBusyIndicator(true)
|
||||
ApiConfigsController.restoreSerivceFromAppStore()
|
||||
ApiConfigsController.restoreServiceFromPaymentMarket()
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ VpnConnection::VpnConnection(std::shared_ptr<Settings> settings, QObject *parent
|
||||
m_checkTimer.setInterval(1000);
|
||||
connect(IosController::Instance(), &IosController::connectionStateChanged, this, &VpnConnection::onConnectionStateChanged);
|
||||
connect(IosController::Instance(), &IosController::bytesChanged, this, &VpnConnection::onBytesChanged);
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -59,7 +58,7 @@ void VpnConnection::onKillSwitchModeChanged(bool enabled)
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
IpcClient::withInterface([enabled](QSharedPointer<IpcInterfaceReplica> iface){
|
||||
QRemoteObjectPendingReply<bool> reply = iface->refreshKillSwitch(enabled);
|
||||
if (reply.waitForFinished(1000) && reply.returnValue())
|
||||
if (reply.waitForFinished() && reply.returnValue())
|
||||
qDebug() << "VpnConnection::onKillSwitchModeChanged: Killswitch refreshed";
|
||||
else
|
||||
qWarning() << "VpnConnection::onKillSwitchModeChanged: Failed to execute remote refreshKillSwitch call";
|
||||
@@ -73,40 +72,57 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
|
||||
auto container = m_settings->defaultContainer(m_settings->defaultServerIndex());
|
||||
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
if (state == Vpn::ConnectionState::Connected) {
|
||||
iface->resetIpStack();
|
||||
iface->flushDns();
|
||||
switch (state) {
|
||||
case Vpn::ConnectionState::Connected: {
|
||||
iface->resetIpStack();
|
||||
|
||||
if (!ContainerProps::isAwgContainer(container) &&
|
||||
container != DockerContainer::WireGuard) {
|
||||
QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString();
|
||||
QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString();
|
||||
auto flushDns = iface->flushDns();
|
||||
if (flushDns.waitForFinished() && flushDns.returnValue())
|
||||
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
|
||||
else
|
||||
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
|
||||
|
||||
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
|
||||
|
||||
if (m_settings->isSitesSplitTunnelingEnabled()) {
|
||||
iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0");
|
||||
// qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size();
|
||||
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
|
||||
QTimer::singleShot(1000, m_vpnProtocol.data(),
|
||||
[this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); });
|
||||
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
|
||||
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1");
|
||||
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1");
|
||||
if (!ContainerProps::isAwgContainer(container) &&
|
||||
container != DockerContainer::WireGuard) {
|
||||
QString dns1 = m_vpnConfiguration.value(config_key::dns1).toString();
|
||||
QString dns2 = m_vpnConfiguration.value(config_key::dns2).toString();
|
||||
|
||||
iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress());
|
||||
addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode());
|
||||
// TODO: add error code handling for all routeAddList (or rework the code below)
|
||||
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << dns1 << dns2);
|
||||
|
||||
if (m_settings->isSitesSplitTunnelingEnabled()) {
|
||||
iface->routeDeleteList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0");
|
||||
// qDebug() << "VpnConnection::onConnectionStateChanged :: adding custom routes, count:" << forwardIps.size();
|
||||
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
|
||||
QTimer::singleShot(1000, m_vpnProtocol.data(),
|
||||
[this]() { addSitesRoutes(m_vpnProtocol->vpnGateway(), m_settings->routeMode()); });
|
||||
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
|
||||
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "0.0.0.0/1");
|
||||
iface->routeAddList(m_vpnProtocol->vpnGateway(), QStringList() << "128.0.0.0/1");
|
||||
|
||||
iface->routeAddList(m_vpnProtocol->routeGateway(), QStringList() << remoteAddress());
|
||||
addSitesRoutes(m_vpnProtocol->routeGateway(), m_settings->routeMode());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (state == Vpn::ConnectionState::Error) {
|
||||
iface->flushDns();
|
||||
} break;
|
||||
case Vpn::ConnectionState::Disconnected:
|
||||
case Vpn::ConnectionState::Error: {
|
||||
auto flushDns = iface->flushDns();
|
||||
if (flushDns.waitForFinished() && flushDns.returnValue())
|
||||
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully flushed DNS";
|
||||
else
|
||||
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to flush DNS";
|
||||
|
||||
if (m_settings->isSitesSplitTunnelingEnabled()) {
|
||||
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
|
||||
iface->clearSavedRoutes();
|
||||
}
|
||||
}
|
||||
auto clearSavedRoutes = iface->clearSavedRoutes();
|
||||
if (clearSavedRoutes.waitForFinished() && clearSavedRoutes.returnValue())
|
||||
qDebug() << "VpnConnection::onConnectionStateChanged: Successfully cleared saved routes";
|
||||
else
|
||||
qWarning() << "VpnConnection::onConnectionStateChanged: Failed to clear saved routes";
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
#endif
|
||||
@@ -120,7 +136,6 @@ void VpnConnection::onConnectionStateChanged(Vpn::ConnectionState state)
|
||||
m_checkTimer.stop();
|
||||
}
|
||||
#endif
|
||||
emit connectionStateChanged(state);
|
||||
}
|
||||
|
||||
const QString &VpnConnection::remoteAddress() const
|
||||
@@ -165,7 +180,11 @@ void VpnConnection::addSitesRoutes(const QString &gw, Settings::RouteMode mode)
|
||||
});
|
||||
m_settings->addVpnSite(mode, site, ip);
|
||||
}
|
||||
flushDns();
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto reply = iface->flushDns();
|
||||
if (reply.waitForFinished() || !reply.returnValue())
|
||||
qWarning() << "VpnConnection::addSitesRoutes: Failed to flush DNS";
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -180,48 +199,6 @@ QSharedPointer<VpnProtocol> VpnConnection::vpnProtocol() const
|
||||
return m_vpnProtocol;
|
||||
}
|
||||
|
||||
void VpnConnection::addRoutes(const QStringList &ips)
|
||||
{
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
if (connectionState() == Vpn::ConnectionState::Connected) {
|
||||
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
|
||||
iface->routeAddList(m_vpnProtocol->vpnGateway(), ips);
|
||||
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
|
||||
iface->routeAddList(m_vpnProtocol->routeGateway(), ips);
|
||||
}
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void VpnConnection::deleteRoutes(const QStringList &ips)
|
||||
{
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
IpcClient::withInterface([&](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
if (connectionState() == Vpn::ConnectionState::Connected) {
|
||||
if (m_settings->routeMode() == Settings::VpnOnlyForwardSites) {
|
||||
iface->routeDeleteList(vpnProtocol()->vpnGateway(), ips);
|
||||
} else if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
|
||||
iface->routeDeleteList(m_vpnProtocol->routeGateway(), ips);
|
||||
}
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void VpnConnection::flushDns()
|
||||
{
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
auto reply = iface->flushDns();
|
||||
if (reply.waitForFinished(1000) || !reply.returnValue()) {
|
||||
qWarning() << "VpnConnection::flushDns(): Failed to flush DNS";
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void VpnConnection::disconnectSlots()
|
||||
{
|
||||
if (m_vpnProtocol) {
|
||||
@@ -251,7 +228,7 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
|
||||
<< m_settings->routeMode();
|
||||
|
||||
m_remoteAddress = NetworkUtilities::getIPAddress(credentials.hostName);
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Connecting);
|
||||
setConnectionState(Vpn::ConnectionState::Connecting);
|
||||
|
||||
m_vpnConfiguration = vpnConfiguration;
|
||||
|
||||
@@ -269,7 +246,7 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
|
||||
#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS) && !defined(MACOS_NE)
|
||||
m_vpnProtocol.reset(VpnProtocol::factory(container, m_vpnConfiguration));
|
||||
if (!m_vpnProtocol) {
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
setConnectionState(Vpn::ConnectionState::Error);
|
||||
return;
|
||||
}
|
||||
m_vpnProtocol->prepare();
|
||||
@@ -287,17 +264,24 @@ void VpnConnection::connectToVpn(int serverIndex, const ServerCredentials &crede
|
||||
|
||||
createProtocolConnections();
|
||||
|
||||
ErrorCode errorCode = m_vpnProtocol->start();
|
||||
if (errorCode != ErrorCode::NoError)
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||
if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) {
|
||||
setConnectionState(Vpn::ConnectionState::Error);
|
||||
emit vpnProtocolError(err);
|
||||
}
|
||||
}
|
||||
|
||||
void VpnConnection::createProtocolConnections()
|
||||
{
|
||||
connect(m_vpnProtocol.data(), &VpnProtocol::protocolError, this, &VpnConnection::vpnProtocolError);
|
||||
connect(m_vpnProtocol.data(), SIGNAL(connectionStateChanged(Vpn::ConnectionState)), this,
|
||||
SLOT(onConnectionStateChanged(Vpn::ConnectionState)));
|
||||
connect(m_vpnProtocol.data(), &VpnProtocol::connectionStateChanged, this, &VpnConnection::setConnectionState);
|
||||
connect(m_vpnProtocol.data(), SIGNAL(bytesChanged(quint64, quint64)), this, SLOT(onBytesChanged(quint64, quint64)));
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
IpcClient::withInterface([this](QSharedPointer<IpcInterfaceReplica> rep) {
|
||||
connect(rep.data(), &IpcInterfaceReplica::networkChanged, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection);
|
||||
connect(rep.data(), &IpcInterfaceReplica::wakeup, this, &VpnConnection::reconnectToVpn, Qt::QueuedConnection);
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
void VpnConnection::appendKillSwitchConfig()
|
||||
@@ -439,6 +423,27 @@ QString VpnConnection::bytesPerSecToText(quint64 bytes)
|
||||
return QString("%1 %2").arg(QString::number(mbps, 'f', 2)).arg(tr("Mbps")); // Mbit/s
|
||||
}
|
||||
|
||||
void VpnConnection::reconnectToVpn() {
|
||||
if (m_vpnProtocol.isNull())
|
||||
return;
|
||||
|
||||
if (m_connectionState != Vpn::ConnectionState::Connected) {
|
||||
qWarning() << QString("Reconnect triggered on %1 during inappropriate state: %2; ignoring slot")
|
||||
.arg(QMetaEnum::fromType<Vpn::ConnectionState>().valueToKey(m_connectionState));
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Reconnect triggered. Reconnecting to the server";
|
||||
|
||||
setConnectionState(Vpn::ConnectionState::Reconnecting);
|
||||
|
||||
m_vpnProtocol->stop();
|
||||
if (ErrorCode err = m_vpnProtocol->start(); err != ErrorCode::NoError) {
|
||||
setConnectionState(Vpn::ConnectionState::Error);
|
||||
emit vpnProtocolError(err);
|
||||
}
|
||||
}
|
||||
|
||||
void VpnConnection::disconnectFromVpn()
|
||||
{
|
||||
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||
@@ -448,27 +453,11 @@ void VpnConnection::disconnectFromVpn()
|
||||
#endif
|
||||
|
||||
if (m_vpnProtocol.isNull()) {
|
||||
emit connectionStateChanged(Vpn::ConnectionState::Disconnected);
|
||||
setConnectionState(Vpn::ConnectionState::Disconnected);
|
||||
return;
|
||||
}
|
||||
|
||||
m_vpnProtocol->stop();
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
IpcClient::withInterface([](QSharedPointer<IpcInterfaceReplica> iface) {
|
||||
QRemoteObjectPendingReply<bool> flushReply = iface->flushDns();
|
||||
if (flushReply.waitForFinished(5000) && flushReply.returnValue())
|
||||
qDebug() << "VpnConnection::disconnectFromVpn(): Successfully flushed DNS";
|
||||
else
|
||||
qWarning() << "VpnConnection::disconnectFromVpn(): Failed to flush DNS";
|
||||
|
||||
QRemoteObjectPendingReply<bool> clearSavedRoutesReply = iface->clearSavedRoutes();
|
||||
if (clearSavedRoutesReply.waitForFinished(5000) && clearSavedRoutesReply.returnValue())
|
||||
qDebug() << "VpnConnection::disconnectFromVpn(): Successfully cleared saved routes";
|
||||
else
|
||||
qWarning() << "VpnConnection::disconnectFromVpn(): Failed to clear saved routes";
|
||||
});
|
||||
#endif
|
||||
setConnectionState(Vpn::ConnectionState::Disconnecting);
|
||||
|
||||
#ifdef Q_OS_ANDROID
|
||||
auto *const connection = new QMetaObject::Connection;
|
||||
@@ -480,9 +469,10 @@ void VpnConnection::disconnectFromVpn()
|
||||
delete connection;
|
||||
}
|
||||
});
|
||||
m_vpnProtocol->stop();
|
||||
#endif
|
||||
|
||||
m_vpnProtocol->stop();
|
||||
|
||||
#if !defined(Q_OS_ANDROID) && !defined(AMNEZIA_DESKTOP)
|
||||
m_vpnProtocol->deleteLater();
|
||||
#endif
|
||||
@@ -490,27 +480,12 @@ void VpnConnection::disconnectFromVpn()
|
||||
m_vpnProtocol = nullptr;
|
||||
}
|
||||
|
||||
Vpn::ConnectionState VpnConnection::connectionState()
|
||||
{
|
||||
if (!m_vpnProtocol)
|
||||
return Vpn::ConnectionState::Disconnected;
|
||||
return m_vpnProtocol->connectionState();
|
||||
}
|
||||
|
||||
bool VpnConnection::isConnected() const
|
||||
{
|
||||
if (m_vpnProtocol.isNull()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_vpnProtocol->isConnected();
|
||||
}
|
||||
|
||||
bool VpnConnection::isDisconnected() const
|
||||
{
|
||||
if (m_vpnProtocol.isNull()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return m_vpnProtocol->isDisconnected();
|
||||
void VpnConnection::setConnectionState(Vpn::ConnectionState state) {
|
||||
onConnectionStateChanged(state);
|
||||
|
||||
if (state == Vpn::Disconnected && m_connectionState == Vpn::Reconnecting)
|
||||
return;
|
||||
|
||||
m_connectionState = state;
|
||||
emit connectionStateChanged(state);
|
||||
}
|
||||
|
||||
@@ -34,10 +34,6 @@ public:
|
||||
|
||||
ErrorCode lastError() const;
|
||||
|
||||
bool isConnected() const;
|
||||
bool isDisconnected() const;
|
||||
|
||||
Vpn::ConnectionState connectionState();
|
||||
QSharedPointer<VpnProtocol> vpnProtocol() const;
|
||||
|
||||
const QString &remoteAddress() const;
|
||||
@@ -48,14 +44,10 @@ public:
|
||||
#endif
|
||||
|
||||
public slots:
|
||||
void connectToVpn(int serverIndex,
|
||||
const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration);
|
||||
|
||||
void connectToVpn(int serverIndex, const ServerCredentials &credentials, DockerContainer container, const QJsonObject &vpnConfiguration);
|
||||
void reconnectToVpn();
|
||||
void disconnectFromVpn();
|
||||
|
||||
void addRoutes(const QStringList &ips);
|
||||
void deleteRoutes(const QStringList &ips);
|
||||
void flushDns();
|
||||
void onKillSwitchModeChanged(bool enabled);
|
||||
void disconnectSlots();
|
||||
|
||||
@@ -70,6 +62,8 @@ protected slots:
|
||||
void onBytesChanged(quint64 receivedBytes, quint64 sentBytes);
|
||||
void onConnectionStateChanged(Vpn::ConnectionState state);
|
||||
|
||||
void setConnectionState(Vpn::ConnectionState state);
|
||||
|
||||
protected:
|
||||
QSharedPointer<VpnProtocol> m_vpnProtocol;
|
||||
|
||||
@@ -89,6 +83,8 @@ private:
|
||||
void createAndroidConnections();
|
||||
#endif
|
||||
|
||||
Vpn::ConnectionState m_connectionState;
|
||||
|
||||
void createProtocolConnections();
|
||||
|
||||
void appendSplitTunnelingConfig();
|
||||
|
||||
@@ -23,6 +23,7 @@ Options:
|
||||
By default, the latest available platform is used
|
||||
-m, --move Move the build result to the root of the build directory
|
||||
-f, --fdroid Build for F-Droid
|
||||
-p, --play Build AAB for Google Play
|
||||
-h, --help Display this help
|
||||
|
||||
EOT
|
||||
@@ -30,7 +31,7 @@ EOT
|
||||
|
||||
BUILD_TYPE="release"
|
||||
|
||||
opts=$(getopt -l debug,aab,apk:,build-platform:,move,fdroid,help -o "dua:b:mfh" -- "$@")
|
||||
opts=$(getopt -l debug,aab,apk:,build-platform:,move,fdroid,play,help -o "dua:b:mfph" -- "$@")
|
||||
eval set -- "$opts"
|
||||
while true; do
|
||||
case "$1" in
|
||||
@@ -40,6 +41,7 @@ while true; do
|
||||
-b | --build-platform) ANDROID_BUILD_PLATFORM=$2; shift 2;;
|
||||
-m | --move) MOVE_RESULT=1; shift;;
|
||||
-f | --fdroid) FDROID=1; shift;;
|
||||
-p | --play) PLAY=1; shift;;
|
||||
-h | --help) usage; exit 0;;
|
||||
--) shift; break;;
|
||||
esac
|
||||
@@ -149,11 +151,17 @@ if [ -v FDROID ]; then
|
||||
BUILD_TYPE="fdroid"
|
||||
fi
|
||||
|
||||
if [ -v PLAY ]; then
|
||||
AAB_FLAVOR="play"
|
||||
else
|
||||
AAB_FLAVOR="oss"
|
||||
fi
|
||||
|
||||
if [ -v AAB ]; then
|
||||
gradle_opts+=(bundle"${BUILD_TYPE^}")
|
||||
gradle_opts+=(bundle"${AAB_FLAVOR^}${BUILD_TYPE^}")
|
||||
fi
|
||||
if [ -v ABIS ]; then
|
||||
gradle_opts+=(assemble"${BUILD_TYPE^}")
|
||||
gradle_opts+=(assembleOss"${BUILD_TYPE^}")
|
||||
fi
|
||||
|
||||
$OUT_APP_DIR/android-build/gradlew \
|
||||
@@ -164,7 +172,7 @@ $OUT_APP_DIR/android-build/gradlew \
|
||||
if [[ -v CI || -v MOVE_RESULT ]]; then
|
||||
echo "Moving APK/AAB..."
|
||||
if [ -v AAB ]; then
|
||||
mv -u $OUT_APP_DIR/android-build/build/outputs/bundle/$BUILD_TYPE/AmneziaVPN-$BUILD_TYPE.aab \
|
||||
mv -u $OUT_APP_DIR/android-build/build/outputs/bundle/$AAB_FLAVOR"${BUILD_TYPE^}"/AmneziaVPN-$AAB_FLAVOR-$BUILD_TYPE.aab \
|
||||
$PROJECT_DIR/deploy/build/
|
||||
fi
|
||||
|
||||
@@ -181,8 +189,8 @@ if [[ -v CI || -v MOVE_RESULT ]]; then
|
||||
IFS=';' read -r -a abi_array <<< "$ABIS"
|
||||
for ABI in "${abi_array[@]}"
|
||||
do
|
||||
mv -u $OUT_APP_DIR/android-build/build/outputs/apk/$BUILD_TYPE/AmneziaVPN-$ABI-$suffix.apk \
|
||||
mv -u $OUT_APP_DIR/android-build/build/outputs/apk/oss/$BUILD_TYPE/AmneziaVPN-oss-$ABI-$suffix.apk \
|
||||
$PROJECT_DIR/deploy/build/
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -14,6 +14,8 @@ sc stop AmneziaVPN-service
|
||||
sc delete AmneziaVPN-service
|
||||
sc stop AmneziaWGTunnel$AmneziaVPN
|
||||
sc delete AmneziaWGTunnel$AmneziaVPN
|
||||
sc stop AmneziaVPNSplitTunnel
|
||||
sc delete AmneziaVPNSplitTunnel
|
||||
taskkill /IM "AmneziaVPN-service.exe" /F
|
||||
taskkill /IM "AmneziaVPN.exe" /F
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ sc stop AmneziaVPN-service
|
||||
sc delete AmneziaVPN-service
|
||||
sc stop AmneziaWGTunnel$AmneziaVPN
|
||||
sc delete AmneziaWGTunnel$AmneziaVPN
|
||||
sc stop AmneziaVPNSplitTunnel
|
||||
sc delete AmneziaVPNSplitTunnel
|
||||
taskkill /IM "AmneziaVPN-service.exe" /F
|
||||
taskkill /IM "AmneziaVPN.exe" /F
|
||||
|
||||
|
||||
@@ -45,5 +45,6 @@ class IpcInterface
|
||||
SLOT( bool stopNetworkCheck() );
|
||||
|
||||
SIGNAL( connectionLose() );
|
||||
SIGNAL( networkChange() );
|
||||
SIGNAL( wakeup() );
|
||||
SIGNAL( networkChanged() );
|
||||
};
|
||||
|
||||
@@ -33,18 +33,10 @@ KillSwitch* KillSwitch::instance()
|
||||
|
||||
bool KillSwitch::init()
|
||||
{
|
||||
#ifdef Q_OS_LINUX
|
||||
if (!LinuxFirewall::isInstalled()) {
|
||||
LinuxFirewall::install();
|
||||
}
|
||||
m_appSettigns = QSharedPointer<SecureQSettings>(new SecureQSettings(ORGANIZATION_NAME, APPLICATION_NAME, nullptr));
|
||||
#endif
|
||||
#ifdef Q_OS_MACOS
|
||||
if (!MacOSFirewall::isInstalled()) {
|
||||
MacOSFirewall::install();
|
||||
}
|
||||
#if defined(Q_OS_LINUX) || defined(Q_OS_MACOS)
|
||||
m_appSettigns = QSharedPointer<SecureQSettings>(new SecureQSettings(ORGANIZATION_NAME, APPLICATION_NAME, nullptr));
|
||||
#endif
|
||||
|
||||
if (isStrictKillSwitchEnabled()) {
|
||||
return disableAllTraffic();
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ LocalServer::LocalServer(QObject *parent) : QObject(parent),
|
||||
}
|
||||
|
||||
m_networkWatcher.initialize();
|
||||
connect(&m_networkWatcher, &NetworkWatcher::sleepMode, &m_ipcServer, &IpcServer::networkChange);
|
||||
connect(&m_networkWatcher, &NetworkWatcher::networkChange, &m_ipcServer, &IpcServer::networkChange);
|
||||
connect(&m_networkWatcher, &NetworkWatcher::networkChanged, &m_ipcServer, &IpcServer::networkChanged);
|
||||
connect(&m_networkWatcher, &NetworkWatcher::wakeup, &m_ipcServer, &IpcServer::wakeup);
|
||||
KillSwitch::instance()->init();
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
|
||||
Reference in New Issue
Block a user