Compare commits

...

16 Commits

Author SHA1 Message Date
lunardunno
e8d365f69a adding a processed phrase to the server controller 2026-03-30 18:55:01 +04:00
lunardunno
7ae42606f2 Extended error description about Docker's regional block 2026-03-30 18:48:07 +04:00
lunardunno
b3c86655f5 Handling a Docker Regional Block Message 2026-03-30 18:32:14 +04:00
NickVs2015
bf3d11e5c4 feat: renewal new status logic (#2409)
* fix: renewal add status logic

* fix: wakeup activity resumed android
2026-03-25 19:48:32 +08:00
NickVs2015
9a0222aee3 fix: ui fixes for renewal subscription (#2406) 2026-03-25 12:34:42 +08:00
NickVs2015
f0f0f7c5be feat: add subscription renewal (#2389)
* feat: add renewal subsribe

* fix: after review
2026-03-24 22:45:02 +08:00
NickVs2015
36b1a863bf fix: black screen resume / pause (#2400) 2026-03-24 22:13:31 +08:00
yyy-amnezia
4103c5bbcf refactor: extract and simplify OpenVPN reachability and network change handling logic (#2402) 2026-03-24 22:12:59 +08:00
vkamn
fa69da6d56 chore: send app version in services request (#2403) 2026-03-24 20:25:04 +08:00
yyy-amnezia
aaf2c9ddeb feat: add Xray split tunnel support for iOS PacketTunnelProvider (#2332) 2026-03-24 16:07:36 +08:00
Mitternacht822
dbbc7119ec feat: add warning info for ssh keys (#2252)
* fix: fixed da typo

* feat: added warning about available ssh keys info
2026-03-24 16:06:40 +08:00
vkamn
c57162c4cc feat: add base amnezia trial support (#2366)
* feat: add base amnezia trial support

* feat: add external-trial
2026-03-24 10:29:51 +08:00
NickVs2015
40e39895c9 fix openfile deadlock (#2373) 2026-03-21 11:46:46 +08:00
vkamn
ec3ab2a03c chore: update licnese file (#2376) 2026-03-20 21:04:13 +08:00
yyy-amnezia
ddecfcad26 fix: apple platform network switch fix (#2359)
* Apple platform network switch fix

* macos_ne exclusion fixed
2026-03-20 20:51:36 +08:00
NickVs2015
67bd880cdf fix: swap buffers error (#2347) 2026-03-16 13:03:20 +08:00
41 changed files with 1130 additions and 263 deletions

View File

@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2117)
set(APP_ANDROID_VERSION_CODE 2118)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -179,7 +179,7 @@ You may face compiling issues in QT Creator after you've worked in Android Studi
## License
GPL v3.0
This project is licensed under the GNU General Public License v3.0 (see LICENSE) and also includes third-party components distributed under their own terms (see THIRD_PARTY_LICENSES.md).
## Donate

149
THIRD_PARTY_LICENSES.md Normal file
View File

@@ -0,0 +1,149 @@
# Third-Party Licenses
This project is licensed under the GNU General Public License v3.0.
This file lists third-party software components used by this repository.
Each component is distributed under its own license as linked below.
---
## QtKeychain
- Source: https://github.com/frankosterfeld/qtkeychain
- License: BSD License
- License Text: https://www.gnu.org/licenses/license-list.html#ModifiedBSD
---
## QSimpleCrypto
- Source: https://github.com/n1flh31mur/QSimpleCrypto
- License: Apache License 2.0
- License Text: https://github.com/n1flh31mur/QSimpleCrypto/blob/master/LICENSE
---
## SortFilterProxyModel
- Source: https://github.com/oKcerG/SortFilterProxyModel
- License: MIT License
- License Text: https://github.com/oKcerG/SortFilterProxyModel/blob/master/LICENSE
---
## QJsonStruct
- Source: https://github.com/Qv2ray/QJsonStruct
- License: MIT License
- License Text: https://github.com/Qv2ray/QJsonStruct/blob/master/LICENSE
---
## QR Code Generator (qrcodegen)
- Source: https://github.com/nayuki/QR-Code-generator
- License: MIT License
- License Text: https://www.nayuki.io/page/qr-code-generator-library
---
## Qt Gamepad
- Source: https://github.com/qt/qtgamepad
- License: GNU General Public License v3.0 (GPL-3.0)
- License Text: https://www.gnu.org/licenses/gpl-3.0.en.html
---
## AmneziaWG Apple (WireGuard)
- Source: https://github.com/amnezia-vpn/amneziawg-apple
- License: MIT License
- License Text: https://github.com/amnezia-vpn/amneziawg-apple/blob/master/COPYING
---
## AmneziaWG Android
- Source: https://github.com/amnezia-vpn/amneziawg-go
- License: MIT License
- License Text: https://github.com/amnezia-vpn/amneziawg-go/blob/master/LICENSE
---
## Xray Core
- Source: https://github.com/XTLS/Xray-core
- License: Mozilla Public License 2.0 (MPL-2.0)
- License Text: https://github.com/XTLS/Xray-core/blob/main/LICENSE
---
## Cloak
- Source: https://github.com/cbeuw/Cloak
- License: GNU General Public License v3.0 (GPL-3.0)
- License Text: https://github.com/cbeuw/Cloak/blob/master/LICENSE
---
## Shadowsocks
- Source: https://github.com/shadowsocks/shadowsocks-libev
- License: GPL-3.0-or-later
- License Text: http://www.gnu.org/licenses/
---
## OpenSSL
- Source: https://github.com/openssl/openssl
- License: Apache License 2.0
- License Text: https://www.openssl.org/source/license.html
---
## libssh
- Source: https://www.libssh.org/
- License: GNU Lesser General Public License (LGPL)
- License Text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
---
## OpenVPNAdapter
- Source: https://github.com/ss-abramchuk/OpenVPNAdapter
- License: GNU Affero General Public License v3.0 (AGPL-3.0)
- License Text: https://github.com/ss-abramchuk/OpenVPNAdapter/blob/master/LICENSE
---
## Wintun
- Source: https://www.wintun.net/
- License: Prebuilt Binaries License
- License Text: https://github.com/WireGuard/wintun/blob/master/prebuilt-binaries-license.txt
---
## Mullvad Split Tunnel Driver
- Source: https://github.com/mullvad/win-split-tunnel
- License: GNU General Public License v3.0 (GPL-3.0) and Mozilla Public License Version 2.0
- License Text: https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-GPL.md https://github.com/mullvad/win-split-tunnel/blob/master/LICENSE-MPL.txt
---
## tun2socks
- Source: https://github.com/eycorsican/go-tun2socks
- License: MIT License
- License Text: https://github.com/eycorsican/go-tun2socks/blob/master/LICENSE
---
## TAP-Windows Driver
- Source: https://github.com/OpenVPN/tap-windows6
- License: tap-windows6 license
- License Text: https://github.com/OpenVPN/tap-windows6/blob/master/COPYING

View File

@@ -109,6 +109,16 @@ void AmneziaApplication::init()
// install filter on main window
if (auto win = qobject_cast<QQuickWindow*>(obj)) {
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();
}
},

View File

@@ -296,9 +296,25 @@ class AmneziaActivity : QtActivity() {
hasWindowFocus = hasFocus
Log.d(TAG, "Window focus changed: hasFocus=$hasFocus")
// Cancel pending operations if window loses focus
if (!hasFocus) {
// Cancel pending operations if window loses focus
resumeHandler.removeCallbacksAndMessages(null)
} else if (isActivityResumed && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.decorView.apply {
invalidate()
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(1f, 1f)
}
}, 50)
resumeHandler.postDelayed({
if (isActivityResumed && hasWindowFocus && !isFinishing && !isDestroyed) {
sendTouch(2f, 2f)
requestLayout()
invalidate()
}
}, 150)
}
}
}
@@ -337,6 +353,13 @@ class AmneziaActivity : QtActivity() {
private external fun nativeGamepadKeyEvent(deviceId: Int, keyCode: Int, pressed: Boolean)
override fun onPause() {
// Notify Qt to stop rendering BEFORE super.onPause() destroys the EGL surface.
// Using a coroutine here would be too late — the surface is gone by the time
// the coroutine runs. A direct synchronous call gives Qt's render thread the
// best chance to process visible=false before surface destruction.
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityPaused()
}
super.onPause()
isActivityResumed = false
// Cancel all pending operations when activity pauses
@@ -349,6 +372,9 @@ class AmneziaActivity : QtActivity() {
super.onResume()
isActivityResumed = true
Log.d(TAG, "Resume Amnezia activity")
if (qtInitialized.isCompleted) {
QtAndroidController.onActivityResumed()
}
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
val uri = pendingOpenFileUri!!
@@ -816,7 +842,7 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun getFd(fileName: String): Int {
Log.v(TAG, "Get fd for $fileName")
return blockingCall {
return blockingCall(Dispatchers.IO) {
try {
pfd = contentResolver.openFileDescriptor(Uri.parse(fileName), "r")
pfd?.fd ?: -1

View File

@@ -31,4 +31,7 @@ object QtAndroidController {
external fun onImeInsetsChanged(heightDp: Int)
external fun onSystemBarsInsetsChanged(navBarHeightDp: Int, statusBarHeightDp: Int)
external fun onActivityPaused()
external fun onActivityResumed()
}

View File

@@ -10,8 +10,10 @@ namespace apiDefs
AmneziaFreeV3,
AmneziaPremiumV1,
AmneziaPremiumV2,
AmneziaTrialV2,
SelfHosted,
ExternalPremium
ExternalPremium,
ExternalTrial
};
enum ConfigSource {
@@ -32,6 +34,7 @@ namespace apiDefs
constexpr QLatin1String stackType("stack_type");
constexpr QLatin1String serviceType("service_type");
constexpr QLatin1String cliVersion("cli_version");
constexpr QLatin1String cliName("cli_name");
constexpr QLatin1String supportedProtocols("supported_protocols");
constexpr QLatin1String vpnKey("vpn_key");

View File

@@ -58,18 +58,24 @@ apiDefs::ConfigType apiUtils::getConfigType(const QJsonObject &serverConfigObjec
};
case apiDefs::ConfigSource::AmneziaGateway: {
constexpr QLatin1String servicePremium("amnezia-premium");
constexpr QLatin1String serviceTrial("amnezia-trial");
constexpr QLatin1String serviceFree("amnezia-free");
constexpr QLatin1String serviceExternalPremium("external-premium");
constexpr QLatin1String serviceExternalTrial("external-trial");
auto apiConfigObject = serverConfigObject.value(apiDefs::key::apiConfig).toObject();
auto serviceType = apiConfigObject.value(apiDefs::key::serviceType).toString();
if (serviceType == servicePremium) {
return apiDefs::ConfigType::AmneziaPremiumV2;
} else if (serviceType == serviceTrial) {
return apiDefs::ConfigType::AmneziaTrialV2;
} else if (serviceType == serviceFree) {
return apiDefs::ConfigType::AmneziaFreeV3;
} else if (serviceType == serviceExternalPremium) {
return apiDefs::ConfigType::ExternalPremium;
} else if (serviceType == serviceExternalTrial) {
return apiDefs::ConfigType::ExternalTrial;
}
}
default: {
@@ -90,6 +96,7 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
const int httpStatusCodeConflict = 409;
const int httpStatusCodeNotFound = 404;
const int httpStatusCodeNotImplemented = 501;
const int httpStatusCodeUnprocessableEntity = 422;
if (!sslErrors.empty()) {
qDebug().noquote() << sslErrors;
@@ -122,6 +129,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
return amnezia::ErrorCode::ApiNotFoundError;
} else if (httpStatusFromBody == httpStatusCodeNotImplemented) {
return amnezia::ErrorCode::ApiUpdateRequestError;
} else if (httpStatusFromBody == httpStatusCodeUnprocessableEntity) {
return amnezia::ErrorCode::ApiSubscriptionExpiredError;
}
return amnezia::ErrorCode::ApiConfigDownloadError;
}
@@ -133,7 +142,8 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
bool apiUtils::isPremiumServer(const QJsonObject &serverConfigObject)
{
static const QSet<apiDefs::ConfigType> premiumTypes = { apiDefs::ConfigType::AmneziaPremiumV1, apiDefs::ConfigType::AmneziaPremiumV2,
apiDefs::ConfigType::ExternalPremium };
apiDefs::ConfigType::AmneziaTrialV2, apiDefs::ConfigType::ExternalPremium,
apiDefs::ConfigType::ExternalTrial };
return premiumTypes.contains(getConfigType(serverConfigObject));
}
@@ -177,7 +187,9 @@ QString apiUtils::getPremiumV1VpnKey(const QJsonObject &serverConfigObject)
QString apiUtils::getPremiumV2VpnKey(const QJsonObject &serverConfigObject)
{
if (apiUtils::getConfigType(serverConfigObject) != apiDefs::ConfigType::AmneziaPremiumV2) {
auto configType = apiUtils::getConfigType(serverConfigObject);
if (configType != apiDefs::ConfigType::AmneziaPremiumV2 && configType != apiDefs::ConfigType::AmneziaTrialV2
&& configType != apiDefs::ConfigType::ExternalPremium && configType != apiDefs::ConfigType::ExternalTrial) {
return {};
}

View File

@@ -153,6 +153,8 @@ void CoreController::initControllers()
m_apiConfigsController.reset(new ApiConfigsController(m_serversModel, m_apiServicesModel, m_settings));
m_engine->rootContext()->setContextProperty("ApiConfigsController", m_apiConfigsController.get());
connect(m_apiConfigsController.get(), &ApiConfigsController::subscriptionRefreshNeeded,
this, [this]() { m_apiSettingsController->getAccountInfo(false); });
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());

View File

@@ -46,6 +46,7 @@ namespace
constexpr int httpStatusCodeConflict = 409;
constexpr int httpStatusCodeNotImplemented = 501;
constexpr int httpStatusCodeUnprocessableEntity = 422;
}
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
@@ -451,6 +452,8 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
}
} else if (httpStatus == httpStatusCodeConflict) {
return false;
} else if (httpStatus == httpStatusCodeUnprocessableEntity) {
return false;
} else if (replyError != QNetworkReply::NetworkError::NoError) {
qDebug() << replyError;
return true;

View File

@@ -479,6 +479,8 @@ ErrorCode ServerController::buildContainerWorker(const ServerCredentials &creden
return ErrorCode::ServerCgroupMountpoint;
if (stdOut.contains("have reached") && stdOut.contains("pull rate limit"))
return ErrorCode::DockerPullRateLimit;
if (stdOut.contains("we now block all IP addresses"))
return ErrorCode::ServerIPaddressBlockedByDocker;
return error;
}

View File

@@ -62,6 +62,7 @@ namespace amnezia
ServerCgroupMountpoint = 212,
DockerPullRateLimit = 213,
ServerLinuxKernelTooOld = 214,
ServerIPaddressBlockedByDocker = 215,
// Ssh connection errors
SshRequestDeniedError = 300,

View File

@@ -30,6 +30,7 @@ QString errorString(ErrorCode code) {
case(ErrorCode::ServerCgroupMountpoint): errorMessage = QObject::tr("Server error: cgroup mountpoint does not exist"); break;
case(ErrorCode::DockerPullRateLimit): errorMessage = QObject::tr("Docker error: The pull rate limit has been reached"); break;
case(ErrorCode::ServerLinuxKernelTooOld): errorMessage = QObject::tr("Server error: Linux kernel is too old"); break;
case(ErrorCode::ServerIPaddressBlockedByDocker): errorMessage = QObject::tr("Docker error: Server IP address blocked by Docker"); break;
// Libssh errors
case(ErrorCode::SshRequestDeniedError): errorMessage = QObject::tr("SSH request was denied"); break;

View File

@@ -101,7 +101,9 @@ bool AndroidController::initialize()
{"onAuthResult", "(Z)V", reinterpret_cast<void *>(onAuthResult)},
{"decodeQrCode", "(Ljava/lang/String;)Z", reinterpret_cast<bool *>(decodeQrCode)},
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)}
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
};
QJniEnvironment env;
@@ -558,3 +560,22 @@ void AndroidController::onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jin
emit AndroidController::instance()->systemBarsInsetsChanged(navBarHeightDp, statusBarHeightDp);
}
// static
void AndroidController::onActivityPaused(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->activityPaused();
}
// static
void AndroidController::onActivityResumed(JNIEnv *env, jobject thiz)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->activityResumed();
}

View File

@@ -75,6 +75,8 @@ signals:
void authenticationResult(bool result);
void imeInsetsChanged(int heightDp);
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
void activityPaused();
void activityResumed();
private:
bool isWaitingStatus = true;
@@ -105,6 +107,8 @@ private:
static bool decodeQrCode(JNIEnv *env, jobject thiz, jstring data);
static void onImeInsetsChanged(JNIEnv *env, jobject thiz, jint heightDp);
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
static void onActivityPaused(JNIEnv *env, jobject thiz);
static void onActivityResumed(JNIEnv *env, jobject thiz);
template <typename Ret, typename ...Args>
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);

View File

@@ -126,8 +126,7 @@ extension PacketTunnelProvider {
}
vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return }
self?.ovpnAdapter?.reconnect(afterTimeInterval: 5)
self?.handleOpenVPNReachabilityChange(status)
}
startHandler = completionHandler

View File

@@ -21,6 +21,44 @@ extension Constants {
}
extension PacketTunnelProvider {
private func applyXraySplitTunnel(_ xrayConfig: XrayConfig,
settings: NEPacketTunnelNetworkSettings) {
guard let splitTunnelType = xrayConfig.splitTunnelType else {
return
}
guard let splitTunnelSites = xrayConfig.splitTunnelSites else {
xrayLog(.error, message: "Split tunnel sites are not set")
return
}
if splitTunnelType == 1 {
var ipv4IncludedRoutes = [NEIPv4Route]()
for allowedIPString in splitTunnelSites {
if let allowedIP = IPAddressRange(from: allowedIPString) {
ipv4IncludedRoutes.append(NEIPv4Route(
destinationAddress: "\(allowedIP.address)",
subnetMask: "\(allowedIP.subnetMask())"))
}
}
settings.ipv4Settings?.includedRoutes = ipv4IncludedRoutes
} else if splitTunnelType == 2 {
var ipv4ExcludedRoutes = [NEIPv4Route]()
for excludedIPString in splitTunnelSites {
if let excludedIP = IPAddressRange(from: excludedIPString) {
ipv4ExcludedRoutes.append(NEIPv4Route(
destinationAddress: "\(excludedIP.address)",
subnetMask: "\(excludedIP.subnetMask())"))
}
}
settings.ipv4Settings?.excludedRoutes = ipv4ExcludedRoutes
}
}
func startXray(completionHandler: @escaping (Error?) -> Void) {
// Xray configuration
@@ -72,6 +110,7 @@ extension PacketTunnelProvider {
settings.dnsSettings = !dnsArray.isEmpty
? NEDNSSettings(servers: dnsArray)
: NEDNSSettings(servers: ["1.1.1.1"])
applyXraySplitTunnel(xrayConfig, settings: settings)
let xrayConfigData = xrayConfig.config.data(using: .utf8)

View File

@@ -41,10 +41,15 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
var ovpnAdapter: OpenVPNAdapter?
private lazy var openVPNPacketFlowAdapter = PacketTunnelFlowAdapter(flow: packetFlow)
private let pathMonitorQueue = DispatchQueue(label: Constants.processQueueName + ".path-monitor")
private let networkChangeQueue = DispatchQueue(label: Constants.processQueueName + ".network-change")
private let pathMonitor = NWPathMonitor()
private var didReceiveInitialPathUpdate = false
private var currentPath: Network.NWPath?
private var currentPathSignature: String?
private var pendingOpenVPNReconnectWorkItem: DispatchWorkItem?
private var pendingNetworkChangeWorkItem: DispatchWorkItem?
private var isApplyingNetworkChange = false
private var lastOpenVPNReachabilityStatus: OpenVPNReachabilityStatus?
var splitTunnelType: Int?
var splitTunnelSites: [String]?
@@ -78,14 +83,22 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
guard hasMeaningfulChange, let proto = self.protoType else { return }
// WireGuard/AWG manages network changes internally; avoid restarting the tunnel here.
// WireGuard/AWG manages network changes internally in its own adapter.
if proto == .wireguard {
return
}
DispatchQueue.main.async {
self.handle(networkChange: path) { _ in }
if proto == .openvpn {
self.scheduleOpenVPNReconnect(reason: "NWPath changed")
return
}
if self.isApplyingNetworkChange || self.reasserting {
xrayLog(.debug, message: "Ignoring path change while xray restart is in progress")
return
}
self.scheduleNetworkChangeHandling(for: proto, path: path)
}
pathMonitor.start(queue: pathMonitorQueue)
@@ -197,6 +210,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return
}
cancelPendingOpenVPNReconnect()
cancelPendingNetworkChangeHandling()
didReceiveInitialPathUpdate = false
updateActiveInterfaceIndexForCurrentPath()
@@ -215,6 +230,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
cancelPendingOpenVPNReconnect()
cancelPendingNetworkChangeHandling()
guard let protoType else {
completionHandler()
return
@@ -259,9 +277,111 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
private func handle(networkChange changePath: Network.NWPath, completion: @escaping (Error?) -> Void) {
guard protoType == .xray else {
updateActiveInterfaceIndex(for: changePath)
completion(nil)
return
}
updateActiveInterfaceIndex(for: changePath)
wg_log(.info, message: "Tunnel restarted.")
startTunnel(options: nil, completionHandler: completion)
reasserting = true
xrayLog(.info, message: "Applying network change to xray tunnel")
stopXray { }
startXray { [weak self] error in
self?.reasserting = false
completion(error)
}
}
private func scheduleNetworkChangeHandling(for proto: TunnelProtoType, path: Network.NWPath) {
guard proto == .xray else { return }
pendingNetworkChangeWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.pendingNetworkChangeWorkItem = nil
if self.isApplyingNetworkChange || self.reasserting {
xrayLog(.debug, message: "Skipping network change while restart is already in progress")
return
}
self.isApplyingNetworkChange = true
DispatchQueue.main.async {
self.handle(networkChange: path) { [weak self] _ in
self?.networkChangeQueue.async {
self?.isApplyingNetworkChange = false
}
}
}
}
pendingNetworkChangeWorkItem = workItem
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
}
private func scheduleOpenVPNReconnect(reason: String) {
guard protoType == .openvpn else { return }
pendingOpenVPNReconnectWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.pendingOpenVPNReconnectWorkItem = nil
guard self.protoType == .openvpn else { return }
if self.reasserting {
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
return
}
DispatchQueue.main.async { [weak self] in
guard let self else { return }
guard !self.reasserting else {
ovpnLog(.debug, message: "Skipping OpenVPN reconnect while session is already reasserting")
return
}
ovpnLog(.info, message: "\(reason), reconnecting OpenVPN session")
self.ovpnAdapter?.reconnect(afterTimeInterval: 1)
}
}
pendingOpenVPNReconnectWorkItem = workItem
networkChangeQueue.asyncAfter(deadline: .now() + 1.0, execute: workItem)
}
func handleOpenVPNReachabilityChange(_ status: OpenVPNReachabilityStatus) {
defer { lastOpenVPNReachabilityStatus = status }
guard let previousStatus = lastOpenVPNReachabilityStatus else {
return
}
guard previousStatus != status else {
return
}
switch status {
case .reachableViaWiFi, .reachableViaWWAN:
scheduleOpenVPNReconnect(reason: "Reachability changed")
default:
break
}
}
private func cancelPendingOpenVPNReconnect() {
pendingOpenVPNReconnectWorkItem?.cancel()
pendingOpenVPNReconnectWorkItem = nil
lastOpenVPNReachabilityStatus = nil
}
private func cancelPendingNetworkChangeHandling() {
pendingNetworkChangeWorkItem?.cancel()
pendingNetworkChangeWorkItem = nil
isApplyingNetworkChange = false
}
}
@@ -271,8 +391,14 @@ private extension PacketTunnelProvider {
signatureComponents.append(path.isExpensive ? "exp" : "noexp")
signatureComponents.append(path.isConstrained ? "con" : "nocon")
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular, .loopback, .other]
let sortedInterfaces = path.availableInterfaces.sorted { lhs, rhs in
// Ignore loopback and tunnel-style `.other` interfaces so Xray does not
// react to its own utun lifecycle as if the physical uplink changed.
let preferredTypes: [NWInterface.InterfaceType] = [.wiredEthernet, .wifi, .cellular]
let externalInterfaces = path.availableInterfaces.filter { interface in
interface.type == .wiredEthernet || interface.type == .wifi || interface.type == .cellular
}
let sortedInterfaces = externalInterfaces.sorted { lhs, rhs in
if lhs.type == rhs.type {
return lhs.index < rhs.index
}
@@ -293,8 +419,8 @@ private extension PacketTunnelProvider {
case .wiredEthernet: typeName = "ethernet"
case .wifi: typeName = "wifi"
case .cellular: typeName = "cellular"
case .loopback: typeName = "loopback"
case .other: typeName = "other"
case .loopback, .other:
continue
@unknown default: typeName = "unknown"
}
signatureComponents.append("\(typeName):\(interface.index)")

View File

@@ -3,5 +3,7 @@ import Foundation
struct XrayConfig: Decodable {
let dns1: String?
let dns2: String?
let splitTunnelType: Int?
let splitTunnelSites: [String]?
let config: String
}

View File

@@ -684,6 +684,15 @@ bool IosController::setupXray()
QJsonObject finalConfig;
finalConfig.insert(config_key::dns1, m_rawConfig[config_key::dns1].toString());
finalConfig.insert(config_key::dns2, m_rawConfig[config_key::dns2].toString());
finalConfig.insert(config_key::splitTunnelType, m_rawConfig[config_key::splitTunnelType]);
QJsonArray splitTunnelSites = m_rawConfig[config_key::splitTunnelSites].toArray();
for(int index = 0; index < splitTunnelSites.count(); index++) {
splitTunnelSites[index] = splitTunnelSites[index].toString().remove(" ");
}
finalConfig.insert(config_key::splitTunnelSites, splitTunnelSites);
finalConfig.insert(config_key::config, xrayConfigStr);
QJsonDocument finalConfigDoc(finalConfig);

View File

@@ -135,6 +135,7 @@
<file>ui/qml/Components/InstalledAppsDrawer.qml</file>
<file>ui/qml/Components/QuestionDrawer.qml</file>
<file>ui/qml/Components/SelectLanguageDrawer.qml</file>
<file>ui/qml/Components/SubscriptionExpiredDrawer.qml</file>
<file>ui/qml/Components/ServersListView.qml</file>
<file>ui/qml/Components/SettingsContainersListView.qml</file>
<file>ui/qml/Components/TransportProtoSelector.qml</file>

View File

@@ -366,6 +366,8 @@ bool ApiConfigsController::fillAvailableServices()
{
QJsonObject apiPayload;
apiPayload[configKey::osVersion] = QSysInfo::productType();
apiPayload[configKey::appVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::cliName] = QString(APPLICATION_NAME);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
QByteArray responseBody;
@@ -447,7 +449,7 @@ bool ApiConfigsController::importService()
importSerivceFromAppStore();
return true;
}
} else {
} else if (m_apiServicesModel->getSelectedServiceType() == serviceType::amneziaFree) {
importServiceFromGateway();
return true;
}
@@ -721,6 +723,7 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
}
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
bool wasSubscriptionExpired = m_serversModel->data(serverIndex, ServersModel::IsSubscriptionExpiredRole).toBool();
QByteArray responseBody;
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody, isTestPurchase);
@@ -747,6 +750,11 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
newServerConfig.insert(config_key::nameOverriddenByUser, true);
}
m_serversModel->editServer(newServerConfig, serverIndex);
if (wasSubscriptionExpired) {
emit subscriptionRefreshNeeded();
}
if (reloadServiceConfig) {
emit reloadServerFromApiFinished(tr("API config reloaded"));
} else if (newCountryName.isEmpty()) {
@@ -756,7 +764,11 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
}
return true;
} else {
emit errorOccurred(errorCode);
if (errorCode == ErrorCode::ApiSubscriptionExpiredError) {
emit subscriptionExpiredOnServer();
} else {
emit errorOccurred(errorCode);
}
return false;
}
}

View File

@@ -43,6 +43,8 @@ public slots:
signals:
void errorOccurred(ErrorCode errorCode);
void subscriptionExpiredOnServer();
void subscriptionRefreshNeeded();
void installServerFromApiFinished(const QString &message);
void changeApiCountryFinished(const QString &message);

View File

@@ -1,6 +1,7 @@
#include "apiSettingsController.h"
#include <QEventLoop>
#include <QJsonDocument>
#include <QTimer>
#include "core/api/apiUtils.h"
@@ -77,6 +78,13 @@ bool ApiSettingsController::getAccountInfo(bool reload)
QJsonObject accountInfo = QJsonDocument::fromJson(responseBody).object();
m_apiAccountInfoModel->updateModel(accountInfo, serverConfig);
QString subscriptionEndDate = accountInfo.value(apiDefs::key::subscriptionEndDate).toString();
if (!subscriptionEndDate.isEmpty()) {
apiConfig.insert(apiDefs::key::subscriptionEndDate, subscriptionEndDate);
serverConfig.insert(configKey::apiConfig, apiConfig);
m_serversModel->editServer(serverConfig, processedIndex);
}
if (reload) {
updateApiCountryModel();
updateApiDevicesModel();
@@ -85,6 +93,42 @@ bool ApiSettingsController::getAccountInfo(bool reload)
return true;
}
void ApiSettingsController::getRenewalLink()
{
auto processedIndex = m_serversModel->getProcessedServerIndex();
auto serverConfig = m_serversModel->getServerConfig(processedIndex);
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
auto authData = serverConfig.value(configKey::authData).toObject();
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
apiPayload[configKey::serviceType] = apiConfig.value(configKey::serviceType).toString();
apiPayload[configKey::authData] = authData;
apiPayload[apiDefs::key::cliVersion] = QString(APP_VERSION);
apiPayload[apiDefs::key::appLanguage] = m_settings->getAppLanguage().name().split("_").first();
auto future = gatewayController->postAsync(QString("%1v1/renewal_link"), apiPayload);
future.then(this, [this, gatewayController](QPair<ErrorCode, QByteArray> result) {
auto [errorCode, responseBody] = result;
if (errorCode != ErrorCode::NoError) {
emit errorOccurred(errorCode);
return;
}
QJsonObject responseJson = QJsonDocument::fromJson(responseBody).object();
QString url = responseJson.value("renewal_url").toString();
if (!url.isEmpty()) {
emit renewalLinkReceived(url);
}
});
}
void ApiSettingsController::updateApiCountryModel()
{
m_apiCountryModel->updateModel(m_apiAccountInfoModel->getAvailableCountries(), "");

View File

@@ -21,9 +21,11 @@ public slots:
bool getAccountInfo(bool reload);
void updateApiCountryModel();
void updateApiDevicesModel();
void getRenewalLink();
signals:
void errorOccurred(ErrorCode errorCode);
void renewalLinkReceived(const QString &url);
private:
QSharedPointer<ServersModel> m_serversModel;

View File

@@ -45,6 +45,8 @@ SettingsController::SettingsController(const QSharedPointer<ServersModel> &serve
emit safeAreaBottomMarginChanged();
emit safeAreaTopMarginChanged();
});
connect(AndroidController::instance(), &AndroidController::activityPaused, this, &SettingsController::activityPaused);
connect(AndroidController::instance(), &AndroidController::activityResumed, this, &SettingsController::activityResumed);
#endif
m_isDevModeEnabled = m_settings->isDevGatewayEnv();

View File

@@ -141,6 +141,9 @@ signals:
void safeAreaTopMarginChanged();
void safeAreaBottomMarginChanged();
void activityPaused();
void activityResumed();
void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged();

View File

@@ -1,5 +1,6 @@
#include "apiAccountInfoModel.h"
#include <QDateTime>
#include <QJsonObject>
#include "core/api/apiUtils.h"
@@ -32,7 +33,7 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
}
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate) ? tr("<p><a style=\"color: #EB5757;\">Inactive</a>")
: tr("Active");
: tr("<p><a style=\"color: #28c840;\">Active</a>");
}
case EndDateRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
@@ -52,7 +53,9 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
}
case IsComponentVisibleRole: {
return m_accountInfoData.configType == apiDefs::ConfigType::AmneziaPremiumV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium;
|| m_accountInfoData.configType == apiDefs::ConfigType::AmneziaTrialV2
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalPremium
|| m_accountInfoData.configType == apiDefs::ConfigType::ExternalTrial;
}
case HasExpiredWorkerRole: {
for (int i = 0; i < m_issuedConfigsInfo.size(); i++) {
@@ -73,6 +76,18 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
}
return false;
}
case IsSubscriptionExpiredRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) return false;
if (m_accountInfoData.subscriptionEndDate.isEmpty()) return false;
return apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate);
}
case IsSubscriptionExpiringSoonRole: {
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) return false;
if (m_accountInfoData.subscriptionEndDate.isEmpty()) return false;
if (apiUtils::isSubscriptionExpired(m_accountInfoData.subscriptionEndDate)) return false;
QDateTime endDate = QDateTime::fromString(m_accountInfoData.subscriptionEndDate, Qt::ISODateWithMs);
return endDate <= QDateTime::currentDateTimeUtc().addDays(10);
}
}
return QVariant();
@@ -164,6 +179,8 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
roles[IsComponentVisibleRole] = "isComponentVisible";
roles[HasExpiredWorkerRole] = "hasExpiredWorker";
roles[IsProtocolSelectionSupportedRole] = "isProtocolSelectionSupported";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
return roles;
}

