mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-20 02:00:55 +07:00
feat: add captcha (#2508)
* test capcha * add test AMNEZIA_GATEWAY_PLAINTEXT_MOCK * ref * remove first QNetworkReply::NoError * fixed macros * fixed http code * add test server * fix cmake * add CAPTCHA refreshed * fixed captcha * update QML Captha * fixed crash app & up vercion & fix qml captha * ver 4.9.0.1 * remove m_gatewayCaptchaStickyBase & outEffectiveRequestBase * reset code PR * remove mock & temp var AMNEZIA_LOCAL_GATEWAY * ref code & remove AMNEZIA_LOCAL_GATEWAY * remove check httpStatusCode & error * add 408 status code * fix update captca * remove fallback на transport * chore: add loader after captcha solved * chore: remove logs from api utils * chore: minor fixes --------- Co-authored-by: vkamn <vk@amnezia.org>
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ set(CMAKE_CXX_STANDARD 17)
|
|||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
set(PROJECT AmneziaVPN)
|
set(PROJECT AmneziaVPN)
|
||||||
set(AMNEZIAVPN_VERSION 4.9.0.0)
|
set(AMNEZIAVPN_VERSION 4.9.0.1)
|
||||||
|
|
||||||
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
set(QT_CREATOR_SKIP_PACKAGE_MANAGER_SETUP ON CACHE BOOL "" FORCE)
|
||||||
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
set(CMAKE_PROJECT_TOP_LEVEL_INCLUDES
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
#include <QPromise>
|
#include <QPromise>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QSysInfo>
|
#include <QSysInfo>
|
||||||
@@ -216,7 +217,8 @@ ErrorCode SubscriptionController::executeRequest(const QString &endpoint, const
|
|||||||
}
|
}
|
||||||
|
|
||||||
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||||
const QString &serviceProtocol, const ProtocolData &protocolData)
|
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||||
|
CaptchaInfo &captchaInfo)
|
||||||
{
|
{
|
||||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||||
QString(APP_VERSION),
|
QString(APP_VERSION),
|
||||||
@@ -233,6 +235,19 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
|
|||||||
|
|
||||||
QByteArray responseBody;
|
QByteArray responseBody;
|
||||||
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||||
|
|
||||||
|
if (errorCode == ErrorCode::ApiCaptchaRequiredError) {
|
||||||
|
QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||||
|
if (jsonDoc.isObject()) {
|
||||||
|
QJsonObject jsonObj = jsonDoc.object();
|
||||||
|
captchaInfo.captchaId = jsonObj.value("captcha_id").toString();
|
||||||
|
captchaInfo.captchaImageBase64 = jsonObj.value("captcha_image").toString();
|
||||||
|
captchaInfo.hint = jsonObj.value("hint").toString();
|
||||||
|
captchaInfo.isRequired = true;
|
||||||
|
}
|
||||||
|
return errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
if (errorCode != ErrorCode::NoError) {
|
if (errorCode != ErrorCode::NoError) {
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
@@ -242,9 +257,9 @@ ErrorCode SubscriptionController::importServiceFromGateway(const QString &userCo
|
|||||||
if (errorCode != ErrorCode::NoError) {
|
if (errorCode != ErrorCode::NoError) {
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
||||||
|
|
||||||
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||||
return ErrorCode::InternalError;
|
return ErrorCode::InternalError;
|
||||||
}
|
}
|
||||||
@@ -956,3 +971,74 @@ QFuture<QPair<ErrorCode, QString>> SubscriptionController::getRenewalLink(const
|
|||||||
return promise->future();
|
return promise->future();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ErrorCode SubscriptionController::resolveImportServiceCaptcha(const QString &userCountryCode,
|
||||||
|
const QString &serviceType,
|
||||||
|
const QString &serviceProtocol,
|
||||||
|
const ProtocolData &protocolData,
|
||||||
|
const QString &captchaId,
|
||||||
|
const QString &captchaSolution,
|
||||||
|
CaptchaInfo *retryCaptchaOut)
|
||||||
|
{
|
||||||
|
GatewayRequestData gatewayRequestData{QSysInfo::productType(),
|
||||||
|
QString(APP_VERSION),
|
||||||
|
m_appSettingsRepository->getAppLanguage().name().split("_").first(),
|
||||||
|
m_appSettingsRepository->getInstallationUuid(true),
|
||||||
|
userCountryCode,
|
||||||
|
"",
|
||||||
|
serviceType,
|
||||||
|
serviceProtocol,
|
||||||
|
QJsonObject()};
|
||||||
|
|
||||||
|
QJsonObject apiPayload = gatewayRequestData.toJsonObject();
|
||||||
|
appendProtocolDataToApiPayload(serviceProtocol, protocolData, apiPayload);
|
||||||
|
|
||||||
|
apiPayload["captcha_id"] = captchaId;
|
||||||
|
QString normalizedSolution;
|
||||||
|
normalizedSolution.reserve(captchaSolution.size());
|
||||||
|
for (const QChar &ch : captchaSolution) {
|
||||||
|
const ushort u = ch.unicode();
|
||||||
|
if (u >= '0' && u <= '9') {
|
||||||
|
normalizedSolution += ch;
|
||||||
|
} else if (u >= 0xFF10 && u <= 0xFF19) {
|
||||||
|
normalizedSolution += QChar(static_cast<char16_t>(u - 0xFF10 + '0'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apiPayload["captcha_solution"] = normalizedSolution.isEmpty() ? captchaSolution.trimmed() : normalizedSolution;
|
||||||
|
|
||||||
|
QByteArray responseBody;
|
||||||
|
ErrorCode errorCode = executeRequest(QString("%1v1/config"), apiPayload, responseBody);
|
||||||
|
if (errorCode != ErrorCode::NoError) {
|
||||||
|
if (retryCaptchaOut
|
||||||
|
&& (errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||||
|
|| errorCode == ErrorCode::ApiCaptchaRequiredError)) {
|
||||||
|
const QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody);
|
||||||
|
if (jsonDoc.isObject()) {
|
||||||
|
const QJsonObject jsonObj = jsonDoc.object();
|
||||||
|
if (jsonObj.contains(QStringLiteral("captcha_id")) && jsonObj.contains(QStringLiteral("captcha_image"))) {
|
||||||
|
retryCaptchaOut->captchaId = jsonObj.value(QStringLiteral("captcha_id")).toString();
|
||||||
|
retryCaptchaOut->captchaImageBase64 = jsonObj.value(QStringLiteral("captcha_image")).toString();
|
||||||
|
retryCaptchaOut->hint = jsonObj.value(QStringLiteral("hint")).toString();
|
||||||
|
retryCaptchaOut->isRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject serverConfigJson;
|
||||||
|
errorCode = extractServerConfigJsonFromResponse(responseBody, serviceProtocol, protocolData, serverConfigJson);
|
||||||
|
if (errorCode != ErrorCode::NoError) {
|
||||||
|
return errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateApiConfigInJson(serverConfigJson, serviceType, serviceProtocol, userCountryCode, responseBody);
|
||||||
|
|
||||||
|
if (serverConfigJson.value(configKey::configVersion).toInt() != serverConfigUtils::ConfigSource::AmneziaGateway) {
|
||||||
|
return ErrorCode::InternalError;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiV2ServerConfig apiV2ServerConfig = ApiV2ServerConfig::fromJson(serverConfigJson);
|
||||||
|
m_serversRepository->addServer(QString(), apiV2ServerConfig.toJson(),
|
||||||
|
serverConfigUtils::configTypeFromJson(apiV2ServerConfig.toJson()));
|
||||||
|
return ErrorCode::NoError;
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ public:
|
|||||||
QJsonObject toJsonObject() const;
|
QJsonObject toJsonObject() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CaptchaInfo {
|
||||||
|
QString captchaId;
|
||||||
|
QString captchaImageBase64;
|
||||||
|
QString hint;
|
||||||
|
bool isRequired = false;
|
||||||
|
};
|
||||||
|
|
||||||
explicit SubscriptionController(SecureServersRepository* serversRepository,
|
explicit SubscriptionController(SecureServersRepository* serversRepository,
|
||||||
SecureAppSettingsRepository* appSettingsRepository);
|
SecureAppSettingsRepository* appSettingsRepository);
|
||||||
|
|
||||||
@@ -49,7 +56,8 @@ public:
|
|||||||
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload);
|
void appendProtocolDataToApiPayload(const QString &protocol, const ProtocolData &protocolData, QJsonObject &apiPayload);
|
||||||
|
|
||||||
ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
ErrorCode importServiceFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||||
const QString &serviceProtocol, const ProtocolData &protocolData);
|
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||||
|
CaptchaInfo &captchaInfo);
|
||||||
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
ErrorCode importTrialFromGateway(const QString &userCountryCode, const QString &serviceType,
|
||||||
const QString &serviceProtocol, const QString &email);
|
const QString &serviceProtocol, const QString &email);
|
||||||
|
|
||||||
@@ -98,6 +106,11 @@ public:
|
|||||||
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
|
AppStoreRestoreResult processAppStoreRestore(const QString &userCountryCode, const QString &serviceType,
|
||||||
const QString &serviceProtocol);
|
const QString &serviceProtocol);
|
||||||
|
|
||||||
|
ErrorCode resolveImportServiceCaptcha(const QString &userCountryCode, const QString &serviceType,
|
||||||
|
const QString &serviceProtocol, const ProtocolData &protocolData,
|
||||||
|
const QString &captchaId, const QString &captchaSolution,
|
||||||
|
CaptchaInfo *retryCaptchaOut = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
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);
|
||||||
bool isApiKeyExpired(const QString &serverId) const;
|
bool isApiKeyExpired(const QString &serverId) const;
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ namespace
|
|||||||
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
|
constexpr QLatin1String errorResponsePattern1("No active configuration found for");
|
||||||
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
|
constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for");
|
||||||
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
constexpr QLatin1String errorResponsePattern3("Account not found.");
|
||||||
|
constexpr QLatin1String errorResponsePatternQrSessionNotFound("QR session not found");
|
||||||
|
constexpr QLatin1String errorResponsePatternSessionNotFound("Session not found");
|
||||||
|
|
||||||
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
constexpr QLatin1String updateRequestResponsePattern("client version update is required");
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ namespace
|
|||||||
constexpr int httpStatusCodeConflict = 409;
|
constexpr int httpStatusCodeConflict = 409;
|
||||||
constexpr int httpStatusCodeNotImplemented = 501;
|
constexpr int httpStatusCodeNotImplemented = 501;
|
||||||
constexpr int httpStatusCodePaymentRequired = 402;
|
constexpr int httpStatusCodePaymentRequired = 402;
|
||||||
|
constexpr int httpStatusCodeRequestTimeout = 408;
|
||||||
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
constexpr int httpStatusCodeUnprocessableEntity = 422;
|
||||||
|
|
||||||
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
|
||||||
@@ -206,8 +209,9 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
|||||||
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
|
bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto errorCode =
|
responseBody = decryptionResult.decryptedBody;
|
||||||
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, decryptionResult.decryptedBody);
|
const auto errorCode =
|
||||||
|
apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, responseBody);
|
||||||
if (errorCode) {
|
if (errorCode) {
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
@@ -217,7 +221,6 @@ ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject api
|
|||||||
return ErrorCode::ApiConfigDecryptionError;
|
return ErrorCode::ApiConfigDecryptionError;
|
||||||
}
|
}
|
||||||
|
|
||||||
responseBody = decryptionResult.decryptedBody;
|
|
||||||
return ErrorCode::NoError;
|
return ErrorCode::NoError;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +259,7 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
|||||||
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
|
auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode,
|
||||||
decryptionResult.decryptedBody);
|
decryptionResult.decryptedBody);
|
||||||
if (errorCode) {
|
if (errorCode) {
|
||||||
promise->addResult(qMakePair(errorCode, QByteArray()));
|
promise->addResult(qMakePair(errorCode, decryptionResult.decryptedBody));
|
||||||
promise->finish();
|
promise->finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -459,15 +462,19 @@ bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &rep
|
|||||||
qDebug() << "the response contains an html tag";
|
qDebug() << "the response contains an html tag";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (apiHttpStatus == httpStatusCodeRequestTimeout) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (apiHttpStatus == httpStatusCodeNotFound) {
|
if (apiHttpStatus == httpStatusCodeNotFound) {
|
||||||
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2)
|
||||||
|| responseBody.contains(errorResponsePattern3)) {
|
|| responseBody.contains(errorResponsePattern3) || responseBody.contains(errorResponsePatternQrSessionNotFound)
|
||||||
|
|| responseBody.contains(errorResponsePatternSessionNotFound)) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
if (apiHttpStatus == httpStatusCodeNotImplemented) {
|
||||||
if (responseBody.contains(updateRequestResponsePattern)) {
|
if (responseBody.contains(updateRequestResponsePattern)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -84,15 +84,14 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
|||||||
const int httpStatusCodeNotFound = 404;
|
const int httpStatusCodeNotFound = 404;
|
||||||
const int httpStatusCodeNotImplemented = 501;
|
const int httpStatusCodeNotImplemented = 501;
|
||||||
const int httpStatusCodePaymentRequired = 402;
|
const int httpStatusCodePaymentRequired = 402;
|
||||||
|
const int httpStatusCodeTooManyRequests = 429;
|
||||||
|
const int httpStatusCodeRequestTimeout = 408;
|
||||||
const int httpStatusCodeUnprocessableEntity = 422;
|
const int httpStatusCodeUnprocessableEntity = 422;
|
||||||
|
|
||||||
if (!sslErrors.empty()) {
|
if (!sslErrors.empty()) {
|
||||||
qDebug().noquote() << sslErrors;
|
qDebug().noquote() << sslErrors;
|
||||||
return amnezia::ErrorCode::ApiConfigSslError;
|
return amnezia::ErrorCode::ApiConfigSslError;
|
||||||
}
|
}
|
||||||
if (replyError == QNetworkReply::NoError) {
|
|
||||||
return amnezia::ErrorCode::NoError;
|
|
||||||
}
|
|
||||||
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
if (replyError == QNetworkReply::NetworkError::OperationCanceledError
|
||||||
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
|| replyError == QNetworkReply::NetworkError::TimeoutError) {
|
||||||
qDebug() << replyError;
|
qDebug() << replyError;
|
||||||
@@ -107,6 +106,10 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
|||||||
if (jsonDoc.isObject()) {
|
if (jsonDoc.isObject()) {
|
||||||
QJsonObject jsonObj = jsonDoc.object();
|
QJsonObject jsonObj = jsonDoc.object();
|
||||||
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
const int httpStatusFromBody = jsonObj.value(QStringLiteral("http_status")).toInt(-1);
|
||||||
|
|
||||||
|
if (httpStatusFromBody == httpStatusCodeTooManyRequests) {
|
||||||
|
return amnezia::ErrorCode::ApiRateLimitError;
|
||||||
|
}
|
||||||
if (httpStatusFromBody == httpStatusCodeConflict) {
|
if (httpStatusFromBody == httpStatusCodeConflict) {
|
||||||
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
if (apiErrorMessageFromJson(jsonObj).contains(trialAlreadyUsedMessage, Qt::CaseInsensitive)) {
|
||||||
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
return amnezia::ErrorCode::ApiTrialAlreadyUsedError;
|
||||||
@@ -116,6 +119,9 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
|||||||
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
if (httpStatusFromBody == httpStatusCodeNotFound) {
|
||||||
return amnezia::ErrorCode::ApiNotFoundError;
|
return amnezia::ErrorCode::ApiNotFoundError;
|
||||||
}
|
}
|
||||||
|
if (httpStatusFromBody == httpStatusCodeRequestTimeout) {
|
||||||
|
return amnezia::ErrorCode::ApiConfigTimeoutError;
|
||||||
|
}
|
||||||
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
if (httpStatusFromBody == httpStatusCodeNotImplemented) {
|
||||||
return amnezia::ErrorCode::ApiUpdateRequestError;
|
return amnezia::ErrorCode::ApiUpdateRequestError;
|
||||||
}
|
}
|
||||||
@@ -126,9 +132,28 @@ amnezia::ErrorCode apiUtils::checkNetworkReplyErrors(const QList<QSslError> &ssl
|
|||||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||||
}
|
}
|
||||||
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
if (httpStatusFromBody == httpStatusCodePaymentRequired) {
|
||||||
|
const QString message = apiErrorMessageFromJson(jsonObj);
|
||||||
|
if (message.contains(QLatin1String("refresh_captcha"), Qt::CaseInsensitive)) {
|
||||||
|
return amnezia::ErrorCode::ApiCaptchaRefreshError;
|
||||||
|
}
|
||||||
|
if (message.contains(QLatin1String("invalid_captcha"), Qt::CaseInsensitive)) {
|
||||||
|
return amnezia::ErrorCode::ApiCaptchaInvalidError;
|
||||||
|
}
|
||||||
|
if (jsonObj.contains(QStringLiteral("captcha_id")) || jsonObj.contains(QStringLiteral("captcha_image"))
|
||||||
|
|| message.compare(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive) == 0
|
||||||
|
|| message.contains(QLatin1String("rate_limit_exceeded"), Qt::CaseInsensitive)) {
|
||||||
|
return amnezia::ErrorCode::ApiCaptchaRequiredError;
|
||||||
|
}
|
||||||
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
return amnezia::ErrorCode::ApiSubscriptionNotActiveError;
|
||||||
}
|
}
|
||||||
return amnezia::ErrorCode::ApiConfigDownloadError;
|
|
||||||
|
if (httpStatusFromBody >= 300) {
|
||||||
|
return amnezia::ErrorCode::ApiConfigDownloadError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replyError == QNetworkReply::NoError) {
|
||||||
|
return amnezia::ErrorCode::NoError;
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug() << "something went wrong";
|
qDebug() << "something went wrong";
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ namespace amnezia
|
|||||||
ApiSubscriptionNotActiveError = 1114,
|
ApiSubscriptionNotActiveError = 1114,
|
||||||
ApiNoPurchasedSubscriptionsError = 1115,
|
ApiNoPurchasedSubscriptionsError = 1115,
|
||||||
ApiTrialAlreadyUsedError = 1116,
|
ApiTrialAlreadyUsedError = 1116,
|
||||||
|
ApiCaptchaRequiredError = 1117,
|
||||||
|
ApiCaptchaInvalidError = 1118,
|
||||||
|
ApiCaptchaRefreshError = 1119,
|
||||||
|
ApiRateLimitError = 1120,
|
||||||
|
|
||||||
// QFile errors
|
// QFile errors
|
||||||
OpenError = 1200,
|
OpenError = 1200,
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ QString errorString(ErrorCode code) {
|
|||||||
case (ErrorCode::ApiSubscriptionNotActiveError): errorMessage = QObject::tr("No active subscription found"); 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;
|
case (ErrorCode::ApiNoPurchasedSubscriptionsError): errorMessage = QObject::tr("No purchased subscriptions found. Please purchase a subscription first"); break;
|
||||||
case (ErrorCode::ApiTrialAlreadyUsedError): errorMessage = QObject::tr("This email address has already been used to activate a trial"); break;
|
case (ErrorCode::ApiTrialAlreadyUsedError): errorMessage = QObject::tr("This email address has already been used to activate a trial"); break;
|
||||||
|
case (ErrorCode::ApiCaptchaRequiredError): errorMessage = QObject::tr("CAPTCHA verification is required"); break;
|
||||||
|
case (ErrorCode::ApiCaptchaInvalidError): errorMessage = QObject::tr("CAPTCHA was incorrect. Please try again"); break;
|
||||||
|
case (ErrorCode::ApiCaptchaRefreshError): errorMessage = QObject::tr("CAPTCHA refreshed. Please try again"); break;
|
||||||
|
case (ErrorCode::ApiRateLimitError): errorMessage = QObject::tr("Too many requests. Please try again later"); break;
|
||||||
|
|
||||||
// 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;
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ SubscriptionUiController::SubscriptionUiController(ServersController* serversCon
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SubscriptionUiController::isCaptchaAwaitingUser() const
|
||||||
|
{
|
||||||
|
return m_captchaState.isPending;
|
||||||
|
}
|
||||||
|
|
||||||
bool SubscriptionUiController::exportVpnKey(const QString &serverId, const QString &fileName)
|
bool SubscriptionUiController::exportVpnKey(const QString &serverId, const QString &fileName)
|
||||||
{
|
{
|
||||||
if (fileName.isEmpty()) {
|
if (fileName.isEmpty()) {
|
||||||
@@ -288,18 +293,105 @@ bool SubscriptionUiController::importFreeFromGateway()
|
|||||||
}
|
}
|
||||||
|
|
||||||
SubscriptionController::ProtocolData protocolData = m_subscriptionController->generateProtocolData(serviceProtocol);
|
SubscriptionController::ProtocolData protocolData = m_subscriptionController->generateProtocolData(serviceProtocol);
|
||||||
|
SubscriptionController::CaptchaInfo captchaInfo;
|
||||||
|
|
||||||
ErrorCode errorCode = m_subscriptionController->importServiceFromGateway(userCountryCode, serviceType,
|
ErrorCode errorCode = m_subscriptionController->importServiceFromGateway(userCountryCode, serviceType,
|
||||||
serviceProtocol, protocolData);
|
serviceProtocol, protocolData,
|
||||||
|
captchaInfo);
|
||||||
|
|
||||||
if (errorCode == ErrorCode::NoError) {
|
if (errorCode == ErrorCode::NoError) {
|
||||||
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||||
return true;
|
return true;
|
||||||
|
} else if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||||
|
m_captchaState.userCountryCode = userCountryCode;
|
||||||
|
m_captchaState.serviceType = serviceType;
|
||||||
|
m_captchaState.serviceProtocol = serviceProtocol;
|
||||||
|
m_captchaState.openvpnPrivKey = protocolData.certPrivKey;
|
||||||
|
m_captchaState.wireguardClientPrivKey = protocolData.wireGuardClientPrivKey;
|
||||||
|
m_captchaState.wireguardClientPubKey = protocolData.wireGuardClientPubKey;
|
||||||
|
m_captchaState.xrayUuid = protocolData.xrayUuid;
|
||||||
|
m_captchaState.isPending = true;
|
||||||
|
|
||||||
|
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||||
|
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
emit errorOccurred(errorCode);
|
emit errorOccurred(errorCode);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SubscriptionUiController::onCaptchaSolved(const QString &captchaId, const QString &solution)
|
||||||
|
{
|
||||||
|
if (!m_captchaState.isPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscriptionController::ProtocolData protocolData;
|
||||||
|
protocolData.certPrivKey = m_captchaState.openvpnPrivKey;
|
||||||
|
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
|
||||||
|
protocolData.wireGuardClientPubKey = m_captchaState.wireguardClientPubKey;
|
||||||
|
protocolData.xrayUuid = m_captchaState.xrayUuid;
|
||||||
|
|
||||||
|
SubscriptionController::CaptchaInfo retryCaptcha;
|
||||||
|
ErrorCode errorCode = m_subscriptionController->resolveImportServiceCaptcha(
|
||||||
|
m_captchaState.userCountryCode,
|
||||||
|
m_captchaState.serviceType,
|
||||||
|
m_captchaState.serviceProtocol,
|
||||||
|
protocolData,
|
||||||
|
captchaId,
|
||||||
|
solution,
|
||||||
|
&retryCaptcha);
|
||||||
|
|
||||||
|
if (errorCode == ErrorCode::NoError) {
|
||||||
|
m_captchaState.isPending = false;
|
||||||
|
emit captchaFlowDismissRequested();
|
||||||
|
emit installServerFromApiFinished(tr("%1 installed successfully.").arg(m_apiServicesModel->getSelectedServiceName()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((errorCode == ErrorCode::ApiCaptchaInvalidError || errorCode == ErrorCode::ApiCaptchaRefreshError
|
||||||
|
|| errorCode == ErrorCode::ApiCaptchaRequiredError)
|
||||||
|
&& retryCaptcha.isRequired) {
|
||||||
|
emit captchaRequired(retryCaptcha.captchaId, retryCaptcha.captchaImageBase64,
|
||||||
|
retryCaptcha.hint.isEmpty() ? tr("Enter the digits from the image to continue") : retryCaptcha.hint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_captchaState.isPending = false;
|
||||||
|
emit errorOccurred(errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SubscriptionUiController::onRefreshCaptchaRequested()
|
||||||
|
{
|
||||||
|
if (!m_captchaState.isPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscriptionController::ProtocolData protocolData;
|
||||||
|
protocolData.certPrivKey = m_captchaState.openvpnPrivKey;
|
||||||
|
protocolData.wireGuardClientPrivKey = m_captchaState.wireguardClientPrivKey;
|
||||||
|
protocolData.wireGuardClientPubKey = m_captchaState.wireguardClientPubKey;
|
||||||
|
protocolData.xrayUuid = m_captchaState.xrayUuid;
|
||||||
|
|
||||||
|
SubscriptionController::CaptchaInfo captchaInfo;
|
||||||
|
|
||||||
|
ErrorCode errorCode = m_subscriptionController->importServiceFromGateway(
|
||||||
|
m_captchaState.userCountryCode,
|
||||||
|
m_captchaState.serviceType,
|
||||||
|
m_captchaState.serviceProtocol,
|
||||||
|
protocolData,
|
||||||
|
captchaInfo);
|
||||||
|
|
||||||
|
if (errorCode == ErrorCode::ApiCaptchaRequiredError && captchaInfo.isRequired) {
|
||||||
|
emit captchaRequired(captchaInfo.captchaId, captchaInfo.captchaImageBase64,
|
||||||
|
captchaInfo.hint.isEmpty() ? tr("Enter the digits from the image to continue") : captchaInfo.hint);
|
||||||
|
} else if (errorCode != ErrorCode::NoError) {
|
||||||
|
m_captchaState.isPending = false;
|
||||||
|
emit errorOccurred(errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool SubscriptionUiController::importTrialFromGateway(const QString &email)
|
bool SubscriptionUiController::importTrialFromGateway(const QString &email)
|
||||||
{
|
{
|
||||||
emit trialEmailError(QString());
|
emit trialEmailError(QString());
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ public slots:
|
|||||||
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
|
void setCurrentProtocol(const QString &serverId, const QString &protocolName);
|
||||||
bool isVlessProtocol(const QString &serverId);
|
bool isVlessProtocol(const QString &serverId);
|
||||||
|
|
||||||
|
bool isCaptchaAwaitingUser() const;
|
||||||
|
void onCaptchaSolved(const QString &captchaId, const QString &solution);
|
||||||
|
void onRefreshCaptchaRequested();
|
||||||
|
|
||||||
void removeApiConfig(const QString &serverId);
|
void removeApiConfig(const QString &serverId);
|
||||||
|
|
||||||
void removeServer(const QString &serverId);
|
void removeServer(const QString &serverId);
|
||||||
@@ -85,9 +89,23 @@ signals:
|
|||||||
void apiServerRemoved(const QString &message);
|
void apiServerRemoved(const QString &message);
|
||||||
|
|
||||||
void vpnKeyExportReady();
|
void vpnKeyExportReady();
|
||||||
|
void captchaRequired(const QString &captchaId, const QString &captchaImageBase64, const QString &hint);
|
||||||
|
void captchaFlowDismissRequested();
|
||||||
|
|
||||||
void unsupportedConnectDrawerRequested();
|
void unsupportedConnectDrawerRequested();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct CaptchaState {
|
||||||
|
QString userCountryCode;
|
||||||
|
QString serviceType;
|
||||||
|
QString serviceProtocol;
|
||||||
|
QString openvpnPrivKey;
|
||||||
|
QString wireguardClientPrivKey;
|
||||||
|
QString wireguardClientPubKey;
|
||||||
|
QString xrayUuid;
|
||||||
|
bool isPending = false;
|
||||||
|
} m_captchaState;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QList<QString> getQrCodes();
|
QList<QString> getQrCodes();
|
||||||
int getQrCodesCount();
|
int getQrCodesCount();
|
||||||
|
|||||||
@@ -0,0 +1,263 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Window
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
|
||||||
|
import Style 1.0
|
||||||
|
|
||||||
|
import "."
|
||||||
|
import "TextTypes"
|
||||||
|
import "../Config"
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string captchaId
|
||||||
|
property string captchaImageBase64
|
||||||
|
property string hint: qsTr("Enter the digits from the image to continue")
|
||||||
|
|
||||||
|
signal captchaSolved(string captchaId, string solution)
|
||||||
|
signal refreshCaptchaRequested()
|
||||||
|
|
||||||
|
leftMargin: 25
|
||||||
|
rightMargin: 25
|
||||||
|
bottomMargin: 70 + SettingsController.safeAreaBottomMargin
|
||||||
|
|
||||||
|
width: parent.width - leftMargin - rightMargin
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
modal: true
|
||||||
|
closePolicy: Popup.NoAutoClose
|
||||||
|
|
||||||
|
Overlay.modal: Rectangle {
|
||||||
|
color: AmneziaStyle.color.translucentMidnightBlack
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
timer.start()
|
||||||
|
solutionField.textField.text = ""
|
||||||
|
solutionField.textField.focus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
onCaptchaIdChanged: {
|
||||||
|
if (opened) {
|
||||||
|
solutionField.textField.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCaptchaImageBase64Changed: {
|
||||||
|
if (opened) {
|
||||||
|
solutionField.textField.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClosed: {
|
||||||
|
FocusController.dropRootObject(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: AmneziaStyle.color.slateGray
|
||||||
|
radius: 22
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: timer
|
||||||
|
interval: 200
|
||||||
|
onTriggered: {
|
||||||
|
FocusController.pushRootObject(root)
|
||||||
|
FocusController.setFocusItem(solutionField.textField)
|
||||||
|
}
|
||||||
|
repeat: false
|
||||||
|
running: true
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Item {
|
||||||
|
implicitWidth: contentLayout.implicitWidth
|
||||||
|
implicitHeight: contentLayout.implicitHeight
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: contentLayout
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 20
|
||||||
|
anchors.rightMargin: 20
|
||||||
|
anchors.topMargin: 20
|
||||||
|
anchors.bottomMargin: 20
|
||||||
|
|
||||||
|
spacing: 16
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: titleText
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||||
|
|
||||||
|
text: root.hint
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
color: AmneziaStyle.color.paleGray
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.weight: Font.Bold
|
||||||
|
font.family: "PT Root UI VF"
|
||||||
|
lineHeight: 24 + LanguageUiController.getLineHeightAppend()
|
||||||
|
lineHeightMode: Text.FixedHeight
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
verticalAlignment: Text.AlignTop
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 200
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: imagePanel
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
color: AmneziaStyle.color.pearlGray
|
||||||
|
radius: 16
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: captchaImage
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (captchaImageBase64 !== "") {
|
||||||
|
source = "data:image/png;base64," + captchaImageBase64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onCaptchaImageBase64Changed() {
|
||||||
|
captchaImage.source = "data:image/png;base64," + root.captchaImageBase64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
running: captchaImage.status === Image.Loading
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: refreshHit
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: 10
|
||||||
|
width: 44
|
||||||
|
height: 44
|
||||||
|
radius: width / 2
|
||||||
|
color: AmneziaStyle.color.charcoalGray
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: refreshIcon
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 26
|
||||||
|
height: 26
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
antialiasing: true
|
||||||
|
source: "qrc:/images/controls/refresh-cw.svg"
|
||||||
|
// Rasterize SVG at high resolution, then scale down — avoids blocky edges on HiDPI.
|
||||||
|
readonly property real _dpr: (Window.window && Window.window.screen)
|
||||||
|
? Window.window.screen.devicePixelRatio : 2.0
|
||||||
|
readonly property int _raster: Math.ceil(64 * Math.min(Math.max(_dpr, 1.0), 4.0))
|
||||||
|
sourceSize: Qt.size(_raster, _raster)
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: true
|
||||||
|
layer.textureSize: Qt.size(_raster, _raster)
|
||||||
|
layer.effect: ColorOverlay {
|
||||||
|
color: AmneziaStyle.color.goldenApricot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.refreshCaptchaRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextFieldWithHeaderType {
|
||||||
|
id: solutionField
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.alignment: Qt.AlignLeft
|
||||||
|
|
||||||
|
headerText: qsTr("Digits from the image")
|
||||||
|
headerTextColor: AmneziaStyle.color.mutedGray
|
||||||
|
|
||||||
|
textField.placeholderText: qsTr("_ _ _ _ _ _")
|
||||||
|
textField.placeholderTextColor: AmneziaStyle.color.mutedGray
|
||||||
|
textField.inputMethodHints: Qt.ImhDigitsOnly | Qt.ImhNoPredictiveText
|
||||||
|
textField.maximumLength: 6
|
||||||
|
textField.font.letterSpacing: 2
|
||||||
|
|
||||||
|
textField.onAccepted: {
|
||||||
|
submitIfNonEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicButtonType {
|
||||||
|
id: continueButton
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 52
|
||||||
|
|
||||||
|
text: qsTr("Continue")
|
||||||
|
defaultColor: AmneziaStyle.color.paleGray
|
||||||
|
hoveredColor: AmneziaStyle.color.lightGray
|
||||||
|
pressedColor: AmneziaStyle.color.mutedGray
|
||||||
|
textColor: AmneziaStyle.color.midnightBlack
|
||||||
|
|
||||||
|
clickedFunc: function() {
|
||||||
|
submitIfNonEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicButtonType {
|
||||||
|
id: closeButton
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 52
|
||||||
|
|
||||||
|
text: qsTr("Close")
|
||||||
|
defaultColor: AmneziaStyle.color.transparent
|
||||||
|
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||||
|
pressedColor: AmneziaStyle.color.sheerWhite
|
||||||
|
textColor: AmneziaStyle.color.paleGray
|
||||||
|
borderWidth: 1
|
||||||
|
borderColor: AmneziaStyle.color.mutedGray
|
||||||
|
borderFocusedColor: AmneziaStyle.color.paleGray
|
||||||
|
|
||||||
|
clickedFunc: function() {
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitIfNonEmpty() {
|
||||||
|
const t = solutionField.textField.text.trim()
|
||||||
|
if (t !== "") {
|
||||||
|
root.captchaSolved(root.captchaId, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -130,6 +130,9 @@ PageType {
|
|||||||
PageController.showBusyIndicator(false)
|
PageController.showBusyIndicator(false)
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
if (SubscriptionUiController.isCaptchaAwaitingUser()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
var endpoint = ApiServicesModel.getStoreEndpoint()
|
var endpoint = ApiServicesModel.getStoreEndpoint()
|
||||||
Qt.openUrlExternally(endpoint)
|
Qt.openUrlExternally(endpoint)
|
||||||
PageController.closePage()
|
PageController.closePage()
|
||||||
|
|||||||
@@ -205,6 +205,27 @@ Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
objectName: "captchaDialogItem"
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
CaptchaDialogType {
|
||||||
|
id: captchaDialog
|
||||||
|
|
||||||
|
onCaptchaSolved: function(captchaId, solution) {
|
||||||
|
PageController.showBusyIndicator(true)
|
||||||
|
Qt.callLater(function() {
|
||||||
|
SubscriptionUiController.onCaptchaSolved(captchaId, solution)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onRefreshCaptchaRequested: function() {
|
||||||
|
SubscriptionUiController.onRefreshCaptchaRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
objectName: "privateKeyPassphraseDrawerItem"
|
objectName: "privateKeyPassphraseDrawerItem"
|
||||||
|
|
||||||
@@ -318,6 +339,27 @@ Window {
|
|||||||
function onSubscriptionExpiredOnServer() {
|
function onSubscriptionExpiredOnServer() {
|
||||||
subscriptionExpiredDrawer.openTriggered()
|
subscriptionExpiredDrawer.openTriggered()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onCaptchaRequired(captchaId, captchaImageBase64, hint) {
|
||||||
|
if (captchaDialog.opened) {
|
||||||
|
PageController.showBusyIndicator(false)
|
||||||
|
}
|
||||||
|
captchaDialog.captchaId = captchaId
|
||||||
|
captchaDialog.captchaImageBase64 = captchaImageBase64
|
||||||
|
captchaDialog.hint = hint
|
||||||
|
captchaDialog.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCaptchaFlowDismissRequested() {
|
||||||
|
PageController.showBusyIndicator(false)
|
||||||
|
captchaDialog.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onErrorOccurred(error) {
|
||||||
|
if (captchaDialog.opened) {
|
||||||
|
PageController.showBusyIndicator(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<file>Controls2/BackButtonType.qml</file>
|
<file>Controls2/BackButtonType.qml</file>
|
||||||
<file>Controls2/BasicButtonType.qml</file>
|
<file>Controls2/BasicButtonType.qml</file>
|
||||||
<file>Controls2/BusyIndicatorType.qml</file>
|
<file>Controls2/BusyIndicatorType.qml</file>
|
||||||
|
<file>Controls2/CaptchaDialogType.qml</file>
|
||||||
<file>Controls2/CardType.qml</file>
|
<file>Controls2/CardType.qml</file>
|
||||||
<file>Controls2/CardWithIconsType.qml</file>
|
<file>Controls2/CardWithIconsType.qml</file>
|
||||||
<file>Controls2/CheckBoxType.qml</file>
|
<file>Controls2/CheckBoxType.qml</file>
|
||||||
|
|||||||
Reference in New Issue
Block a user