mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-23 02:00:20 +07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e4f8a5ec5 | |||
| 6de556e730 | |||
| 1134dc194b | |||
| 67bd880cdf |
+1
-2
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
|
||||||
|
|
||||||
set(PROJECT AmneziaVPN)
|
set(PROJECT AmneziaVPN)
|
||||||
set(AMNEZIAVPN_VERSION 4.8.14.6)
|
set(AMNEZIAVPN_VERSION 4.8.14.5)
|
||||||
|
|
||||||
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
|
||||||
DESCRIPTION "AmneziaVPN"
|
DESCRIPTION "AmneziaVPN"
|
||||||
@@ -12,7 +12,6 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
|
|||||||
set(RELEASE_DATE "${CURRENT_DATE}")
|
set(RELEASE_DATE "${CURRENT_DATE}")
|
||||||
|
|
||||||
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
|
||||||
|
|
||||||
set(APP_ANDROID_VERSION_CODE 2118)
|
set(APP_ANDROID_VERSION_CODE 2118)
|
||||||
|
|
||||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||||
|
|||||||
@@ -109,6 +109,16 @@ void AmneziaApplication::init()
|
|||||||
// install filter on main window
|
// install filter on main window
|
||||||
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
|
||||||
win->installEventFilter(this);
|
win->installEventFilter(this);
|
||||||
|
#ifdef Q_OS_ANDROID
|
||||||
|
QObject::connect(win, &QQuickWindow::sceneGraphError,
|
||||||
|
[](QQuickWindow::SceneGraphError, const QString &msg) {
|
||||||
|
qWarning() << "Scene graph error (suppressed):" << msg;
|
||||||
|
});
|
||||||
|
// Keep graphics context alive across hide/show cycles to avoid
|
||||||
|
// eglSwapBuffers/makeCurrent being called on a context Android has reclaimed.
|
||||||
|
win->setPersistentSceneGraph(true);
|
||||||
|
win->setPersistentGraphics(true);
|
||||||
|
#endif
|
||||||
win->show();
|
win->show();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -122,5 +122,4 @@ dependencies {
|
|||||||
implementation(libs.google.mlkit)
|
implementation(libs.google.mlkit)
|
||||||
implementation(libs.androidx.datastore)
|
implementation(libs.androidx.datastore)
|
||||||
implementation(libs.androidx.biometric)
|
implementation(libs.androidx.biometric)
|
||||||
implementation(libs.google.play.review)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ androidx-datastore = "1.1.1"
|
|||||||
kotlinx-coroutines = "1.8.1"
|
kotlinx-coroutines = "1.8.1"
|
||||||
kotlinx-serialization = "1.6.3"
|
kotlinx-serialization = "1.6.3"
|
||||||
google-mlkit = "17.3.0"
|
google-mlkit = "17.3.0"
|
||||||
google-play-review = "2.0.2"
|
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||||
@@ -29,7 +28,6 @@ androidx-datastore = { module = "androidx.datastore:datastore-preferences", vers
|
|||||||
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||||
kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" }
|
kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" }
|
||||||
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }
|
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }
|
||||||
google-play-review = { module = "com.google.android.play:review-ktx", version.ref = "google-play-review" }
|
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
androidx-camera = [
|
androidx-camera = [
|
||||||
|
|||||||
@@ -297,10 +297,7 @@ class AmneziaActivity : QtActivity() {
|
|||||||
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
|
||||||
|
|
||||||
// Cancel pending operations if window loses focus
|
// Cancel pending operations if window loses focus
|
||||||
if (hasFocus) {
|
if (!hasFocus) {
|
||||||
ReviewManager.onWindowFocusGained(this, mainScope)
|
|
||||||
} else {
|
|
||||||
// Cancel pending operations if window loses focus
|
|
||||||
resumeHandler.removeCallbacksAndMessages(null)
|
resumeHandler.removeCallbacksAndMessages(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,8 +350,6 @@ class AmneziaActivity : QtActivity() {
|
|||||||
isActivityResumed = true
|
isActivityResumed = true
|
||||||
Log.d(TAG, "Resume Amnezia activity")
|
Log.d(TAG, "Resume Amnezia activity")
|
||||||
|
|
||||||
ReviewManager.onActivityResumed(mainScope)
|
|
||||||
|
|
||||||
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
|
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
|
||||||
val uri = pendingOpenFileUri!!
|
val uri = pendingOpenFileUri!!
|
||||||
openFileDeliveryScheduled = true
|
openFileDeliveryScheduled = true
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package org.amnezia.vpn
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import com.google.android.play.core.review.ReviewManagerFactory
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.amnezia.vpn.util.Log
|
|
||||||
import org.amnezia.vpn.util.Prefs
|
|
||||||
|
|
||||||
private const val TAG = "ReviewManager"
|
|
||||||
|
|
||||||
private const val PREFS_REVIEW_OPEN_COUNT = "REVIEW_OPEN_COUNT"
|
|
||||||
private const val REVIEW_REQUEST_OPEN_INTERVAL = 20
|
|
||||||
|
|
||||||
object ReviewManager {
|
|
||||||
|
|
||||||
@Volatile
|
|
||||||
private var shouldRequestReviewOnFocus = false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call from onResume
|
|
||||||
* Increments the open count and arms the flag if the interval is hit.
|
|
||||||
* I/O runs on the IO dispatcher to avoid blocking the main thread.
|
|
||||||
*/
|
|
||||||
fun onActivityResumed(scope: CoroutineScope) {
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
val openCount = Prefs.load<Int>(PREFS_REVIEW_OPEN_COUNT) + 1
|
|
||||||
val shouldRequest = openCount >= REVIEW_REQUEST_OPEN_INTERVAL
|
|
||||||
Prefs.save(PREFS_REVIEW_OPEN_COUNT, if (shouldRequest) 0 else openCount)
|
|
||||||
|
|
||||||
shouldRequestReviewOnFocus = shouldRequest
|
|
||||||
Log.i(TAG, "onActivityResumed: openCount=$openCount, shouldRequestReview=$shouldRequestReviewOnFocus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call from onWindowFocusChanged (hasFocus=true)
|
|
||||||
*/
|
|
||||||
fun onWindowFocusGained(activity: Activity, scope: CoroutineScope) {
|
|
||||||
if (!shouldRequestReviewOnFocus) return
|
|
||||||
shouldRequestReviewOnFocus = false
|
|
||||||
|
|
||||||
scope.launch(Dispatchers.Main) {
|
|
||||||
requestReviewFlow(activity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun requestReviewFlow(activity: Activity) {
|
|
||||||
val reviewManager = ReviewManagerFactory.create(activity)
|
|
||||||
reviewManager.requestReviewFlow().addOnCompleteListener { request ->
|
|
||||||
if (request.isSuccessful) {
|
|
||||||
reviewManager.launchReviewFlow(activity, request.result)
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Review flow request failed: ${request.exception}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -121,6 +121,7 @@ target_sources(${PROJECT} PRIVATE
|
|||||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||||
)
|
)
|
||||||
|
|
||||||
target_sources(${PROJECT} PRIVATE
|
target_sources(${PROJECT} PRIVATE
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ target_sources(${PROJECT} PRIVATE
|
|||||||
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift
|
||||||
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
${CLIENT_ROOT_DIR}/platforms/ios/VPNCController.swift
|
||||||
|
${CLIENT_ROOT_DIR}/platforms/ios/StoreKit2Helper.swift
|
||||||
)
|
)
|
||||||
|
|
||||||
target_sources(${PROJECT} PRIVATE
|
target_sources(${PROJECT} PRIVATE
|
||||||
|
|||||||
@@ -90,39 +90,46 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
|||||||
const int httpStatusCodeConflict = 409;
|
const int httpStatusCodeConflict = 409;
|
||||||
const int httpStatusCodeNotFound = 404;
|
const int httpStatusCodeNotFound = 404;
|
||||||
const int httpStatusCodeNotImplemented = 501;
|
const int httpStatusCodeNotImplemented = 501;
|
||||||
|
const int httpStatusCodePaymentRequired = 402;
|
||||||
|
|
||||||
if (!sslErrors.empty()) {
|
if (!sslErrors.empty()) {
|
||||||
qDebug().noquote() << sslErrors;
|
qDebug().noquote() << sslErrors;
|
||||||
return amnezia::ErrorCode::ApiConfigSslError;
|
return amnezia::ErrorCode::ApiConfigSslError;
|
||||||
} else if (replyError == QNetworkReply::NoError) {
|
}
|
||||||
|
if (replyError == QNetworkReply::NoError) {
|
||||||
return amnezia::ErrorCode::NoError;
|
return amnezia::ErrorCode::NoError;
|
||||||
} else if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
}
|
||||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||||
|
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||||
} else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
}
|
||||||
|
if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) {
|
||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||||
} else {
|
}
|
||||||
qDebug() << QString::fromUtf8(responseBody);
|
|
||||||
qDebug() << replyError;
|
|
||||||
qDebug() << replyErrorString;
|
|
||||||
qDebug() << httpStatusCode;
|
|
||||||
|
|
||||||
int httpStatusFromBody = -1;
|
qDebug() << QString::fromUtf8(responseBody);
|
||||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
qDebug() << replyError;
|
||||||
if (jsonDoc.isObject()) {
|
qDebug() << replyErrorString;
|
||||||
QJsonObject jsonObj = jsonDoc.object();
|
qDebug() << httpStatusCode;
|
||||||
httpStatusFromBody = jsonObj.value("http_status").toInt(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||||
|
if (jsonDoc.isObject()) {
|
||||||
|
QJsonObject jsonObj = jsonDoc.object();
|
||||||
|
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||||
return amnezia::ErrorCode::ApiConfigLimitError;
|
return amnezia::ErrorCode::ApiConfigLimitError;
|
||||||
} else if (httpStatusFromBody == httpStatusCodeNotFound) {
|
}
|
||||||
|
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||||
return amnezia::ErrorCode::ApiNotFoundError;
|
return amnezia::ErrorCode::ApiNotFoundError;
|
||||||
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
}
|
||||||
|
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||||
}
|
}
|
||||||
|
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||||
|
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||||
|
}
|
||||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ namespace
|
|||||||
|
|
||||||
constexpr int httpStatusCodeNotFound = 404;
|
constexpr int httpStatusCodeNotFound = 404;
|
||||||
constexpr int httpStatusCodeConflict = 409;
|
constexpr int httpStatusCodeConflict = 409;
|
||||||
|
|
||||||
constexpr int httpStatusCodeNotImplemented = 501;
|
constexpr int httpStatusCodeNotImplemented = 501;
|
||||||
|
constexpr int httpStatusCodePaymentRequired = 402;
|
||||||
}
|
}
|
||||||
|
|
||||||
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
|
||||||
@@ -451,6 +451,8 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
|||||||
}
|
}
|
||||||
} else if (httpStatus == httpStatusCodeConflict) {
|
} else if (httpStatus == httpStatusCodeConflict) {
|
||||||
return false;
|
return false;
|
||||||
|
} else if (httpStatus == httpStatusCodePaymentRequired) {
|
||||||
|
return false;
|
||||||
} else if (replyError != QNetworkReply::NetworkError::NoError) {
|
} else if (replyError != QNetworkReply::NetworkError::NoError) {
|
||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -123,6 +123,8 @@ namespace amnezia
|
|||||||
ApiUpdateRequestError = 1111,
|
ApiUpdateRequestError = 1111,
|
||||||
ApiSubscriptionExpiredError = 1112,
|
ApiSubscriptionExpiredError = 1112,
|
||||||
ApiPurchaseError = 1113,
|
ApiPurchaseError = 1113,
|
||||||
|
ApiSubscriptionNotActiveError = 1114,
|
||||||
|
ApiNoPurchasedSubscriptionsError = 1115,
|
||||||
|
|
||||||
// QFile errors
|
// QFile errors
|
||||||
OpenError = 1200,
|
OpenError = 1200,
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ QString errorString(ErrorCode code) {
|
|||||||
case (ErrorCode::ApiUpdateRequestError): errorMessage = QObject::tr("Please update the application to use this feature"); break;
|
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::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::ApiPurchaseError): errorMessage = QObject::tr("Unable to process purchase"); break;
|
||||||
|
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); break;
|
||||||
|
case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break;
|
||||||
|
|
||||||
// QFile errors
|
// QFile errors
|
||||||
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
case(ErrorCode::OpenError): errorMessage = QObject::tr("QFile error: The file could not be opened"); break;
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import Foundation
|
||||||
|
import StoreKit
|
||||||
|
|
||||||
|
@available(iOS 15.0, macOS 12.0, *)
|
||||||
|
@objcMembers
|
||||||
|
public class StoreKit2Helper: NSObject {
|
||||||
|
|
||||||
|
public static let shared = StoreKit2Helper()
|
||||||
|
|
||||||
|
private struct EntitlementInfo {
|
||||||
|
let transactionId: UInt64
|
||||||
|
let originalTransactionId: UInt64
|
||||||
|
let productId: String
|
||||||
|
let purchaseDate: Date
|
||||||
|
|
||||||
|
var dictionary: NSDictionary {
|
||||||
|
[
|
||||||
|
"transactionId": String(transactionId),
|
||||||
|
"originalTransactionId": String(originalTransactionId),
|
||||||
|
"productId": productId
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func fetchCurrentEntitlements(completion: @escaping (Bool, [NSDictionary]?, NSError?) -> Void) {
|
||||||
|
Task { @MainActor in
|
||||||
|
do {
|
||||||
|
try await AppStore.sync()
|
||||||
|
|
||||||
|
var entitlements: [EntitlementInfo] = []
|
||||||
|
for await result in Transaction.currentEntitlements {
|
||||||
|
switch result {
|
||||||
|
case .verified(let transaction):
|
||||||
|
entitlements.append(EntitlementInfo(transactionId: transaction.id,
|
||||||
|
originalTransactionId: transaction.originalID,
|
||||||
|
productId: transaction.productID,
|
||||||
|
purchaseDate: transaction.purchaseDate))
|
||||||
|
case .unverified(_, let error):
|
||||||
|
print("[IAP][StoreKit2] Unverified transaction skipped: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let sortedEntitlements = entitlements.sorted { lhs, rhs in
|
||||||
|
if lhs.purchaseDate != rhs.purchaseDate {
|
||||||
|
return lhs.purchaseDate > rhs.purchaseDate
|
||||||
|
}
|
||||||
|
return lhs.transactionId > rhs.transactionId
|
||||||
|
}.map { $0.dictionary }
|
||||||
|
completion(true, sortedEntitlements, nil)
|
||||||
|
} catch {
|
||||||
|
completion(false, nil, error as NSError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func purchaseProduct(productIdentifier: String, completion: @escaping (Bool, String?, String?, String?, NSError?) -> Void) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let products = try await Product.products(for: [productIdentifier])
|
||||||
|
guard let product = products.first else {
|
||||||
|
let error = NSError(domain: "StoreKit2Helper", code: 0, userInfo: [NSLocalizedDescriptionKey: "Product not found"])
|
||||||
|
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let result = try await product.purchase()
|
||||||
|
switch result {
|
||||||
|
case .success(let verification):
|
||||||
|
switch verification {
|
||||||
|
case .verified(let transaction):
|
||||||
|
await transaction.finish()
|
||||||
|
let txId = String(transaction.id)
|
||||||
|
let origTxId = String(transaction.originalID)
|
||||||
|
let pId = transaction.productID
|
||||||
|
DispatchQueue.main.async { completion(true, txId, pId, origTxId, nil) }
|
||||||
|
case .unverified(_, let error):
|
||||||
|
DispatchQueue.main.async { completion(false, nil, nil, nil, error as NSError) }
|
||||||
|
}
|
||||||
|
case .userCancelled:
|
||||||
|
let error = NSError(domain: "StoreKit2Helper", code: 1, userInfo: [NSLocalizedDescriptionKey: "Purchase cancelled"])
|
||||||
|
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
|
||||||
|
case .pending:
|
||||||
|
let error = NSError(domain: "StoreKit2Helper", code: 2, userInfo: [NSLocalizedDescriptionKey: "Purchase pending"])
|
||||||
|
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
|
||||||
|
@unknown default:
|
||||||
|
let error = NSError(domain: "StoreKit2Helper", code: 3, userInfo: [NSLocalizedDescriptionKey: "Unknown purchase result"])
|
||||||
|
DispatchQueue.main.async { completion(false, nil, nil, nil, error) }
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
DispatchQueue.main.async { completion(false, nil, nil, nil, error as NSError) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func storefrontCurrencyCode(for product: Product) -> String {
|
||||||
|
product.priceFormatStyle.locale.currencyCode ?? ""
|
||||||
|
}
|
||||||
|
|
||||||
|
public func fetchProducts(identifiers: Set<String>, completion: @escaping ([NSDictionary], [String], NSError?) -> Void) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let products = try await Product.products(for: identifiers)
|
||||||
|
let productDicts = products.map { product -> NSDictionary in
|
||||||
|
let currencyCode = storefrontCurrencyCode(for: product)
|
||||||
|
return [
|
||||||
|
"productId": product.id,
|
||||||
|
"title": product.displayName,
|
||||||
|
"description": product.description,
|
||||||
|
"price": "\(product.price)",
|
||||||
|
"currencyCode": currencyCode
|
||||||
|
]
|
||||||
|
}
|
||||||
|
let fetchedIds = Set(products.map { $0.id })
|
||||||
|
let invalidIdentifiers = identifiers.filter { !fetchedIds.contains($0) }
|
||||||
|
DispatchQueue.main.async { completion(productDicts, Array(invalidIdentifiers), nil) }
|
||||||
|
} catch {
|
||||||
|
DispatchQueue.main.async { completion([], Array(identifiers), error as NSError) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,27 +4,12 @@
|
|||||||
|
|
||||||
#import "StoreKitController.h"
|
#import "StoreKitController.h"
|
||||||
#import <StoreKit/StoreKit.h>
|
#import <StoreKit/StoreKit.h>
|
||||||
|
#import <AmneziaVPN-Swift.h>
|
||||||
|
|
||||||
#include <QtCore/QDebug>
|
#include <QtCore/QDebug>
|
||||||
#include <QtCore/QString>
|
#include <QtCore/QString>
|
||||||
|
|
||||||
API_AVAILABLE(ios(15.0), macos(12.0))
|
API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
@interface StoreKitController () <SKProductsRequestDelegate, SKPaymentTransactionObserver>
|
|
||||||
@property (nonatomic, copy) void (^purchaseCompletion)(BOOL success,
|
|
||||||
NSString *_Nullable transactionId,
|
|
||||||
NSString *_Nullable productId,
|
|
||||||
NSString *_Nullable originalTransactionId,
|
|
||||||
NSError *_Nullable error);
|
|
||||||
@property (nonatomic, copy) void (^restoreCompletion)(BOOL success,
|
|
||||||
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
|
||||||
NSError *_Nullable error);
|
|
||||||
@property (nonatomic, copy) void (^productsFetchCompletion)(NSArray<NSDictionary *> *products,
|
|
||||||
NSArray<NSString *> *invalidIdentifiers,
|
|
||||||
NSError *_Nullable error);
|
|
||||||
@property (nonatomic, strong) SKProductsRequest *productsRequest;
|
|
||||||
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *restoredTransactions;
|
|
||||||
@end
|
|
||||||
|
|
||||||
@implementation StoreKitController
|
@implementation StoreKitController
|
||||||
|
|
||||||
+ (instancetype)sharedInstance
|
+ (instancetype)sharedInstance
|
||||||
@@ -42,17 +27,9 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
|||||||
- (instancetype)init API_AVAILABLE(ios(15.0), macos(12.0))
|
- (instancetype)init API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
{
|
{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
if (self) {
|
|
||||||
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
|
|
||||||
}
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc
|
|
||||||
{
|
|
||||||
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)purchaseProduct:(NSString *)productIdentifier
|
- (void)purchaseProduct:(NSString *)productIdentifier
|
||||||
completion:(void (^)(BOOL success,
|
completion:(void (^)(BOOL success,
|
||||||
NSString *_Nullable transactionId,
|
NSString *_Nullable transactionId,
|
||||||
@@ -60,41 +37,50 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
|||||||
NSString *_Nullable originalTransactionId,
|
NSString *_Nullable originalTransactionId,
|
||||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
{
|
{
|
||||||
self.purchaseCompletion = completion;
|
qInfo().noquote() << "[IAP][StoreKit2] Starting purchase for" << QString::fromUtf8(productIdentifier.UTF8String);
|
||||||
|
[[StoreKit2Helper shared] purchaseProductWithProductIdentifier:productIdentifier
|
||||||
qInfo().noquote() << "[IAP][StoreKit] Starting purchase for" << QString::fromUtf8(productIdentifier.UTF8String);
|
completion:^(BOOL success,
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
NSString *transactionId,
|
||||||
[self performPurchaseAsync:productIdentifier];
|
NSString *productId,
|
||||||
});
|
NSString *originalTransactionId,
|
||||||
}
|
NSError *error) {
|
||||||
|
if (success) {
|
||||||
- (void)performPurchaseAsync:(NSString *)productIdentifier API_AVAILABLE(ios(15.0), macos(12.0))
|
qInfo().noquote() << "[IAP][StoreKit2] Purchase success. transactionId =" << QString::fromUtf8(transactionId.UTF8String)
|
||||||
{
|
<< "originalTransactionId =" << QString::fromUtf8(originalTransactionId.UTF8String)
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
<< "productId =" << QString::fromUtf8(productId.UTF8String);
|
||||||
@try {
|
} else if (error) {
|
||||||
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:productIdentifier]];
|
qWarning().noquote() << "[IAP][StoreKit2] Purchase failed:" << QString::fromUtf8(error.localizedDescription.UTF8String);
|
||||||
request.delegate = self;
|
|
||||||
[request start];
|
|
||||||
|
|
||||||
} @catch (NSException *exception) {
|
|
||||||
NSError *error = [NSError errorWithDomain:@"StoreKitController"
|
|
||||||
code:1
|
|
||||||
userInfo:@{ NSLocalizedDescriptionKey : exception.reason ?: @"Purchase failed" }];
|
|
||||||
if (self.purchaseCompletion) {
|
|
||||||
self.purchaseCompletion(NO, nil, nil, nil, error);
|
|
||||||
self.purchaseCompletion = nil;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
if (completion) {
|
||||||
|
completion(success, transactionId, productId, originalTransactionId, error);
|
||||||
|
}
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)restorePurchasesWithCompletion:(void (^)(BOOL success,
|
- (void)restorePurchasesWithCompletion:(void (^)(BOOL success,
|
||||||
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
NSArray<NSDictionary *> *_Nullable restoredTransactions,
|
||||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
{
|
{
|
||||||
self.restoreCompletion = completion;
|
[[StoreKit2Helper shared] fetchCurrentEntitlementsWithCompletion:^(BOOL success,
|
||||||
self.restoredTransactions = [NSMutableArray array];
|
NSArray<NSDictionary *> *entitlements,
|
||||||
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
|
NSError *error) {
|
||||||
|
if (success) {
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit2] currentEntitlements returned"
|
||||||
|
<< (int)(entitlements ? entitlements.count : 0) << "active entitlements";
|
||||||
|
for (NSDictionary *info in entitlements) {
|
||||||
|
qInfo().noquote() << "[IAP][StoreKit2] Active entitlement:"
|
||||||
|
<< "transactionId=" << QString::fromUtf8([info[@"transactionId"] UTF8String])
|
||||||
|
<< "originalTransactionId=" << QString::fromUtf8([info[@"originalTransactionId"] UTF8String])
|
||||||
|
<< "productId=" << QString::fromUtf8([info[@"productId"] UTF8String]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
qWarning().noquote() << "[IAP][StoreKit2] fetchCurrentEntitlements failed:"
|
||||||
|
<< QString::fromUtf8(error.localizedDescription.UTF8String);
|
||||||
|
}
|
||||||
|
if (completion) {
|
||||||
|
completion(success, entitlements, error);
|
||||||
|
}
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)fetchProductsWithIdentifiers:(NSSet<NSString *> *)productIdentifiers
|
- (void)fetchProductsWithIdentifiers:(NSSet<NSString *> *)productIdentifiers
|
||||||
@@ -102,163 +88,21 @@ API_AVAILABLE(ios(15.0), macos(12.0))
|
|||||||
NSArray<NSString *> *invalidIdentifiers,
|
NSArray<NSString *> *invalidIdentifiers,
|
||||||
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
NSError *_Nullable error))completion API_AVAILABLE(ios(15.0), macos(12.0))
|
||||||
{
|
{
|
||||||
self.productsFetchCompletion = completion;
|
[[StoreKit2Helper shared] fetchProductsWithIdentifiers:productIdentifiers
|
||||||
self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];
|
completion:^(NSArray<NSDictionary *> *products,
|
||||||
self.productsRequest.delegate = self;
|
NSArray<NSString *> *invalidIdentifiers,
|
||||||
[self.productsRequest start];
|
NSError *error) {
|
||||||
}
|
if (!error) {
|
||||||
|
for (NSDictionary *p in products) {
|
||||||
#pragma mark - SKProductsRequestDelegate / SKRequestDelegate
|
qInfo().noquote() << "[IAP][StoreKit2] Fetched product info" << QString::fromUtf8([p[@"productId"] UTF8String])
|
||||||
|
<< "price=" << QString::fromUtf8([p[@"price"] UTF8String])
|
||||||
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
|
<< "currency=" << QString::fromUtf8([p[@"currencyCode"] UTF8String]);
|
||||||
{
|
|
||||||
if (self.purchaseCompletion) {
|
|
||||||
SKProduct *product = response.products.firstObject;
|
|
||||||
if (!product) {
|
|
||||||
NSError *error = [NSError errorWithDomain:@"StoreKitController"
|
|
||||||
code:0
|
|
||||||
userInfo:@{ NSLocalizedDescriptionKey : @"Product not found" }];
|
|
||||||
self.purchaseCompletion(NO, nil, nil, nil, error);
|
|
||||||
self.purchaseCompletion = nil;
|
|
||||||
self.productsRequest = nil;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
NSString *currencyCode = [product.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @"";
|
|
||||||
NSString *priceString = [product.price stringValue] ?: @"";
|
|
||||||
qInfo().noquote() << "[IAP][StoreKit] Received product" << QString::fromUtf8(product.productIdentifier.UTF8String)
|
|
||||||
<< "price=" << QString::fromUtf8(priceString.UTF8String)
|
|
||||||
<< "currency=" << QString::fromUtf8(currencyCode.UTF8String);
|
|
||||||
SKPayment *payment = [SKPayment paymentWithProduct:product];
|
|
||||||
[[SKPaymentQueue defaultQueue] addPayment:payment];
|
|
||||||
self.productsRequest = nil;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.productsFetchCompletion) {
|
|
||||||
NSMutableArray<NSDictionary *> *productDicts = [NSMutableArray array];
|
|
||||||
for (SKProduct *p in response.products) {
|
|
||||||
NSDictionary *productDict = @{
|
|
||||||
@"productId": p.productIdentifier,
|
|
||||||
@"title": p.localizedTitle,
|
|
||||||
@"description": p.localizedDescription,
|
|
||||||
@"price": p.price.stringValue,
|
|
||||||
@"currencyCode": [p.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @""
|
|
||||||
};
|
|
||||||
[productDicts addObject:productDict];
|
|
||||||
NSString *productCurrency = [p.priceLocale objectForKey:NSLocaleCurrencyCode] ?: @"";
|
|
||||||
NSString *productPrice = [p.price stringValue] ?: @"";
|
|
||||||
qInfo().noquote() << "[IAP][StoreKit] Fetched product info" << QString::fromUtf8(p.productIdentifier.UTF8String)
|
|
||||||
<< "price=" << QString::fromUtf8(productPrice.UTF8String)
|
|
||||||
<< "currency=" << QString::fromUtf8(productCurrency.UTF8String);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.productsFetchCompletion(productDicts, response.invalidProductIdentifiers, nil);
|
|
||||||
self.productsFetchCompletion = nil;
|
|
||||||
self.productsRequest = nil;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
|
|
||||||
{
|
|
||||||
if (self.purchaseCompletion) {
|
|
||||||
self.purchaseCompletion(NO, nil, nil, nil, error);
|
|
||||||
self.purchaseCompletion = nil;
|
|
||||||
}
|
|
||||||
if (self.productsFetchCompletion) {
|
|
||||||
self.productsFetchCompletion(@[], @[], error);
|
|
||||||
self.productsFetchCompletion = nil;
|
|
||||||
}
|
|
||||||
self.productsRequest = nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma mark - SKPaymentTransactionObserver
|
|
||||||
|
|
||||||
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
|
|
||||||
{
|
|
||||||
for (SKPaymentTransaction *transaction in transactions) {
|
|
||||||
switch (transaction.transactionState) {
|
|
||||||
case SKPaymentTransactionStatePurchased: {
|
|
||||||
NSString *originalTransactionId = transaction.originalTransaction.transactionIdentifier ?: transaction.transactionIdentifier;
|
|
||||||
qInfo().noquote() << "[IAP][StoreKit] Transaction purchased" << QString::fromUtf8(transaction.transactionIdentifier.UTF8String)
|
|
||||||
<< "original=" << QString::fromUtf8((originalTransactionId ?: @"").UTF8String)
|
|
||||||
<< "product=" << QString::fromUtf8(transaction.payment.productIdentifier.UTF8String);
|
|
||||||
|
|
||||||
if (self.purchaseCompletion) {
|
|
||||||
self.purchaseCompletion(YES,
|
|
||||||
transaction.transactionIdentifier,
|
|
||||||
transaction.payment.productIdentifier,
|
|
||||||
originalTransactionId,
|
|
||||||
nil);
|
|
||||||
self.purchaseCompletion = nil;
|
|
||||||
}
|
}
|
||||||
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case SKPaymentTransactionStateFailed:
|
if (completion) {
|
||||||
qInfo().noquote() << "[IAP][StoreKit] Transaction failed" << QString::fromUtf8(transaction.transactionIdentifier.UTF8String)
|
completion(products ?: @[], invalidIdentifiers ?: @[], error);
|
||||||
<< "product=" << QString::fromUtf8(transaction.payment.productIdentifier.UTF8String)
|
|
||||||
<< "error=" << QString::fromUtf8(transaction.error.localizedDescription.UTF8String);
|
|
||||||
if (self.purchaseCompletion) {
|
|
||||||
self.purchaseCompletion(NO,
|
|
||||||
transaction.transactionIdentifier,
|
|
||||||
transaction.payment.productIdentifier,
|
|
||||||
nil,
|
|
||||||
transaction.error);
|
|
||||||
self.purchaseCompletion = nil;
|
|
||||||
}
|
|
||||||
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
|
||||||
break;
|
|
||||||
case SKPaymentTransactionStateRestored: {
|
|
||||||
if (self.restoreCompletion) {
|
|
||||||
NSString *transactionId = transaction.transactionIdentifier ?: @"";
|
|
||||||
NSString *originalTransactionId = transaction.originalTransaction.transactionIdentifier ?: transactionId;
|
|
||||||
NSString *productId = transaction.payment.productIdentifier ?: @"";
|
|
||||||
|
|
||||||
qInfo().noquote() << "[IAP][StoreKit] Transaction restored"
|
|
||||||
<< QString::fromUtf8(transactionId.UTF8String)
|
|
||||||
<< "original="
|
|
||||||
<< QString::fromUtf8((originalTransactionId ?: @"").UTF8String)
|
|
||||||
<< "product="
|
|
||||||
<< QString::fromUtf8((productId ?: @"").UTF8String);
|
|
||||||
|
|
||||||
NSDictionary *info = @{
|
|
||||||
@"transactionId": transactionId,
|
|
||||||
@"originalTransactionId": originalTransactionId ?: @"",
|
|
||||||
@"productId": productId ?: @""
|
|
||||||
};
|
|
||||||
if (!self.restoredTransactions) {
|
|
||||||
self.restoredTransactions = [NSMutableArray array];
|
|
||||||
}
|
|
||||||
[self.restoredTransactions addObject:info];
|
|
||||||
}
|
|
||||||
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case SKPaymentTransactionStatePurchasing:
|
}];
|
||||||
case SKPaymentTransactionStateDeferred:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
|
|
||||||
{
|
|
||||||
if (self.restoreCompletion) {
|
|
||||||
NSArray<NSDictionary *> *transactions = [self.restoredTransactions copy];
|
|
||||||
self.restoreCompletion(YES, transactions, nil);
|
|
||||||
self.restoreCompletion = nil;
|
|
||||||
self.restoredTransactions = nil;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
|
|
||||||
{
|
|
||||||
if (self.restoreCompletion) {
|
|
||||||
self.restoreCompletion(NO, nil, error);
|
|
||||||
self.restoreCompletion = nil;
|
|
||||||
self.restoredTransactions = nil;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -179,8 +179,9 @@ bool IosController::initialize()
|
|||||||
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
|
[NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
|
||||||
@try {
|
@try {
|
||||||
if (error) {
|
if (error) {
|
||||||
qDebug() << "IosController::initialize : Error:" << [error.localizedDescription UTF8String];
|
qWarning() << "IosController::initialize : loadAllFromPreferences failed:"
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
<< [error.localizedDescription UTF8String]
|
||||||
|
<< "domain:" << [error.domain UTF8String] << "code:" << error.code;
|
||||||
ok = false;
|
ok = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -397,8 +398,14 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
|||||||
{
|
{
|
||||||
NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification;
|
NETunnelProviderSession *session = (NETunnelProviderSession *)pNotification;
|
||||||
|
|
||||||
if (session /* && session == TunnelManager.session */ ) {
|
if (!session) {
|
||||||
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
|
return;
|
||||||
|
}
|
||||||
|
if (!m_currentTunnel || (NETunnelProviderSession *)m_currentTunnel.connection != session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qDebug() << "IosController::vpnStatusDidChange" << iosStatusToState(session.status) << session;
|
||||||
|
|
||||||
if (session.status == NEVPNStatusDisconnected) {
|
if (session.status == NEVPNStatusDisconnected) {
|
||||||
if (@available(iOS 16.0, *)) {
|
if (@available(iOS 16.0, *)) {
|
||||||
@@ -512,7 +519,6 @@ void IosController::vpnStatusDidChange(void *pNotification)
|
|||||||
m_statusRequestInFlight = false;
|
m_statusRequestInFlight = false;
|
||||||
}
|
}
|
||||||
emitConnectionStateIfChanged(nextState);
|
emitConnectionStateIfChanged(nextState);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void IosController::vpnConfigurationDidChange(void *pNotification)
|
void IosController::vpnConfigurationDidChange(void *pNotification)
|
||||||
@@ -835,39 +841,49 @@ void IosController::startTunnel()
|
|||||||
m_rxBytes = 0;
|
m_rxBytes = 0;
|
||||||
m_txBytes = 0;
|
m_txBytes = 0;
|
||||||
|
|
||||||
[m_currentTunnel setEnabled:YES];
|
NETunnelProviderManager *tunnel = m_currentTunnel;
|
||||||
|
[tunnel setEnabled:YES];
|
||||||
|
|
||||||
[m_currentTunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
[tunnel saveToPreferencesWithCompletionHandler:^(NSError *saveError) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
if (saveError) {
|
||||||
|
qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName
|
||||||
|
<< " Tunnel Save Error" << saveError.localizedDescription.UTF8String << " domain:"
|
||||||
|
<< saveError.domain.UTF8String << " code:" << saveError.code;
|
||||||
|
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (saveError) {
|
[tunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
|
||||||
qDebug().nospace() << "IosController::startTunnel" << protocolName << ": Connect " << protocolName << " Tunnel Save Error" << saveError.localizedDescription.UTF8String;
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
if (loadError) {
|
||||||
return;
|
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
||||||
}
|
<< ": Connect " << protocolName << " Tunnel Load Error"
|
||||||
|
<< loadError.localizedDescription.UTF8String;
|
||||||
|
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
[m_currentTunnel loadFromPreferencesWithCompletionHandler:^(NSError *loadError) {
|
NSError *startError = nil;
|
||||||
if (loadError) {
|
qDebug() << iosStatusToState(tunnel.connection.status);
|
||||||
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << ": Connect " << protocolName << " Tunnel Load Error" << loadError.localizedDescription.UTF8String;
|
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSError *startError = nil;
|
BOOL started = [tunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
|
||||||
qDebug() << iosStatusToState(m_currentTunnel.connection.status);
|
|
||||||
|
|
||||||
BOOL started = [m_currentTunnel.connection startVPNTunnelWithOptions:nil andReturnError:&startError];
|
if (!started || startError) {
|
||||||
|
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
||||||
if (!started || startError) {
|
<< " : Connect " << protocolName << " Tunnel Start Error"
|
||||||
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Connect " << protocolName << " Tunnel Start Error"
|
<< (startError ? startError.localizedDescription.UTF8String : "");
|
||||||
<< (startError ? startError.localizedDescription.UTF8String : "");
|
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
||||||
emit connectionStateChanged(Vpn::ConnectionState::Error);
|
} else {
|
||||||
} else {
|
qDebug().nospace() << "IosController::startTunnel :" << tunnel.localizedDescription << protocolName
|
||||||
qDebug().nospace() << "IosController::startTunnel :" << m_currentTunnel.localizedDescription << protocolName << " : Starting the tunnel succeeded";
|
<< " : Starting the tunnel succeeded";
|
||||||
}
|
}
|
||||||
}];
|
});
|
||||||
});
|
}];
|
||||||
}];
|
});
|
||||||
|
}];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool IosController::isOurManager(NETunnelProviderManager* manager) {
|
bool IosController::isOurManager(NETunnelProviderManager* manager) {
|
||||||
|
|||||||
@@ -444,8 +444,7 @@ bool ApiConfigsController::importService()
|
|||||||
|
|
||||||
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
|
if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaPremium) {
|
||||||
if (isIosOrMacOsNe) {
|
if (isIosOrMacOsNe) {
|
||||||
importSerivceFromAppStore();
|
return importSerivceFromAppStore();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
importServiceFromGateway();
|
importServiceFromGateway();
|
||||||
@@ -505,13 +504,18 @@ bool ApiConfigsController::importSerivceFromAppStore()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorCode = importServiceFromBilling(responseBody, isTestPurchase);
|
int duplicateServerIndex = -1;
|
||||||
|
errorCode = importServiceFromBilling(responseBody, isTestPurchase, duplicateServerIndex);
|
||||||
|
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||||
|
emit installServerFromApiFinished(tr("This subscription is already in the app."), duplicateServerIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (errorCode != ErrorCode::NoError) {
|
if (errorCode != ErrorCode::NoError) {
|
||||||
emit errorOccurred(errorCode);
|
emit errorOccurred(errorCode);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
emit installServerFromApiFinished(
|
||||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
tr("%1 was added to the app.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||||
#endif
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -567,15 +571,23 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (restoredTransactions.isEmpty()) {
|
if (restoredTransactions.isEmpty()) {
|
||||||
qInfo().noquote() << "[IAP] Restore completed, but no transactions were returned";
|
qInfo().noquote() << "[IAP] Restore completed, but no active entitlements found";
|
||||||
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
emit errorOccurred(ErrorCode::ApiNoPurchasedSubscriptionsError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool isTestPurchase = IosController::Instance()->isTestFlight();
|
||||||
|
const QString serviceType = m_apiServicesModel->getSelectedServiceType();
|
||||||
|
const QString serviceProtocol = m_apiServicesModel->getSelectedServiceProtocol();
|
||||||
|
const QString countryCode = m_apiServicesModel->getCountryCode();
|
||||||
|
const QString appLanguage = m_settings->getAppLanguage().name().split("_").first();
|
||||||
|
const QString installationUuid = m_settings->getInstallationUuid(true);
|
||||||
|
|
||||||
bool hasInstalledConfig = false;
|
bool hasInstalledConfig = false;
|
||||||
bool duplicateConfigAlreadyPresent = false;
|
bool duplicateConfigAlreadyPresent = false;
|
||||||
int duplicateCount = 0;
|
int duplicateServerIndex = -1;
|
||||||
QSet<QString> processedTransactions;
|
QSet<QString> processedOriginalTransactionIds;
|
||||||
|
|
||||||
for (const QVariantMap &transaction : restoredTransactions) {
|
for (const QVariantMap &transaction : restoredTransactions) {
|
||||||
const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString();
|
const QString originalTransactionId = transaction.value(QStringLiteral("originalTransactionId")).toString();
|
||||||
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
|
const QString transactionId = transaction.value(QStringLiteral("transactionId")).toString();
|
||||||
@@ -586,28 +598,28 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processedTransactions.contains(originalTransactionId)) {
|
if (processedOriginalTransactionIds.contains(originalTransactionId)) {
|
||||||
duplicateCount++;
|
qInfo().noquote() << "[IAP] Skipping duplicate restored transaction" << originalTransactionId;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
processedTransactions.insert(originalTransactionId);
|
processedOriginalTransactionIds.insert(originalTransactionId);
|
||||||
|
|
||||||
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
|
qInfo().noquote() << "[IAP] Restoring subscription. transactionId =" << transactionId
|
||||||
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId;
|
<< "originalTransactionId =" << originalTransactionId << "productId =" << productId;
|
||||||
|
|
||||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||||
QString(APP_VERSION),
|
QString(APP_VERSION),
|
||||||
m_settings->getAppLanguage().name().split("_").first(),
|
appLanguage,
|
||||||
m_settings->getInstallationUuid(true),
|
installationUuid,
|
||||||
m_apiServicesModel->getCountryCode(),
|
countryCode,
|
||||||
"",
|
"",
|
||||||
m_apiServicesModel->getSelectedServiceType(),
|
serviceType,
|
||||||
m_apiServicesModel->getSelectedServiceProtocol(),
|
serviceProtocol,
|
||||||
QJsonObject() };
|
QJsonObject() };
|
||||||
|
|
||||||
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||||
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
|
apiPayload[apiDefs::key::transactionId] = originalTransactionId;
|
||||||
auto isTestPurchase = IosController::Instance()->isTestFlight();
|
|
||||||
QByteArray responseBody;
|
QByteArray responseBody;
|
||||||
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
ErrorCode errorCode = executeRequest(QString("%1v1/subscriptions"), apiPayload, responseBody, isTestPurchase);
|
||||||
if (errorCode != ErrorCode::NoError) {
|
if (errorCode != ErrorCode::NoError) {
|
||||||
@@ -616,29 +628,37 @@ bool ApiConfigsController::restoreSerivceFromAppStore()
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorCode installError = importServiceFromBilling(responseBody, isTestPurchase);
|
int currentDuplicateServerIndex = -1;
|
||||||
|
errorCode = importServiceFromBilling(responseBody, isTestPurchase, currentDuplicateServerIndex);
|
||||||
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
if (errorCode == ErrorCode::ApiConfigAlreadyAdded) {
|
||||||
duplicateConfigAlreadyPresent = true;
|
duplicateConfigAlreadyPresent = true;
|
||||||
qInfo().noquote() << "[IAP] Skipping restored transaction" << originalTransactionId
|
if (duplicateServerIndex < 0) {
|
||||||
<< "because subscription config with the same vpn_key already exists";
|
duplicateServerIndex = currentDuplicateServerIndex;
|
||||||
} else if (errorCode != ErrorCode::NoError) {
|
}
|
||||||
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId;
|
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists" << originalTransactionId;
|
||||||
} else {
|
continue;
|
||||||
hasInstalledConfig = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errorCode != ErrorCode::NoError) {
|
||||||
|
qWarning().noquote() << "[IAP] Failed to process restored subscription response for transaction" << originalTransactionId
|
||||||
|
<< "errorCode =" << static_cast<int>(errorCode);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasInstalledConfig = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasInstalledConfig) {
|
if (!hasInstalledConfig) {
|
||||||
const ErrorCode restoreError = duplicateConfigAlreadyPresent ? ErrorCode::ApiConfigAlreadyAdded : ErrorCode::ApiPurchaseError;
|
if (duplicateConfigAlreadyPresent) {
|
||||||
emit errorOccurred(restoreError);
|
emit installServerFromApiFinished(tr("This subscription is already in the app."), duplicateServerIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit errorOccurred(ErrorCode::ApiPurchaseError);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit installServerFromApiFinished(tr("Subscription restored successfully."));
|
emit installServerFromApiFinished(tr("Subscription restored successfully."));
|
||||||
if (duplicateCount > 0) {
|
|
||||||
qInfo().noquote() << "[IAP] Skipped" << duplicateCount
|
|
||||||
<< "duplicate restored transactions for original transaction IDs already processed";
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -942,9 +962,11 @@ QString ApiConfigsController::getVpnKey()
|
|||||||
return m_vpnKey;
|
return m_vpnKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase)
|
ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase,
|
||||||
|
int &duplicateServerIndex)
|
||||||
{
|
{
|
||||||
#ifdef Q_OS_IOS
|
#if defined(Q_OS_IOS) || defined(MACOS_NE)
|
||||||
|
duplicateServerIndex = -1;
|
||||||
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
QJsonObject responseObject = QJsonDocument::fromJson(responseBody).object();
|
||||||
QString key = responseObject.value(QStringLiteral("key")).toString();
|
QString key = responseObject.value(QStringLiteral("key")).toString();
|
||||||
if (key.isEmpty()) {
|
if (key.isEmpty()) {
|
||||||
@@ -952,7 +974,8 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
|
|||||||
return ErrorCode::ApiPurchaseError;
|
return ErrorCode::ApiPurchaseError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_serversModel->hasServerWithVpnKey(key)) {
|
duplicateServerIndex = m_serversModel->indexOfServerWithVpnKey(key);
|
||||||
|
if (duplicateServerIndex >= 0) {
|
||||||
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
qInfo().noquote() << "[IAP] Subscription config with the same vpn_key already exists";
|
||||||
return ErrorCode::ApiConfigAlreadyAdded;
|
return ErrorCode::ApiConfigAlreadyAdded;
|
||||||
}
|
}
|
||||||
@@ -986,6 +1009,7 @@ ErrorCode ApiConfigsController::importServiceFromBilling(const QByteArray &respo
|
|||||||
#else
|
#else
|
||||||
Q_UNUSED(responseBody)
|
Q_UNUSED(responseBody)
|
||||||
Q_UNUSED(isTestPurchase)
|
Q_UNUSED(isTestPurchase)
|
||||||
|
duplicateServerIndex = -1;
|
||||||
return ErrorCode::NoError;
|
return ErrorCode::NoError;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ public slots:
|
|||||||
signals:
|
signals:
|
||||||
void errorOccurred(ErrorCode errorCode);
|
void errorOccurred(ErrorCode errorCode);
|
||||||
|
|
||||||
void installServerFromApiFinished(const QString &message);
|
void installServerFromApiFinished(const QString &message, int preferredDefaultServerIndex = -1);
|
||||||
void changeApiCountryFinished(const QString &message);
|
void changeApiCountryFinished(const QString &message);
|
||||||
void reloadServerFromApiFinished(const QString &message);
|
void reloadServerFromApiFinished(const QString &message);
|
||||||
void updateServerFromApiFinished();
|
void updateServerFromApiFinished();
|
||||||
@@ -57,7 +57,7 @@ private:
|
|||||||
QString getVpnKey();
|
QString getVpnKey();
|
||||||
|
|
||||||
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
ErrorCode executeRequest(const QString &endpoint, const QJsonObject &apiPayload, QByteArray &responseBody, bool isTestPurchase = false);
|
||||||
ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase);
|
ErrorCode importServiceFromBilling(const QByteArray &responseBody, const bool isTestPurchase, int &duplicateServerIndex);
|
||||||
|
|
||||||
QList<QString> m_qrCodes;
|
QList<QString> m_qrCodes;
|
||||||
QString m_vpnKey;
|
QString m_vpnKey;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "amnezia_application.h"
|
||||||
#include "utilities.h"
|
#include "utilities.h"
|
||||||
#include "core/controllers/vpnConfigurationController.h"
|
#include "core/controllers/vpnConfigurationController.h"
|
||||||
#include "version.h"
|
#include "version.h"
|
||||||
@@ -81,6 +82,8 @@ void ConnectionController::onConnectionStateChanged(Vpn::ConnectionState state)
|
|||||||
m_connectionStateText = tr("Connecting...");
|
m_connectionStateText = tr("Connecting...");
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case Vpn::ConnectionState::Connected: {
|
case Vpn::ConnectionState::Connected: {
|
||||||
|
amnApp->networkManager()->clearConnectionCache();
|
||||||
|
|
||||||
m_isConnectionInProgress = false;
|
m_isConnectionInProgress = false;
|
||||||
m_isConnected = true;
|
m_isConnected = true;
|
||||||
m_connectionStateText = tr("Connected");
|
m_connectionStateText = tr("Connected");
|
||||||
|
|||||||
@@ -727,21 +727,21 @@ bool ServersModel::isServerFromApiAlreadyExists(const QString &userCountryCode,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ServersModel::hasServerWithVpnKey(const QString &vpnKey) const
|
int ServersModel::indexOfServerWithVpnKey(const QString &vpnKey) const
|
||||||
{
|
{
|
||||||
const QString normalizedInput = normalizeVpnKey(vpnKey);
|
const QString normalizedInput = normalizeVpnKey(vpnKey);
|
||||||
if (normalizedInput.isEmpty()) {
|
if (normalizedInput.isEmpty()) {
|
||||||
return false;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &server : std::as_const(m_servers)) {
|
for (int i = 0; i < m_servers.size(); ++i) {
|
||||||
const auto apiConfig = server.toObject().value(configKey::apiConfig).toObject();
|
const auto apiConfig = m_servers.at(i).toObject().value(configKey::apiConfig).toObject();
|
||||||
const QString existingKey = normalizeVpnKey(apiConfig.value(apiDefs::key::vpnKey).toString());
|
const QString existingKey = normalizeVpnKey(apiConfig.value(apiDefs::key::vpnKey).toString());
|
||||||
if (!existingKey.isEmpty() && existingKey == normalizedInput) {
|
if (!existingKey.isEmpty() && existingKey == normalizedInput) {
|
||||||
return true;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ServersModel::serverHasInstalledContainers(const int serverIndex) const
|
bool ServersModel::serverHasInstalledContainers(const int serverIndex) const
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ public slots:
|
|||||||
|
|
||||||
bool isServerFromApiAlreadyExists(const quint16 crc);
|
bool isServerFromApiAlreadyExists(const quint16 crc);
|
||||||
bool isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol);
|
bool isServerFromApiAlreadyExists(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol);
|
||||||
bool hasServerWithVpnKey(const QString &vpnKey) const;
|
int indexOfServerWithVpnKey(const QString &vpnKey) const;
|
||||||
|
|
||||||
QVariant getDefaultServerData(const QString roleString);
|
QVariant getDefaultServerData(const QString roleString);
|
||||||
|
|
||||||
|
|||||||
@@ -225,9 +225,13 @@ PageType {
|
|||||||
Connections {
|
Connections {
|
||||||
target: ApiConfigsController
|
target: ApiConfigsController
|
||||||
|
|
||||||
function onInstallServerFromApiFinished(message) {
|
function onInstallServerFromApiFinished(message, preferredDefaultIndex) {
|
||||||
if (!ConnectionController.isConnected) {
|
if (!ConnectionController.isConnected) {
|
||||||
ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1);
|
if (preferredDefaultIndex !== undefined && preferredDefaultIndex >= 0) {
|
||||||
|
ServersModel.setDefaultServerIndex(preferredDefaultIndex)
|
||||||
|
} else {
|
||||||
|
ServersModel.setDefaultServerIndex(ServersModel.getServersCount() - 1)
|
||||||
|
}
|
||||||
ServersModel.processedIndex = ServersModel.defaultIndex
|
ServersModel.processedIndex = ServersModel.defaultIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,14 @@ Window {
|
|||||||
function onStateChanged() {
|
function onStateChanged() {
|
||||||
if (Qt.platform.os === "android") {
|
if (Qt.platform.os === "android") {
|
||||||
if (Qt.application.state === Qt.ApplicationActive) {
|
if (Qt.application.state === Qt.ApplicationActive) {
|
||||||
|
root.visible = true
|
||||||
refreshTimer.restart()
|
refreshTimer.restart()
|
||||||
} else if (Qt.application.state === Qt.ApplicationSuspended ||
|
} else if (Qt.application.state === Qt.ApplicationSuspended) {
|
||||||
Qt.application.state === Qt.ApplicationInactive) {
|
// Hide window to stop the Qt render loop and prevent
|
||||||
console.log("QML: Application going to background, state:", Qt.application.state)
|
// eglSwapBuffers from being called on a lost EGL context.
|
||||||
|
// NOTE: Do NOT hide on ApplicationInactive — that fires on any
|
||||||
|
// focus change (IME, notifications) and would blank the screen.
|
||||||
|
root.visible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user