View File

@@ -19,7 +19,9 @@ public:
EndDateRole,
IsComponentVisibleRole,
HasExpiredWorkerRole,
IsProtocolSelectionSupportedRole
IsProtocolSelectionSupportedRole,
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole
};
explicit ApiAccountInfoModel(QObject *parent = nullptr);
@@ -31,7 +33,6 @@ public:
public slots:
void updateModel(const QJsonObject &accountInfoObject, const QJsonObject &serverConfig);
QVariant data(const QString &roleString);
QJsonArray getAvailableCountries();
QJsonArray getIssuedConfigsInfo();

View File

@@ -41,6 +41,7 @@ namespace
{
constexpr char amneziaFree[] = "amnezia-free";
constexpr char amneziaPremium[] = "amnezia-premium";
constexpr char amneziaTrial[] = "amnezia-trial";
}
}
@@ -69,7 +70,7 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
}
case CardDescriptionRole: {
auto speed = apiServiceData.serviceInfo.speed;
if (serviceType == serviceType::amneziaPremium) {
if (serviceType == serviceType::amneziaPremium || serviceType == serviceType::amneziaTrial) {
return apiServiceData.serviceInfo.cardDescription.arg(speed);
} else if (serviceType == serviceType::amneziaFree) {
QString description = apiServiceData.serviceInfo.cardDescription;
@@ -124,8 +125,10 @@ QVariant ApiServicesModel::data(const QModelIndex &index, int role) const
case OrderRole: {
if (serviceType == serviceType::amneziaPremium) {
return 0;
} else if (serviceType == serviceType::amneziaFree) {
} else if (serviceType == serviceType::amneziaTrial) {
return 1;
} else if (serviceType == serviceType::amneziaFree) {
return 2;
}
}
}

View File

@@ -179,6 +179,20 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
case AdEndpointRole: {
return apiConfig.value(apiDefs::key::serviceInfo).toObject().value(apiDefs::key::adEndpoint).toString();
}
case IsSubscriptionExpiredRole: {
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) return false;
QString endDate = apiConfig.value(apiDefs::key::subscriptionEndDate).toString();
if (endDate.isEmpty()) return false;
return apiUtils::isSubscriptionExpired(endDate);
}
case IsSubscriptionExpiringSoonRole: {
if (configVersion != apiDefs::ConfigSource::AmneziaGateway) return false;
QString endDate = apiConfig.value(apiDefs::key::subscriptionEndDate).toString();
if (endDate.isEmpty()) return false;
if (apiUtils::isSubscriptionExpired(endDate)) return false;
QDateTime endDateTime = QDateTime::fromString(endDate, Qt::ISODateWithMs);
return endDateTime <= QDateTime::currentDateTimeUtc().addDays(10);
}
}
return QVariant();
@@ -443,6 +457,9 @@ QHash<int, QByteArray> ServersModel::roleNames() const
roles[AdDescriptionRole] = "adDescription";
roles[AdEndpointRole] = "adEndpoint";
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
return roles;
}

View File

@@ -52,6 +52,9 @@ public:
AdDescriptionRole,
AdEndpointRole,
IsSubscriptionExpiredRole,
IsSubscriptionExpiringSoonRole,
HasAmneziaDns
};

View File

@@ -126,6 +126,18 @@ ListViewType {
}
}
CaptionTextType {
visible: isServerFromGatewayApi && (isSubscriptionExpired || isSubscriptionExpiringSoon)
Layout.fillWidth: true
Layout.leftMargin: 64
Layout.bottomMargin: 8
text: isSubscriptionExpired ? qsTr("Subscription expired. Please renew.") : qsTr("Subscription expiring soon.")
color: isSubscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot
wrapMode: Text.WordWrap
}
DividerType {
Layout.fillWidth: true
Layout.leftMargin: 0

View File

@@ -0,0 +1,93 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
DrawerType2 {
id: root
expandedStateContent: ColumnLayout {
id: content
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
onImplicitHeightChanged: {
root.expandedHeight = content.implicitHeight + 32 + SettingsController.safeAreaBottomMargin
}
Item {
Layout.fillWidth: true
Layout.topMargin: 24
Layout.rightMargin: 16
Layout.leftMargin: 16
implicitHeight: titleText.implicitHeight
Header2TextType {
id: titleText
anchors.left: parent.left
anchors.right: parent.right
text: qsTr("Amnezia Premium subscription has expired")
horizontalAlignment: Text.AlignLeft
}
}
ParagraphTextType {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.rightMargin: 16
Layout.leftMargin: 16
text: qsTr("Renew your subscription to continue using VPN")
horizontalAlignment: Text.AlignLeft
}
BasicButtonType {
Layout.fillWidth: true
Layout.topMargin: 16
Layout.rightMargin: 16
Layout.leftMargin: 16
text: qsTr("Renew")
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
clickedFunc: function() {
ApiSettingsController.getRenewalLink()
}
}
BasicButtonType {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 8
Layout.bottomMargin: 8
implicitHeight: 25
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
textColor: AmneziaStyle.color.goldenApricot
text: qsTr("Support")
clickedFunc: function() {
root.closeTriggered()
PageController.goToPage(PageEnum.PageSettingsApiSupport)
}
}
}
}

View File

@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Style 1.0
@@ -37,6 +38,7 @@ Item {
property int borderFocusedWidth: 1
property string rightImageColor: AmneziaStyle.color.paleGray
property string leftImageColor: ""
property bool descriptionOnTop: false
property bool hideDescription: true
@@ -140,6 +142,14 @@ Item {
anchors.centerIn: parent
source: leftImageSource
visible: leftImageColor === ""
}
ColorOverlay {
anchors.fill: leftImage
source: leftImage
color: leftImageColor
visible: leftImageColor !== ""
}
}

View File

@@ -18,12 +18,23 @@ PageType {
id: root
property var processedServer
property bool subscriptionExpired: false
property bool subscriptionExpiringSoon: false
function updateSubscriptionState() {
root.subscriptionExpired = ServersModel.getProcessedServerData("isSubscriptionExpired")
root.subscriptionExpiringSoon = ServersModel.getProcessedServerData("isSubscriptionExpiringSoon")
}
Component.onCompleted: {
root.updateSubscriptionState()
}
Connections {
target: ServersModel
function onProcessedServerChanged() {
root.processedServer = proxyServersModel.get(0)
root.updateSubscriptionState()
}
}
@@ -76,12 +87,11 @@ PageType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.bottomMargin: 10
Layout.bottomMargin: 4
actionButtonImage: "qrc:/images/controls/settings.svg"
headerText: root.processedServer.name
descriptionText: qsTr("Location for connection")
actionButtonFunction: function() {
PageController.showBusyIndicator(true)
@@ -94,6 +104,50 @@ PageType {
PageController.goToPage(PageEnum.PageSettingsApiServerInfo)
}
}
CaptionTextType {
visible: root.subscriptionExpired || root.subscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 4
text: root.subscriptionExpired ? qsTr("Subscription expired") : qsTr("Subscription expiring soon")
color: root.subscriptionExpired ? AmneziaStyle.color.vibrantRed : AmneziaStyle.color.goldenApricot
}
BasicButtonType {
visible: root.subscriptionExpired || root.subscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.bottomMargin: 4
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
text: qsTr("Renew subscription")
clickedFunc: function() {
ApiSettingsController.getRenewalLink()
}
}
CaptionTextType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: (root.subscriptionExpired || root.subscriptionExpiringSoon) ? 8 : 4
Layout.bottomMargin: 8
text: qsTr("Location for connection")
color: AmneziaStyle.color.mutedGray
}
}
delegate: ColumnLayout {

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import Qt5Compat.GraphicalEffects
import SortFilterProxyModel 0.2
@@ -52,6 +53,26 @@ PageType {
property var processedServer
property bool isSubscriptionExpired: false
property bool isSubscriptionExpiringSoon: false
function updateSubscriptionState() {
root.isSubscriptionExpired = ApiAccountInfoModel.data("isSubscriptionExpired")
root.isSubscriptionExpiringSoon = ApiAccountInfoModel.data("isSubscriptionExpiringSoon")
}
Component.onCompleted: {
root.updateSubscriptionState()
}
Connections {
target: ApiAccountInfoModel
function onModelReset() {
root.updateSubscriptionState()
}
}
Connections {
target: ServersModel
@@ -108,12 +129,66 @@ PageType {
actionButtonImage: "qrc:/images/controls/edit-3.svg"
headerText: root.processedServer.name
descriptionText: ApiAccountInfoModel.data("serviceDescription")
actionButtonFunction: function() {
serverNameEditDrawer.openTriggered()
}
}
Text {
visible: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 4
text: root.isSubscriptionExpired
? qsTr("Subscription expired")
: qsTr("Subscription expiring soon")
color: root.isSubscriptionExpired
? AmneziaStyle.color.vibrantRed
: AmneziaStyle.color.goldenApricot
font.pixelSize: 14
font.weight: Font.Medium
wrapMode: Text.WordWrap
}
ParagraphTextType {
visible: ApiAccountInfoModel.data("serviceDescription") !== ""
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 16
Layout.bottomMargin: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon ? 0 : 10
text: ApiAccountInfoModel.data("serviceDescription")
color: AmneziaStyle.color.mutedGray
}
BasicButtonType {
visible: root.isSubscriptionExpired || root.isSubscriptionExpiringSoon
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
Layout.bottomMargin: 8
text: qsTr("Renew subscription")
defaultColor: AmneziaStyle.color.paleGray
hoveredColor: AmneziaStyle.color.lightGray
pressedColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.midnightBlack
clickedFunc: function() {
ApiSettingsController.getRenewalLink()
}
}
}
delegate: ColumnLayout {
@@ -151,6 +226,54 @@ PageType {
readonly property bool isVisibleForAmneziaFree: ApiAccountInfoModel.data("isComponentVisible")
Item {
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
Layout.fillWidth: true
implicitHeight: renewRow.implicitHeight + 32
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: ApiSettingsController.getRenewalLink()
}
Row {
id: renewRow
anchors.centerIn: parent
spacing: 12
Item {
width: renewIcon.implicitWidth
height: renewIcon.implicitHeight
anchors.verticalCenter: parent.verticalCenter
Image {
id: renewIcon
source: "qrc:/images/controls/refresh-cw.svg"
}
ColorOverlay {
anchors.fill: renewIcon
source: renewIcon
color: AmneziaStyle.color.goldenApricot
}
}
Text {
text: qsTr("Renew subscription")
color: AmneziaStyle.color.goldenApricot
font.pixelSize: 18
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
DividerType {
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
}
SwitcherType {
id: switcher
@@ -177,10 +300,14 @@ PageType {
}
}
DividerType {
visible: footer.isVisibleForAmneziaFree
}
WarningType {
id: warning
Layout.topMargin: 32
Layout.topMargin: 24
Layout.rightMargin: 16
Layout.leftMargin: 16
Layout.fillWidth: true
@@ -204,7 +331,7 @@ PageType {
id: vpnKey
Layout.fillWidth: true
Layout.topMargin: warning.visible ? 16 : 32
Layout.topMargin: warning.visible ? 16 : 0
visible: footer.isVisibleForAmneziaFree

View File

@@ -1,226 +1,226 @@
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"
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") : (ApiServicesModel.getSelectedServiceType() === "amnezia-trial" ? qsTr("Try Trial") : 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
}
}

View File

@@ -79,11 +79,23 @@ PageType {
}
textField.onTextChanged: {
if (headerText == qsTr("Password or SSH private key")) {
if (headerText === qsTr("Password or SSH private key")) {
buttonImageSource = textField.text !== "" ? imageSource : ""
}
}
}
WarningType {
Layout.fillWidth: true
Layout.leftMargin: 16
Layout.rightMargin: 16
Layout.topMargin: 8
visible: title === qsTr("Password or SSH private key")
backGroundColor: AmneziaStyle.color.translucentWhite
iconPath: "qrc:/images/controls/alert-circle.svg"
textString: qsTr("SSH key requirements: supported ED25519 or RSA in PEM. Paste the private key including BEGIN/END lines. If your key doesnt work, generate a compatible one.")
}
}
footer: ColumnLayout {

View File

@@ -21,15 +21,27 @@ Window {
function onStateChanged() {
if (Qt.platform.os === "android") {
if (Qt.application.state === Qt.ApplicationActive) {
root.visible = true
refreshTimer.restart()
} else if (Qt.application.state === Qt.ApplicationSuspended ||
Qt.application.state === Qt.ApplicationInactive) {
console.log("QML: Application going to background, state:", Qt.application.state)
}
}
}
}
// Hide the window immediately when Android Activity.onPause() fires so that
// Qt's render loop stops before the EGL surface is disconnected. This
// prevents "QRhiGles2: Failed to make context current" and the resulting
// black screen that appears after swiping home and returning.
Connections {
target: SettingsController
function onActivityPaused() {
if (Qt.platform.os === "android") root.visible = false
}
function onActivityResumed() {
if (Qt.platform.os === "android") root.visible = true
}
}
Timer {
id: refreshTimer
interval: 150
@@ -276,6 +288,34 @@ Window {
}
}
Item {
objectName: "subscriptionExpiredDrawerItem"
anchors.fill: parent
SubscriptionExpiredDrawer {
id: subscriptionExpiredDrawer
anchors.fill: parent
}
}
Connections {
target: ApiConfigsController
function onSubscriptionExpiredOnServer() {
subscriptionExpiredDrawer.openTriggered()
}
}
Connections {
target: ApiSettingsController
function onRenewalLinkReceived(url) {
Qt.openUrlExternally(url)
}
}
Item {
objectName: "busyIndicatorItem"