1
0
forked from BRT/arc

Add Ozon scripts for retrieving card info, profile details, and transaction history

Implemented `GetCardInfoScript`, `GetProfileInfoScript`, and `GetLastTransactionsScript` to automate data extraction workflows for Ozon platform. Includes support for XML parsing, ad banner handling, card navigation, profile parsing, and transaction retrieval. Added related asset files.
This commit is contained in:
trnsmkot 2026-02-27 10:15:19 +07:00
parent 3a7b6dfc24
commit 6fde44a9a4
47 changed files with 6114 additions and 103 deletions

View File

@ -27,8 +27,7 @@ if (WIN32)
elseif (APPLE)
# Укажи путь к Qt6
set(CMAKE_PREFIX_PATH "/opt/homebrew/opt/qt")
include_directories("/opt/homebrew/opt/tesseract/include")
link_directories("/opt/homebrew/opt/tesseract/lib")
link_directories("/opt/homebrew/lib")
endif ()
find_package(Qt6 REQUIRED COMPONENTS
@ -136,7 +135,6 @@ elseif (APPLE)
Qt6::Sql
Qt6::Xml
Qt6::Network
tesseract
)
endif ()

View File

@ -0,0 +1,309 @@
#include "GetCardInfoScript.h"
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSettings>
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
#include "net/NetworkService.h"
namespace Ozon {
GetCardInfoScript::GetCardInfoScript(
QString deviceId,
QString appCode,
QString lastNumbers,
QObject *parent
) : CommonScript(parent),
m_deviceId(std::move(deviceId)),
m_appCode(std::move(appCode)),
m_lastNumbers(std::move(lastNumbers)) {
}
GetCardInfoScript::~GetCardInfoScript() = default;
void GetCardInfoScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
// Читаем текущий экран
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Закрываем рекламный bottom sheet, если открыт
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[GetCardInfo] Ad banner detected, closing...";
AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Свежий дамп перед проверкой домашнего экрана
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Если не на домашнем экране — перезапускаем приложение
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[GetCardInfo] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Закрываем баннеры на главной
tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Переходим в профиль и парсим имя и телефон (только если ещё нет в БД)
const bool hasProfileInfo = !app.fullName.isEmpty() && !app.phone.isEmpty();
Node userNameNode;
if (!hasProfileInfo) {
userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight);
}
if (!hasProfileInfo && !userNameNode.isEmpty()) {
qDebug() << "[GetCardInfo] Navigating to profile to parse name/phone...";
AdbUtils::makeTap(m_deviceId, userNameNode.x(), userNameNode.y());
QThread::msleep(2500);
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isProfileScreen()) break;
QThread::msleep(1000);
}
if (xmlScreenParser.isProfileScreen()) {
const QString fullName = xmlScreenParser.parseProfileFullName();
const QString phone = xmlScreenParser.parseProfilePhone();
qDebug() << "[GetCardInfo] Profile parsed - fullName:" << fullName << "phone:" << phone;
ApplicationDAO::updateProfileInfo(app.id, fullName, phone);
} else {
qWarning() << "[GetCardInfo] Profile screen not reached, skipping profile parse";
}
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
} else {
qDebug() << "[GetCardInfo] Profile info already in DB or username not found, skipping profile parse";
}
// Ждём пока все карты подгрузятся — до 5 попыток
QList<AccountInfo> cards;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
cards = xmlScreenParser.parseCardsOnHomeScreen();
if (!cards.isEmpty()) break;
qDebug() << "[GetCardInfo] No cards found on home screen, attempt" << attempt + 1 << "/ 5";
QThread::msleep(1500);
}
if (cards.isEmpty()) {
m_error = "No cards found on home screen: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
qDebug() << "[GetCardInfo] Found" << cards.size() << "card(s) on home screen";
// Апсёртим аккаунты для всех найденных карт
for (AccountInfo &card : cards) {
card.deviceId = m_deviceId;
card.appCode = m_appCode;
card.updateTime = QDateTime::currentDateTimeUtc();
card.status = AccountStatus::Active;
if (!AccountDAO::upsertAccount(card)) {
qWarning() << "[GetCardInfo] Failed to upsert account for card" << card.lastNumbers;
} else {
qDebug() << "[GetCardInfo] Upserted account:" << card.lastNumbers
<< card.description << card.amount;
}
}
// Сохраняем баланс домашнего экрана в application
const double totalAmount = cards.isEmpty() ? 0.0 : cards[0].amount;
if (totalAmount > 0.0) {
ApplicationDAO::updateAmount(app.id, totalAmount);
}
// Синхронная отправка на сервер (NetworkService блокируется через QEventLoop внутри)
NetworkService ns;
ns.setDeviceId(m_deviceId);
// 1. Создаём девайс если нет api_id
if (device.apiId.isEmpty()) {
QJsonObject deviceBody;
deviceBody["desktop_id"] = QSettings("config.ini", QSettings::IniFormat).value("common/desktop_id").toString();
deviceBody["status"] = "ONLINE";
deviceBody["name"] = device.name;
deviceBody["screen_width"] = device.screenWidth;
deviceBody["screen_height"]= device.screenHeight;
deviceBody["battery"] = device.battery;
ns.setPayload(QJsonDocument(deviceBody).toJson());
ns.createDevice();
qDebug() << "[GetCardInfo] createDevice done";
}
// 2. Отправляем банковский профиль (используем свежие данные из БД — имя/телефон уже спарсены)
{
const ApplicationInfo freshApp = ApplicationDAO::getApplication(m_deviceId, m_appCode);
const QStringList parts = freshApp.fullName.split(' ', Qt::SkipEmptyParts);
// ISO 4217 numeric currency codes
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}
};
const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643);
QJsonObject profileBody;
profileBody["first_name"] = parts.value(0);
profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : "";
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
profileBody["bank_name"] = m_appCode;
// Телефон: убираем пробелы, оставляем "+"
const QString phone = freshApp.phone.simplified().remove(' ');
profileBody["phone_number"] = phone;
profileBody["balance"] = QString::number(qRound64(totalAmount * 100));
profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled";
profileBody["currency"] = currencyCode;
ns.setPayload(QJsonDocument(profileBody).toJson());
if (freshApp.bankProfileId.isEmpty()) {
ns.addExtra("app_id", freshApp.id);
ns.postBankProfile();
qDebug() << "[GetCardInfo] postBankProfile done";
} else {
ns.addExtra("bank_profile_id", freshApp.bankProfileId);
ns.patchBankProfile();
qDebug() << "[GetCardInfo] patchBankProfile done";
}
}
// Для каждой карты переходим на её экран и получаем полный номер через OCR
for (int i = 0; i < cards.size(); ++i) {
const AccountInfo &card = cards[i];
// Если передан конкретный lastNumbers — обрабатываем только его
if (!m_lastNumbers.isEmpty() && card.lastNumbers != m_lastNumbers) continue;
qDebug() << "[GetCardInfo] Getting card number for" << card.lastNumbers;
// Если номер карты уже есть в БД — сумма уже обновлена через upsert, навигация не нужна
AccountInfo existing = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers);
if (existing.id != -1 && !existing.cardNumber.isEmpty()) {
qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "already has number, skipping";
continue;
}
// Тапаем по кнопке конкретной карты — каждая открывает свой экран
Node cardBtn = xmlScreenParser.findCardButtonByLastNumbers(card.lastNumbers);
if (cardBtn.isEmpty()) {
qWarning() << "[GetCardInfo] Card button not found for" << card.lastNumbers;
continue;
}
AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y());
// Ждём экран карты — ищем "Реквизиты карты" в XML дампе
Node showBtn;
for (int attempt = 0; attempt < 10; ++attempt) {
QThread::msleep(2000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
Node reqNode = xmlScreenParser.findTextNode("Реквизиты карты");
if (!reqNode.isEmpty()) {
showBtn = xmlScreenParser.findButtonNode("Показать", false);
if (!showBtn.isEmpty()) break;
}
qDebug() << "[GetCardInfo] Card screen not loaded, attempt" << attempt + 1 << "/ 10";
}
if (showBtn.isEmpty()) {
qWarning() << "[GetCardInfo] 'Показать' not found for card" << card.lastNumbers;
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
continue;
}
qDebug() << "[GetCardInfo] 'Показать' at" << showBtn.x() << showBtn.y();
AdbUtils::makeTap(m_deviceId, showBtn.x(), showBtn.y());
// Ждём появления полного номера карты в XML дампе
QString cardNumber;
for (int attempt = 0; attempt < 10; ++attempt) {
QThread::msleep(2000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
cardNumber = xmlScreenParser.findFullCardNumber();
if (!cardNumber.isEmpty()) {
qDebug() << "[GetCardInfo] Card number found on attempt" << attempt + 1;
break;
}
qDebug() << "[GetCardInfo] Card number not visible, attempt" << attempt + 1 << "/ 10";
}
if (!cardNumber.isEmpty()) {
qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "full number:" << cardNumber;
// Получаем id аккаунта из БД и сохраняем номер карты
AccountInfo saved = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers);
if (saved.id != -1) {
AccountDAO::updateCardNumber(saved.id, cardNumber);
}
} else {
qWarning() << "[GetCardInfo] Card number not found for" << card.lastNumbers;
}
// Возвращаемся на домашний экран для следующей карты
if (i < cards.size() - 1) {
AdbUtils::goBack(m_deviceId);
QThread::msleep(2000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
}
// 3. Отправляем материалы (карты) — после OCR, когда номера карт уже в БД
const ApplicationInfo updatedApp = ApplicationDAO::getApplication(m_deviceId, m_appCode);
if (!updatedApp.bankProfileId.isEmpty()) {
for (const AccountInfo &card : cards) {
const AccountInfo dbCard = AccountDAO::findAppAccount(m_deviceId, m_appCode, card.lastNumbers);
if (dbCard.cardNumber.isEmpty()) continue;
if (!dbCard.materialId.isEmpty()) {
qDebug() << "[GetCardInfo] material already exists for card" << dbCard.lastNumbers << ", skipping";
continue;
}
QJsonObject materialBody;
materialBody["bank_profile_id"] = updatedApp.bankProfileId;
materialBody["card_number"] = dbCard.cardNumber;
materialBody["name"] = updatedApp.fullName;
ns.setPayload(QJsonDocument(materialBody).toJson());
ns.addExtra("account_id", dbCard.id);
ns.postMaterial();
qDebug() << "[GetCardInfo] postMaterial done for card" << dbCard.lastNumbers;
}
} else {
qWarning() << "[GetCardInfo] bankProfileId empty, skipping materials";
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
}
} // namespace Ozon

View File

@ -0,0 +1,32 @@
#pragma once
#include "CommonScript.h"
namespace Ozon {
class GetCardInfoScript final : public CommonScript {
Q_OBJECT
public:
// lastNumbers — последние 4 цифры карты (например "9865").
// Если пусто — берём первую найденную карту на домашнем экране.
explicit GetCardInfoScript(
QString deviceId,
QString appCode,
QString lastNumbers,
QObject *parent = nullptr
);
~GetCardInfoScript() override;
protected:
void doStart() override;
private:
const QString m_deviceId;
const QString m_appCode;
const QString m_lastNumbers;
QString m_error;
};
} // namespace Ozon

View File

@ -0,0 +1,92 @@
#include "GetLastTransactionsScript.h"
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
namespace Ozon {
GetLastTransactionsScript::GetLastTransactionsScript(
QString deviceId,
QString appCode,
QObject *parent
) : CommonScript(parent),
m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)) {
}
GetLastTransactionsScript::~GetLastTransactionsScript() = default;
void GetLastTransactionsScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Закрываем рекламный bottom sheet, если открыт
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[GetLastTransactions] Ad banner detected, closing...";
AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Если не на домашнем экране — перезапускаем приложение
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[GetLastTransactions] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Закрываем баннеры на главной
tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Скроллим вниз до кнопки "Все" и тапаем по ней
Node allBtn = swipeToButton(m_deviceId, "Все", m_counter,
device.screenWidth, device.screenHeight);
if (allBtn.isEmpty()) {
m_error = "Button 'Все' not found after scrolling: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
qDebug() << "[GetLastTransactions] Found 'Все' at" << allBtn.x() << allBtn.y();
AdbUtils::makeTap(m_deviceId, allBtn.x(), allBtn.y());
QThread::msleep(2000);
// Парсим транзакции за сегодня и вчера
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
QList<TransactionInfo> transactions = xmlScreenParser.parseTodayAndYesterdayHistory();
qDebug() << "[GetLastTransactions] Found" << transactions.size() << "transactions";
for (const TransactionInfo &tx : transactions) {
qDebug() << "[GetLastTransactions]" << tx.description << tx.amount << tx.bankTime;
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
}
} // namespace Ozon

View File

@ -0,0 +1,27 @@
#pragma once
#include "CommonScript.h"
namespace Ozon {
class GetLastTransactionsScript final : public CommonScript {
Q_OBJECT
public:
explicit GetLastTransactionsScript(
QString deviceId,
QString appCode,
QObject *parent = nullptr
);
~GetLastTransactionsScript() override;
protected:
void doStart() override;
private:
const QString m_deviceId;
const QString m_appCode;
QString m_error;
};
} // namespace Ozon

View File

@ -0,0 +1,149 @@
#include "GetProfileInfoScript.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QMap>
#include <QtGlobal>
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
#include "net/NetworkService.h"
namespace Ozon {
GetProfileInfoScript::GetProfileInfoScript(
QString deviceId,
QString appCode,
QObject *parent
) : CommonScript(parent),
m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)) {
}
GetProfileInfoScript::~GetProfileInfoScript() = default;
void GetProfileInfoScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_deviceId, m_appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_deviceId + " " + m_appCode;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
// Читаем текущий экран
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Закрываем рекламный bottom sheet, если открыт (home_ad_1)
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[GetProfileInfo] Ad banner detected, closing...";
AdbUtils::makeTap(m_deviceId, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Свежий дамп перед проверкой домашнего экрана
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Если мы не на домашнем экране — перезапускаем приложение и вводим пин-код
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[GetProfileInfo] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + m_deviceId + " " + m_appCode;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
// После runAppAndGoToHomeScreen xml уже обновлён внутри метода,
// но делаем свежий дамп, чтобы убедиться
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
}
// Закрываем баннеры на главной, если есть
tryToCloseAllBannersOnHomePage(m_deviceId, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Находим имя пользователя в верхней части домашнего экрана и тапаем по нему
Node userNameNode = xmlScreenParser.findUserNameOnHomeScreen(device.screenHeight);
if (userNameNode.isEmpty()) {
m_error = "Cannot find user name on home screen: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
qDebug() << "[GetProfileInfo] Tapping on user name:" << userNameNode.text
<< "at" << userNameNode.x() << userNameNode.y();
AdbUtils::makeTap(m_deviceId, userNameNode.x(), userNameNode.y());
// Ждём загрузки страницы профиля
QThread::msleep(2500);
// Читаем XML профиля и проверяем что мы действительно там
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isProfileScreen()) {
break;
}
QThread::msleep(1000);
}
if (!xmlScreenParser.isProfileScreen()) {
m_error = "Profile screen not detected after tap: " + m_deviceId;
qWarning() << m_error;
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
return;
}
// Извлекаем полное имя и телефон
const QString fullName = xmlScreenParser.parseProfileFullName();
const QString phone = xmlScreenParser.parseProfilePhone().simplified().remove(' ');
qDebug() << "[GetProfileInfo] fullName:" << fullName << "phone:" << phone;
if (!ApplicationDAO::updateProfileInfo(app.id, fullName, phone)) {
m_error = "Failed to save profile info to DB: " + m_deviceId;
qWarning() << m_error;
}
// Отправляем bank_profile на сервер если профиля ещё нет
if (app.bankProfileId.isEmpty()) {
const ApplicationInfo freshApp = ApplicationDAO::getApplication(m_deviceId, m_appCode);
const QMap<QString, int> currencyCodes = {
{"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}
};
const int currencyCode = currencyCodes.value(freshApp.currency.toUpper(), 643);
const QStringList parts = fullName.split(' ', Qt::SkipEmptyParts);
QJsonObject profileBody;
profileBody["first_name"] = parts.value(0);
profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : "";
profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1);
profileBody["bank_name"] = m_appCode;
profileBody["phone_number"] = phone;
profileBody["balance"] = QString::number(qRound64(freshApp.amount * 100));
profileBody["state"] = (freshApp.status == "active") ? "enabled" : "disabled";
profileBody["currency"] = currencyCode;
NetworkService ns;
ns.setDeviceId(m_deviceId);
ns.setPayload(QJsonDocument(profileBody).toJson());
ns.addExtra("app_id", freshApp.id);
ns.postBankProfile();
qDebug() << "[GetProfileInfo] postBankProfile done";
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
}
} // namespace Ozon

View File

@ -0,0 +1,27 @@
#pragma once
#include "CommonScript.h"
namespace Ozon {
class GetProfileInfoScript final : public CommonScript {
Q_OBJECT
public:
~GetProfileInfoScript() override;
explicit GetProfileInfoScript(
QString deviceId,
QString appCode,
QObject *parent = nullptr
);
protected:
void doStart() override;
private:
const QString m_deviceId;
const QString m_appCode;
QString m_error;
};
} // namespace Ozon

View File

@ -1,9 +1,12 @@
#include "LoginAndCheckAccountsScript.h"
#include <QThread>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
#include "GetCardInfoScript.h"
namespace Ozon {
@ -60,9 +63,12 @@ void LoginAndCheckAccountsScript::doStart() {
} else {
qWarning() << "Cant go to all products: " << m_deviceId << " " << m_appCode;
}
// Запускаем сканирование баланса, данных карт и отправку на сервер (синхронно)
GetCardInfoScript cardScript(m_deviceId, m_appCode, "");
cardScript.start();
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
}

View File

@ -179,6 +179,181 @@ bool ScreenXmlParser::isHomeScreen() {
return false;
}
Node ScreenXmlParser::tryToFindAdBanner() {
// Рекламный bottom sheet (home_ad_1) определяется по наличию кликабельного
// фонового оверлея "touch_outside" и самого bottom sheet контейнера.
bool hasBottomSheet = false;
Node touchOutside;
for (const Node &node: m_nodes) {
if (node.resourceId == "ru.ozon.fintech.finance:id/design_bottom_sheet") {
hasBottomSheet = true;
}
if (node.resourceId == "ru.ozon.fintech.finance:id/touch_outside" && node.clickable) {
touchOutside = node;
}
}
if (hasBottomSheet && !touchOutside.isEmpty()) {
return touchOutside;
}
return {};
}
bool ScreenXmlParser::isProfileScreen() {
// Страница профиля Ozon: есть заголовок "Аккаунт" и кнопка "Выйти"
bool hasTitle = false;
bool hasExit = false;
for (const Node &node: m_nodes) {
const QString t = node.text.trimmed();
if (t == "Аккаунт") hasTitle = true;
if (t == "Выйти") hasExit = true;
if (hasTitle && hasExit) return true;
}
return false;
}
Node ScreenXmlParser::findUserNameOnHomeScreen(const int screenHeight) {
// Имя пользователя — TextView в верхней части домашнего экрана (y < screenHeight/5).
// Тап по его координатам делегируется родительскому кликабельному View.
static const QSet<QString> knownLabels = {
"Финансы", "Платежи", "Выгода", "Чат", "Ещё"
};
for (const Node &node: m_nodes) {
if (node.className == "android.widget.TextView"
&& !node.text.trimmed().isEmpty()
&& node.y() > 0
&& node.y() < screenHeight / 5
&& !knownLabels.contains(node.text.trimmed())) {
return node;
}
}
return {};
}
QString ScreenXmlParser::parseProfileFullName() {
// Полное имя — TextView ниже зоны заголовка, выше строки с телефоном.
// В profile.xml это единственный короткий нейтральный TextView в диапазоне y ≈ 400750.
static const QSet<QString> uiLabels = {
"Аккаунт", "Мои документы", "Паспорт и ИНН",
"Уровень счёта Лимиты и тарифы", "Повысить",
"Изменить код-пароль", "Изменить кодовое слово",
"Использовать данные биометрии", "Выйти",
"Оформите подписку",
"QR-код для пополнений Принимайте чаевые и донаты",
"Добавить Почта"
};
for (const Node &node: m_nodes) {
if (node.className == "android.widget.TextView"
&& !node.text.trimmed().isEmpty()
&& node.y() > 400
&& node.y() < 750
&& !uiLabels.contains(node.text.trimmed())) {
return node.text.trimmed();
}
}
return {};
}
Node ScreenXmlParser::findCardButtonOnHomeScreen() {
// Карточка на домашнем экране — Button, текст которого заканчивается на 4 цифры.
// Формат может быть "9865", "•• 9865", "**** 9865" и т.п.
static const QRegularExpression endsWithFourDigits(R"(\d{4}\s*$)");
for (const Node &node : m_nodes) {
if (node.className == "android.widget.Button"
&& node.clickable
&& endsWithFourDigits.match(node.text.trimmed()).hasMatch()) {
qDebug() << "[findCardButton] Found:" << node.text << "at" << node.x() << node.y();
return node;
}
}
// Диагностика: выводим все clickable Button'ы чтобы понять что есть на экране
qDebug() << "[findCardButton] Not found. All clickable Buttons:";
for (const Node &node : m_nodes) {
if (node.className == "android.widget.Button" && node.clickable) {
qDebug() << " text=" << node.text << "resId=" << node.resourceId;
}
}
return {};
}
Node ScreenXmlParser::findCardButtonByLastNumbers(const QString &lastNumbers) {
for (const Node &node : m_nodes) {
if (node.className != "android.widget.Button" || !node.clickable) continue;
if (node.text.trimmed().endsWith(lastNumbers)) {
qDebug() << "[findCardBtn] Found by lastNumbers" << lastNumbers
<< "text=" << node.text << "at" << node.x() << node.y();
return node;
}
}
qWarning() << "[findCardBtn] Not found for lastNumbers=" << lastNumbers
<< ". Clickable buttons:";
for (const Node &node : m_nodes) {
if (node.className == "android.widget.Button" && node.clickable)
qWarning() << " text=" << node.text;
}
return {};
}
QList<AccountInfo> ScreenXmlParser::parseCardsOnHomeScreen() {
// "Основной счёт X ₽" — общая кнопка с балансом для всех карт.
// Карты — отдельные кнопки с текстом из 4 цифр (могут быть любые non-breaking spaces).
// Сумма из "Основной счёт" проставляется на все карты.
static const QRegularExpression endsWithFourDigits(R"(\d{4}\s*$)");
// 1. Ищем кнопку "Основной счёт X ₽" — содержит "Основной" и "₽"
double commonAmount = 0.0;
for (const Node &node : m_nodes) {
if (node.className != "android.widget.Button") continue;
QString t = node.text;
t.replace(QChar(0x00A0), ' ').replace(QChar(0x202F), ' ');
if (!t.contains("Основной", Qt::CaseInsensitive) || !t.contains("")) continue;
qDebug() << "[parseCards] Found balance button, text:" << t;
const QRegularExpressionMatch m = QRegularExpression(
R"(([\d\s]+(?:[.,]\d+)?)\s*₽)"
).match(t);
if (m.hasMatch()) {
QString amtStr = m.captured(1).remove(' ').replace(',', '.');
commonAmount = amtStr.toDouble();
qDebug() << "[parseCards] Parsed amount:" << commonAmount;
break;
}
qWarning() << "[parseCards] Amount regex not matched in:" << t;
}
if (commonAmount == 0.0)
qWarning() << "[parseCards] Balance button not found or amount=0";
// 2. Собираем все кнопки карт (текст заканчивается на 4 цифры)
QList<AccountInfo> cards;
for (const Node &node : m_nodes) {
if (node.className != "android.widget.Button" || !node.clickable) continue;
QString t = node.text.trimmed();
if (!endsWithFourDigits.match(t).hasMatch()) continue;
AccountInfo acc;
acc.lastNumbers = t.right(4);
acc.amount = commonAmount;
acc.currency = "RUB";
acc.description = "Виртуальная карта";
qDebug() << "[parseCards] Card found: lastNumbers=" << acc.lastNumbers
<< "text=" << node.text << "amount=" << commonAmount;
cards.append(acc);
}
qDebug() << "[parseCards] Total cards found:" << cards.size();
return cards;
}
QString ScreenXmlParser::parseProfilePhone() {
// Телефон — Button с текстом вида "+7 953 803 83 18 Номер телефона"
for (const Node &node: m_nodes) {
if (node.text.contains("Номер телефона")) {
const int idx = node.text.indexOf("Номер телефона");
return node.text.left(idx).trimmed();
}
}
return {};
}
QList<Node> ScreenXmlParser::findPinCodeNodes(const QString &pinCode) {
// Ozon PIN кнопки — анонимные FrameLayout без text/content-desc.
// Алгоритм не зависит от разрешения: ищем строки из ровно 3 кликабельных узлов.
@ -1035,6 +1210,19 @@ QList<QPair<AccountInfo, Node> > ScreenXmlParser::parseAccountsInfo() {
return accounts;
}
QString ScreenXmlParser::findFullCardNumber() {
static const QRegularExpression cardNumberRegex(R"(^\d{4}\s\d{4}\s\d{4}\s\d{4}$)");
for (const Node &node : m_nodes) {
if (node.className == "android.widget.TextView") {
const QString text = node.text.trimmed();
if (cardNumberRegex.match(text).hasMatch()) {
return QString(text).remove(QChar(' '));
}
}
}
return {};
}
void ScreenXmlParser::test() {
// TransactionInfo info2 = parseExpandedInfo(m_xml);
// qDebug().noquote().nospace() << convertTransactionToString(info2);

View File

@ -34,6 +34,25 @@ public:
bool isHomeScreen();
Node tryToFindAdBanner();
// Returns a Button node whose text is exactly 4 digits (card last numbers)
Node findCardButtonOnHomeScreen();
// Finds a card button whose text ends with the given last 4 digits
Node findCardButtonByLastNumbers(const QString &lastNumbers);
// Returns all cards visible on the home screen as AccountInfo stubs
// (lastNumbers, description, amount filled; id=-1, cardNumber empty)
QList<AccountInfo> parseCardsOnHomeScreen();
bool isProfileScreen();
Node findUserNameOnHomeScreen(int screenHeight);
QString parseProfileFullName();
QString parseProfilePhone();
QList<Node> tryToFindPinCode(const QString &pinCode, const QString &title);
@ -51,6 +70,9 @@ public:
QList<QPair<AccountInfo, Node> > parseAccountsInfo();
// Finds a TextView with full card number (format "XXXX XXXX XXXX XXXX")
QString findFullCardNumber();
void test();
private:

BIN
assets/ozon/card_screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

119
assets/ozon/card_screen.xml Normal file
View File

@ -0,0 +1,119 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="0" hint="">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/action_bar_root"
class="android.widget.LinearLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout"
package="ru.ozon.fintech.finance" content-desc="" checkable="false" checked="false"
clickable="false" enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]"
drawing-order="2" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/main_activity_top"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="1" text="" resource-id="ru.ozon.fintech.finance:id/over_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2400]" drawing-order="4" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/over_fragment_top"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1"
hint=""/>
</node>
<node index="3" text="" resource-id="" class="android.view.ViewGroup"
package="ru.ozon.fintech.finance" content-desc="" checkable="false" checked="false"
clickable="false" enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]"
drawing-order="3" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="2"
hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/web_view_top"
class="android.view.ViewGroup" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/web_view_holder"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="2" hint="">
<node index="0" text="" resource-id=""
class="android.widget.FrameLayout"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="1" hint="">
<node NAF="true" index="0" text="" resource-id=""
class="android.webkit.WebView"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="true"
enabled="true" focusable="true" focused="true"
scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]" drawing-order="1"
hint=""/>
</node>
</node>
<node index="1" text=""
resource-id="ru.ozon.fintech.finance:id/web_view_toolbar"
class="android.view.ViewGroup" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,147]" drawing-order="1" hint=""/>
</node>
</node>
</node>
<node index="1" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_bottom_navigation_holder"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,2201][1080,2400]" drawing-order="3"
hint=""/>
</node>
</node>
</node>
</node>
<node index="1" text="" resource-id="" class="android.view.View" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[818,0][1080,262]" drawing-order="2" hint=""/>
</node>
</node>
</node>
</hierarchy>

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@ -0,0 +1,821 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="0" hint="">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/snacks_holder_window_top"
class="android.widget.LinearLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="3" hint=""/>
<node index="1" text="" resource-id="ru.ozon.fintech.finance:id/action_bar_root"
class="android.widget.LinearLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout"
package="ru.ozon.fintech.finance" content-desc="" checkable="false" checked="false"
clickable="false" enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]"
drawing-order="2" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/main_activity_top"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="1" text="" resource-id="ru.ozon.fintech.finance:id/over_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2400]" drawing-order="4" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/over_fragment_top"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1"
hint=""/>
</node>
<node index="3" text="" resource-id="" class="android.view.ViewGroup"
package="ru.ozon.fintech.finance" content-desc="" checkable="false" checked="false"
clickable="false" enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]"
drawing-order="3" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="2"
hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/web_view_top"
class="android.view.ViewGroup" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/web_view_holder"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="2" hint="">
<node index="0" text="" resource-id=""
class="android.widget.FrameLayout"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="1" hint="">
<node index="0" text="" resource-id=""
class="android.webkit.WebView"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]" drawing-order="0"
hint="">
<node index="0" text="Карта" resource-id=""
class="android.webkit.WebView"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="true" focused="true"
scrollable="true" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="0" hint="">
<node index="0" text="" resource-id="svelte"
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,241]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="base-go-back-button"
checkable="false" checked="false"
clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[28,144][118,231]"
drawing-order="0" hint=""/>
<node index="1" text="Карта" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[477,160][601,215]"
drawing-order="0" hint=""/>
<node index="2" text="" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="native-toolbar-close-button"
checkable="false" checked="false"
clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[1036,186][1080,241]"
drawing-order="0" hint=""/>
</node>
<node index="1" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,238][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="Основной счёт 1 251,50₽"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[378,296][766,341]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[0,364][1080,808]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[0,385][1080,756]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[249,385][1080,756]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[249,385][829,756]"
drawing-order="0" hint="">
<node index="0"
text="Bank Card Виртуальная карта •• 7919"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[249,385][829,756]"
drawing-order="0"
hint=""/>
</node>
<node index="1" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[826,385][1080,756]"
drawing-order="0" hint="">
<node index="0"
text="Bank Card Виртуальная карта •• 9865"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[897,430][1080,711]"
drawing-order="0"
hint=""/>
</node>
</node>
</node>
</node>
<node index="2"
text="Оплачивайте этой картой вне Ozon с кешбэком до 25%"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,805][1039,976]"
drawing-order="0" hint=""/>
<node index="3" text="Пополнить" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,994][375,1215]"
drawing-order="0" hint=""/>
<node index="4" text="Перевести" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[372,994][706,1215]"
drawing-order="0" hint=""/>
<node index="5" text="Блокировать"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[703,994][1039,1215]"
drawing-order="0" hint=""/>
<node index="6" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,1210][1039,1446]"
drawing-order="0" hint="">
<node index="0"
text="Добавить в MIR Pay Ozon Карта в мобильном телефоне"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,1231][1039,1446]"
drawing-order="0" hint=""/>
</node>
<node index="7"
text="Переводы за рубеж Через сервис «Золотая корона»"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,1462][1039,1677]"
drawing-order="0" hint=""/>
<node index="8" text="Реквизиты карты"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,1735][832,1824]"
drawing-order="0" hint=""/>
<node index="9" text="Показать" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[826,1735][1039,1824]"
drawing-order="0" hint=""/>
<node index="10" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,1861][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,1861][1039,2337]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id="svelte"
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,1861][1039,2337]"
drawing-order="0" hint="">
<node index="0"
text="•••• •••• •••• 7919"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[136,1945][984,2000]"
drawing-order="0" hint=""/>
<node index="1" text="••/••"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[136,2081][530,2139]"
drawing-order="0" hint=""/>
<node index="2" text="•••"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[593,2081][984,2139]"
drawing-order="0" hint=""/>
<node index="3" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[91,2186][987,2307]"
drawing-order="0" hint="">
<node index="0"
text="Если нужно указать имя"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[91,2186][987,2307]"
drawing-order="0"
hint=""/>
</node>
<node index="4" text=""
resource-id="svelte-announcer"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,1861][47,1866]"
drawing-order="0" hint=""/>
</node>
</node>
</node>
<node index="11" text="Настройки карты"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint=""/>
<node index="12"
text="MIR Pay Ozon Карта в мобильном телефоне"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint=""/>
<node index="13" text="Установить PIN-код"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint=""/>
<node index="14" text="Перевыпустить карту"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint=""/>
<node index="15" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="Лимиты по карте"
checkable="false" checked="false"
clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="Лимиты по карте"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[84,2337][892,2337]"
drawing-order="0" hint=""/>
</node>
</node>
<node index="16" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="Тариф" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="Тариф"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[84,2337][924,2337]"
drawing-order="0" hint=""/>
</node>
</node>
<node index="17" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="Получать зарплату на Ozon Карту"
checkable="false" checked="false"
clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0"
text="Получать зарплату на Ozon Карту"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[84,2337][924,2337]"
drawing-order="0" hint=""/>
</node>
</node>
</node>
<node index="2" text=""
resource-id="modal-container"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,2337][1080,2337]"
drawing-order="0" hint=""/>
<node index="3" text="Карта"
resource-id="svelte-announcer"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][5,5]"
drawing-order="0" hint=""/>
</node>
<node index="1" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
</node>
<node index="2" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
<node index="1" text=""
resource-id="ru.ozon.fintech.finance:id/web_view_toolbar"
class="android.view.ViewGroup" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,147]" drawing-order="1" hint=""/>
</node>
</node>
</node>
<node index="1" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_bottom_navigation_holder"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,2201][1080,2400]" drawing-order="3"
hint=""/>
</node>
</node>
</node>
</node>
<node index="2" text="" resource-id="" class="android.view.View" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[818,0][1080,262]" drawing-order="2" hint=""/>
</node>
</node>
</node>
</hierarchy>

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@ -0,0 +1,872 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="0" hint="">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/snacks_holder_window_top"
class="android.widget.LinearLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="3" hint=""/>
<node index="1" text="" resource-id="ru.ozon.fintech.finance:id/action_bar_root"
class="android.widget.LinearLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout"
package="ru.ozon.fintech.finance" content-desc="" checkable="false" checked="false"
clickable="false" enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]"
drawing-order="2" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/main_activity_top"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="1" text="" resource-id="ru.ozon.fintech.finance:id/over_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2400]" drawing-order="4" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/over_fragment_top"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="1"
hint=""/>
</node>
<node index="3" text="" resource-id="" class="android.view.ViewGroup"
package="ru.ozon.fintech.finance" content-desc="" checkable="false" checked="false"
clickable="false" enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false" bounds="[0,0][1080,2400]"
drawing-order="3" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,0][1080,2400]" drawing-order="2"
hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_host_fragment"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/web_view_top"
class="android.view.ViewGroup" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2400]" drawing-order="1" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/web_view_holder"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="2" hint="">
<node index="0" text="" resource-id=""
class="android.widget.FrameLayout"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="1" hint="">
<node index="0" text="" resource-id=""
class="android.webkit.WebView"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]" drawing-order="0"
hint="">
<node index="0" text="Карта" resource-id=""
class="android.webkit.WebView"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="true" focused="false"
scrollable="true" long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[0,0][1080,2337]" drawing-order="0" hint="">
<node index="0" text="" resource-id="svelte"
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,244]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="base-go-back-button"
checkable="false" checked="false"
clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[28,144][118,233]"
drawing-order="0" hint=""/>
<node index="1" text="Карта" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[477,160][601,217]"
drawing-order="0" hint=""/>
<node index="2" text="" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="native-toolbar-close-button"
checkable="false" checked="false"
clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[1036,186][1080,244]"
drawing-order="0" hint=""/>
</node>
<node index="1" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,52][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="Основной счёт 1 251,50₽"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[378,110][766,154]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[0,178][1080,622]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[0,199][1080,569]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[249,199][1080,569]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[249,199][829,569]"
drawing-order="0" hint="">
<node index="0"
text="Bank Card Виртуальная карта •• 7919"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[249,199][829,569]"
drawing-order="0"
hint=""/>
</node>
<node index="1" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[826,199][1080,569]"
drawing-order="0" hint="">
<node index="0"
text="Bank Card Виртуальная карта •• 9865"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[897,244][1080,525]"
drawing-order="0"
hint=""/>
</node>
</node>
</node>
</node>
<node index="2"
text="Оплачивайте этой картой вне Ozon с кешбэком до 25%"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,619][1039,790]"
drawing-order="0" hint=""/>
<node index="3" text="Пополнить" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,808][375,1029]"
drawing-order="0" hint=""/>
<node index="4" text="Перевести" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[372,808][706,1029]"
drawing-order="0" hint=""/>
<node index="5" text="Блокировать"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[703,808][1039,1029]"
drawing-order="0" hint=""/>
<node index="6" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,1023][1039,1260]"
drawing-order="0" hint="">
<node index="0"
text="Добавить в MIR Pay Ozon Карта в мобильном телефоне"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,1044][1039,1260]"
drawing-order="0" hint=""/>
</node>
<node index="7"
text="Переводы за рубеж Через сервис «Золотая корона»"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,1275][1039,1491]"
drawing-order="0" hint=""/>
<node index="8" text="Реквизиты карты"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,1548][861,1638]"
drawing-order="0" hint=""/>
<node index="9" text="Скрыть" resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="true" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[858,1548][1039,1638]"
drawing-order="0" hint=""/>
<node index="10" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,1674][1039,2170]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,1674][1039,2173]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id="svelte"
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,1674][1039,2173]"
drawing-order="0" hint="">
<node index="0"
text="2204 3210 3520 7919"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[136,1758][884,1813]"
drawing-order="0" hint=""/>
<node NAF="true" index="1" text=""
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[903,1753][968,1819]"
drawing-order="0" hint=""/>
<node index="2" text="02/36"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[136,1895][430,1953]"
drawing-order="0" hint=""/>
<node NAF="true" index="3" text=""
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[446,1890][514,1955]"
drawing-order="0" hint=""/>
<node index="4" text="914"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[593,1895][884,1953]"
drawing-order="0" hint=""/>
<node NAF="true" index="5" text=""
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[903,1890][968,1955]"
drawing-order="0" hint=""/>
<node index="6" text=""
resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[91,2000][987,2121]"
drawing-order="0" hint="">
<node index="0"
text="Если нужно указать имя"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[91,2000][987,2121]"
drawing-order="0"
hint=""/>
</node>
<node index="7" text=""
resource-id="svelte-announcer"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc=""
checkable="false"
checked="false"
clickable="false"
enabled="true"
focusable="false"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,1674][47,1680]"
drawing-order="0" hint=""/>
</node>
</node>
</node>
<node index="11" text="Настройки карты"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,2252][1039,2317]"
drawing-order="0" hint=""/>
<node index="12"
text="MIR Pay Ozon Карта в мобильном телефоне"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint=""/>
<node index="13" text="Установить PIN-код"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint=""/>
<node index="14" text="Перевыпустить карту"
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint=""/>
<node index="15" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="Лимиты по карте"
checkable="false" checked="false"
clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="Лимиты по карте"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[84,2337][892,2337]"
drawing-order="0" hint=""/>
</node>
</node>
<node index="16" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="Тариф" checkable="false"
checked="false" clickable="true"
enabled="true" focusable="true"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="Тариф"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[84,2337][924,2337]"
drawing-order="0" hint=""/>
</node>
</node>
<node index="17" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="Получать зарплату на Ozon Карту"
checkable="false" checked="false"
clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[42,2337][1039,2337]"
drawing-order="0" hint="">
<node index="0"
text="Получать зарплату на Ozon Карту"
resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false"
password="false" selected="false"
bounds="[84,2337][924,2337]"
drawing-order="0" hint=""/>
</node>
</node>
</node>
<node index="2" text=""
resource-id="modal-container"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,2337][1080,2337]"
drawing-order="0" hint=""/>
<node index="3" text="Карта"
resource-id="svelte-announcer"
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][0,0]"
drawing-order="0" hint=""/>
</node>
<node index="1" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
</node>
<node index="2" text="" resource-id=""
class="android.view.View"
package="ru.ozon.fintech.finance" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
<node index="1" text="" resource-id=""
class="android.widget.TextView"
package="ru.ozon.fintech.finance"
content-desc="" checkable="false"
checked="false" clickable="false"
enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[0,0][1080,2337]"
drawing-order="0" hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
<node index="1" text=""
resource-id="ru.ozon.fintech.finance:id/web_view_toolbar"
class="android.view.ViewGroup" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,0][1080,147]" drawing-order="1" hint=""/>
</node>
</node>
</node>
<node index="1" text=""
resource-id="ru.ozon.fintech.finance:id/main_activity_bottom_navigation_holder"
class="android.widget.FrameLayout" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[0,2201][1080,2400]" drawing-order="3"
hint=""/>
</node>
</node>
</node>
</node>
<node index="2" text="" resource-id="" class="android.view.View" package="ru.ozon.fintech.finance"
content-desc="" checkable="false" checked="false" clickable="false" enabled="true"
focusable="false" focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[818,0][1080,262]" drawing-order="2" hint=""/>
</node>
</node>
</node>
</hierarchy>

BIN
assets/ozon/home_page_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

1552
assets/ozon/home_page_2.xml Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

File diff suppressed because it is too large Load Diff

BIN
assets/ozon/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

1
assets/ozon/profile.xml Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,18 +1,14 @@
[apps]
list=ozon
[birbank]
android_package_name=az.kapitalbank.mbanking
name=Birbank
[common]
access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcxODU0MzI0fQ.xLalNxKuPK1QV3D94Je1OJITBV-VfIQJG6iSwPa_mMg
access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyMjQ4MzM1fQ.gDA57WCWCCwdrxa84McLJLcIyV4RYLRQ7s9YT3AcFd4
api_key=0eee77b62013253e02e1a69bc6a505bb37757fb7
currencies=USD, KRW, AZN, RUB
desktop_id=6554b2a2-2885-430e-aae2-0045f82f068e
desktop_id=b86eac8e-dc89-4a9e-a5e8-7f85db011391
profile_id=c4552156-d179-4a79-9e91-587038c95cf0
refresh_token=d22175aa-96ec-405d-8c9f-f6cfce1a0913
token_expires_at=1771854324
refresh_token=2cdf2bd7-9d5f-463f-98f4-fce79a9ee9a4
token_expires_at=1772248335
version=0.0.1
[db]
@ -24,22 +20,23 @@ apiBase=http://93.183.75.134:8005
appInfoUpdate=http://localhost:9999/api/v1/device/update
appStatusUpdate=http://localhost:9999/api/v1/application/update
eventStatusUpdate=http://localhost:9999/api/v1/event/update
pathAuthLogin=/api/v1/auth/login
pathAuthRefresh=/api/v1/auth/refresh_token
pathBankProfile=/api/v1/profile/bank_profile
pathDesktopCreate=/api/v1/desktop/
pathDevice=/api/v1/device/
pathDeviceScreenshot=/api/v1/device/add_screenshot/
pathMaterial=/api/v1/profile/material
paymentsGet=http://localhost:9999/api/v1/payments
transactionInsert=http://localhost:9999/api/v1/transaction/insert
transactionUpdate=http://localhost:9999/api/v1/transaction/update
[ozon]
android_package_name=ru.ozon.fintech.finance
currency=RUB
name=Ozon
[rshb]
android_package_name=ru.rshb.dbo
currency=RUB
name=Росcельхозбанк
[toss]
android_package_name=viva.republica.toss
name=Toss
[ziraat]
android_package_name=com.ziraat.ziraatmobil
name=Ziraat

View File

@ -1,6 +1,7 @@
#include "DatabaseManager.h"
#include <QCoreApplication>
#include <QSqlError>
#include <QSqlQuery>
#include <QDebug>
#include <QDir>
#include <QSettings>
@ -65,6 +66,213 @@ QString DatabaseManager::connectionName() {
}
static bool columnExists(QSqlQuery &q, const QString &table, const QString &column) {
q.exec(QString("SELECT COUNT(*) FROM pragma_table_info('%1') WHERE name='%2'")
.arg(table, column));
return q.next() && q.value(0).toInt() > 0;
}
void DatabaseManager::initSchema() {
QSqlDatabase db = database();
QSqlQuery q(db);
// Читаем текущую версию схемы
q.exec("PRAGMA user_version");
const int version = q.next() ? q.value(0).toInt() : 0;
// ── Версия 1: начальная схема + api_id для devices ────────────────────────
if (version < 1) {
db.transaction();
q.exec(R"(CREATE TABLE IF NOT EXISTS devices (
id TEXT PRIMARY KEY,
api_id TEXT,
status TEXT,
name TEXT,
android TEXT,
screen_width INTEGER,
screen_height INTEGER,
battery INTEGER,
update_time DATETIME,
timestamp DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
image BLOB
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
pin_code TEXT,
pin_code_checked TEXT DEFAULT 'no',
name TEXT,
code TEXT,
package TEXT,
status TEXT DEFAULT 'off',
install TEXT DEFAULT 'no',
comment TEXT,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, code)
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
app_code TEXT,
last_numbers TEXT,
description TEXT,
amount REAL,
currency TEXT DEFAULT '',
update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
status TEXT DEFAULT 'active',
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, app_code, last_numbers)
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
external_id INTEGER,
amount REAL,
fee REAL,
status TEXT,
type TEXT,
phone TEXT,
name TEXT,
short_description TEXT,
updated_short_description TEXT,
description TEXT,
updated_description TEXT,
bank_name TEXT,
bank_time TEXT,
bank_tr_external_id TEXT,
update_time DATETIME,
complete_time DATETIME,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE
))");
q.exec(R"(CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
external_id INTEGER,
device_id TEXT,
amount REAL,
phone TEXT,
bank_name TEXT,
comment TEXT,
status TEXT,
type TEXT,
update_time DATETIME,
complete_time DATETIME,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (external_id)
))");
// Добавляем api_id в devices если ещё нет (для существующих БД)
if (!columnExists(q, "devices", "api_id")) {
q.exec("ALTER TABLE devices ADD COLUMN api_id TEXT");
}
db.commit();
q.exec("PRAGMA user_version = 1");
qDebug() << "[DB] Migration 1 applied";
}
// ── Версия 2: currency в accounts + переименование app_name → app_code ───
if (version < 2) {
db.transaction();
// Добавляем currency если ещё нет (существующие БД без этого поля)
if (!columnExists(q, "accounts", "currency")) {
q.exec("ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT ''");
}
// Переименовываем app_name → app_code (SQLite 3.25+)
if (columnExists(q, "accounts", "app_name")) {
q.exec("ALTER TABLE accounts RENAME COLUMN app_name TO app_code");
if (q.lastError().isValid()) {
qWarning() << "[DB] Migration 2: rename app_name failed:" << q.lastError().text();
}
}
db.commit();
q.exec("PRAGMA user_version = 2");
qDebug() << "[DB] Migration 2 applied";
}
// ── Версия 3: phone и full_name в applications ────────────────────────────
if (version < 3) {
db.transaction();
if (!columnExists(q, "applications", "phone")) {
q.exec("ALTER TABLE applications ADD COLUMN phone TEXT DEFAULT ''");
}
if (!columnExists(q, "applications", "full_name")) {
q.exec("ALTER TABLE applications ADD COLUMN full_name TEXT DEFAULT ''");
}
db.commit();
q.exec("PRAGMA user_version = 3");
qDebug() << "[DB] Migration 3 applied";
}
// ── Версия 4: card_number в accounts ──────────────────────────────────────
if (version < 4) {
db.transaction();
if (!columnExists(q, "accounts", "card_number")) {
q.exec("ALTER TABLE accounts ADD COLUMN card_number TEXT DEFAULT ''");
}
db.commit();
q.exec("PRAGMA user_version = 4");
qDebug() << "[DB] Migration 4 applied";
}
// ── Версия 5: bank_profile_id в applications ──────────────────────────────
if (version < 5) {
db.transaction();
if (!columnExists(q, "applications", "bank_profile_id")) {
q.exec("ALTER TABLE applications ADD COLUMN bank_profile_id TEXT DEFAULT ''");
}
db.commit();
q.exec("PRAGMA user_version = 5");
qDebug() << "[DB] Migration 5 applied";
}
// ── Версия 6: currency и amount в applications ─────────────────────────────
if (version < 6) {
db.transaction();
if (!columnExists(q, "applications", "currency")) {
q.exec("ALTER TABLE applications ADD COLUMN currency TEXT DEFAULT ''");
}
if (!columnExists(q, "applications", "amount")) {
q.exec("ALTER TABLE applications ADD COLUMN amount REAL DEFAULT 0");
}
db.commit();
q.exec("PRAGMA user_version = 6");
qDebug() << "[DB] Migration 6 applied";
}
// ── Версия 7: material_id в accounts ──────────────────────────────────────
if (version < 7) {
db.transaction();
if (!columnExists(q, "accounts", "material_id")) {
q.exec("ALTER TABLE accounts ADD COLUMN material_id TEXT DEFAULT ''");
}
db.commit();
q.exec("PRAGMA user_version = 7");
qDebug() << "[DB] Migration 7 applied";
}
}
void DatabaseManager::closeConnection() {
QMutexLocker locker(&mutex);
if (const QString name = connectionName(); connectionPool.contains(name)) {

View File

@ -11,6 +11,7 @@ public:
static DatabaseManager& instance();
QSqlDatabase database();
void initSchema();
void closeConnection();
private:

Binary file not shown.

View File

@ -10,9 +10,9 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
INSERT INTO accounts (device_id, app_name, last_numbers, amount, update_time, status, description, currency)
VALUES (:device_id, :app_name, :last_numbers, :amount, :update_time, :status, :description, :currency)
ON CONFLICT(device_id, app_name, last_numbers)
INSERT INTO accounts (device_id, app_code, last_numbers, amount, update_time, status, description, currency)
VALUES (:device_id, :app_code, :last_numbers, :amount, :update_time, :status, :description, :currency)
ON CONFLICT(device_id, app_code, last_numbers)
DO UPDATE SET
amount = excluded.amount,
update_time = excluded.update_time,
@ -23,7 +23,7 @@ bool AccountDAO::upsertAccount(const AccountInfo &info) {
)");
query.bindValue(":device_id", info.deviceId);
query.bindValue(":app_name", info.appCode);
query.bindValue(":app_code", info.appCode);
query.bindValue(":last_numbers", info.lastNumbers);
query.bindValue(":currency", info.currency);
query.bindValue(":description", info.description);
@ -77,12 +77,40 @@ bool AccountDAO::deleteAccount(const int id) {
return query.exec();
}
bool AccountDAO::updateCardNumber(const int accountId, const QString &cardNumber) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE accounts SET card_number = :card_number WHERE id = :id
)");
query.bindValue(":card_number", cardNumber);
query.bindValue(":id", accountId);
if (!query.exec()) {
qWarning() << "Ошибка при обновлении card_number:" << query.lastError().text();
return false;
}
return true;
}
bool AccountDAO::updateMaterialId(const int accountId, const QString &materialId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE accounts SET material_id = :material_id WHERE id = :id");
query.bindValue(":material_id", materialId);
query.bindValue(":id", accountId);
if (!query.exec()) {
qWarning() << "Ошибка при обновлении material_id:" << query.lastError().text();
return false;
}
return true;
}
AccountInfo parseAccount(const QSqlQuery &query) {
AccountInfo acc;
acc.id = query.value("id").toInt();
acc.deviceId = query.value("device_id").toString();
acc.appCode = query.value("app_name").toString();
acc.appCode = query.value("app_code").toString();
acc.lastNumbers = query.value("last_numbers").toString();
acc.cardNumber = query.value("card_number").toString();
acc.materialId = query.value("material_id").toString();
acc.amount = query.value("amount").toDouble();
acc.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());
acc.status = accountStatusFromString(query.value("status").toString());
@ -96,13 +124,13 @@ QList<AccountInfo> AccountDAO::getAccounts(const QString &deviceId, const QStrin
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, app_name, last_numbers, amount, update_time, status, description, currency
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency
FROM accounts
WHERE device_id = :device_id AND app_name = :app_name
WHERE device_id = :device_id AND app_code = :app_code
)");
query.bindValue(":device_id", deviceId);
query.bindValue(":app_name", appName);
query.bindValue(":app_code", appName);
if (!query.exec()) {
qWarning() << "Ошибка при получении accounts:" << query.lastError().text();
@ -120,7 +148,7 @@ AccountInfo AccountDAO::getAccountById(const int accountId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, app_name, last_numbers, amount, update_time, status, description, currency
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency
FROM accounts
WHERE id = :id
LIMIT 1
@ -143,9 +171,9 @@ AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &a
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, app_name, last_numbers, amount, update_time, status, description, currency
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency
FROM accounts
WHERE app_name = :appName AND last_numbers = :lastNumber AND device_id = :deviceId
WHERE app_code = :appName AND last_numbers = :lastNumber AND device_id = :deviceId
LIMIT 1
)");

View File

@ -14,6 +14,10 @@ public:
[[nodiscard]] static bool deleteAccount(int id);
[[nodiscard]] static bool updateCardNumber(int accountId, const QString &cardNumber);
[[nodiscard]] static bool updateMaterialId(int accountId, const QString &materialId);
[[nodiscard]] static QList<AccountInfo> getAccounts(const QString &deviceId, const QString &appName);
[[nodiscard]] static AccountInfo getAccountById(int accountId);

View File

@ -22,7 +22,7 @@ bool ApplicationDAO::upsertApplication(const ApplicationInfo &info) {
status = excluded.status,
comment = excluded.comment,
install = excluded.install,
pin_code_checked = excluded.pin_code_checked,
pin_code_checked = excluded.pin_code_checked
)");
query.bindValue(":id", info.id);
@ -127,6 +127,11 @@ ApplicationInfo parseApplication(const QSqlQuery &query) {
app.comment = query.value("comment").toString();
app.install = query.value("install").toString() == "yes";
app.pinCodeChecked = query.value("pin_code_checked").toString() == "yes";
app.phone = query.value("phone").toString();
app.fullName = query.value("full_name").toString();
app.bankProfileId = query.value("bank_profile_id").toString();
app.currency = query.value("currency").toString();
app.amount = query.value("amount").toDouble();
return app;
}
@ -134,7 +139,7 @@ ApplicationInfo ApplicationDAO::getApplication(const QString &deviceId, const QS
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, full_name, bank_profile_id, currency, amount
FROM applications
WHERE device_id = :device_id AND code = :code
LIMIT 1
@ -159,7 +164,7 @@ QList<ApplicationInfo> ApplicationDAO::getApplicationsByDeviceId(const QString &
QList<ApplicationInfo> list;
query.prepare(R"(
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, full_name, bank_profile_id, currency, amount
FROM applications
WHERE device_id = :device_id AND install = 'yes'
)");
@ -177,6 +182,82 @@ QList<ApplicationInfo> ApplicationDAO::getApplicationsByDeviceId(const QString &
return list;
}
bool ApplicationDAO::updateProfileInfo(
const int &appId,
const QString &fullName,
const QString &phone
) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE applications
SET full_name = :full_name, phone = :phone
WHERE id = :id
)");
query.bindValue(":id", appId);
query.bindValue(":full_name", fullName);
query.bindValue(":phone", phone);
if (!query.exec()) {
qWarning() << "Ошибка при обновлении профиля приложения:" << query.lastError().text();
return false;
}
return true;
}
bool ApplicationDAO::updateAmount(const int appId, const double amount) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE applications SET amount = :amount WHERE id = :id");
query.bindValue(":amount", amount);
query.bindValue(":id", appId);
if (!query.exec()) {
qWarning() << "Ошибка при обновлении amount в application:" << query.lastError().text();
return false;
}
return true;
}
bool ApplicationDAO::updateBankProfileId(const int appId, const QString &bankProfileId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE applications SET bank_profile_id = :bank_profile_id WHERE id = :id");
query.bindValue(":bank_profile_id", bankProfileId);
query.bindValue(":id", appId);
if (!query.exec()) {
qWarning() << "Ошибка при обновлении bank_profile_id:" << query.lastError().text();
return false;
}
return true;
}
bool ApplicationDAO::activateAppsForDevice(const QString &deviceId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE applications SET status = 'active'
WHERE device_id = :device_id AND install = 'yes' AND pin_code_checked = 'yes'
)");
query.bindValue(":device_id", deviceId);
if (!query.exec()) {
qWarning() << "Ошибка при активации приложений:" << query.lastError().text();
return false;
}
return true;
}
bool ApplicationDAO::deactivateAppsForDevice(const QString &deviceId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE applications SET status = 'off'
WHERE device_id = :device_id AND install = 'yes'
)");
query.bindValue(":device_id", deviceId);
if (!query.exec()) {
qWarning() << "Ошибка при деактивации приложений:" << query.lastError().text();
return false;
}
return true;
}
bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet<QString> &apps) {
QSqlQuery query(DatabaseManager::instance().database());
const QSettings settings("config.ini", QSettings::IniFormat);
@ -201,12 +282,15 @@ bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet<QStr
QString package = settings.value(bank + "/android_package_name").toString();
QString name = settings.value(bank + "/name").toString();
const QString currency = settings.value(bank + "/currency").toString();
query.prepare(R"(
INSERT INTO applications (device_id, name, code, package, status, install, pin_code_checked)
VALUES (:device_id, :name, :code, :package, :status, :install, :pin_code_checked)
INSERT INTO applications (device_id, name, code, package, status, install, pin_code_checked, currency)
VALUES (:device_id, :name, :code, :package, :status, :install, :pin_code_checked, :currency)
ON CONFLICT(device_id, code)
DO UPDATE SET
install = excluded.install
install = excluded.install,
currency = CASE WHEN excluded.currency != '' THEN excluded.currency ELSE currency END
)");
query.bindValue(":device_id", deviceId);
@ -216,6 +300,7 @@ bool ApplicationDAO::checkInstalledApps(const QString &deviceId, const QSet<QStr
query.bindValue(":status", "off");
query.bindValue(":pin_code_checked", "no");
query.bindValue(":install", apps.contains(package) ? "yes" : "no");
query.bindValue(":currency", currency);
if (!query.exec()) {
qWarning() << "Ошибка при проверке приложений:" << query.lastError().text();

View File

@ -21,9 +21,23 @@ public:
const QString &comment
);
[[nodiscard]] static bool updateProfileInfo(
const int &appId,
const QString &fullName,
const QString &phone
);
[[nodiscard]] static bool updateBankProfileId(int appId, const QString &bankProfileId);
[[nodiscard]] static bool updateAmount(int appId, double amount);
[[nodiscard]] static ApplicationInfo getApplication(const QString &deviceId, const QString &appCode);
[[nodiscard]] static QList<ApplicationInfo> getApplicationsByDeviceId(const QString &deviceId);
[[nodiscard]] static bool checkInstalledApps(const QString &deviceId, const QSet<QString> &apps);
[[nodiscard]] static bool activateAppsForDevice(const QString &deviceId);
[[nodiscard]] static bool deactivateAppsForDevice(const QString &deviceId);
};

View File

@ -20,12 +20,6 @@ DeviceInfo parseDevice(const QSqlQuery &query) {
return device;
}
void DeviceDAO::migrate() {
QSqlQuery query(DatabaseManager::instance().database());
// Добавляем колонку если её ещё нет (SQLite вернёт ошибку — игнорируем)
query.exec("ALTER TABLE devices ADD COLUMN api_id TEXT");
}
QList<DeviceInfo> DeviceDAO::getAllDevices(const bool online) {
QList<DeviceInfo> devices;
@ -33,10 +27,9 @@ QList<DeviceInfo> DeviceDAO::getAllDevices(const bool online) {
QString sql = R"(
SELECT id, api_id, status, name, android, screen_width, screen_height, battery, update_time, image
FROM devices
WHERE image IS NOT NULL
)";
if (online) {
sql += " AND status = 'device'";
sql += " WHERE status = 'device'";
}
query.prepare(sql);

View File

@ -4,8 +4,6 @@
class DeviceDAO {
public:
static void migrate();
[[nodiscard]] static DeviceInfo getDeviceById(const QString &deviceId);
[[nodiscard]] static bool upsertDevice(const DeviceInfo &device);

View File

@ -156,8 +156,8 @@ int main(int argc, char *argv[]) {
qInstallMessageHandler(messageHandler);
// Миграции БД (добавляем новые колонки если их ещё нет)
DeviceDAO::migrate();
// Миграции БД
DatabaseManager::instance().initSchema();
// 1. Если есть api_key — логинимся и проверяем/создаём desktop
// - ошибка сети или сервера [code -1]
@ -191,9 +191,35 @@ int main(int argc, char *argv[]) {
QObject::connect(loader, &LoaderWindow::loadingFinished, loader, [loader, &app, showRegistration](const int result) {
if (result == -1) {
auto *errorWindow = new ErrorWindow(
"Нет подключения к интернету или сервер не отвечает.\n\nПожалуйста, попробуйте позже."
"Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.",
true
);
errorWindow->show();
QObject::connect(errorWindow, &ErrorWindow::retryRequested, errorWindow, [errorWindow, &app, showRegistration]() {
errorWindow->close();
auto *newLoader = new LoaderWindow();
newLoader->show();
QObject::connect(newLoader, &LoaderWindow::loadingFinished, newLoader, [newLoader, &app, showRegistration](const int r) {
if (r == 1) {
auto *mainWindow = new MainWindow();
mainWindow->show();
startNetworkService(app);
startDeviceScreener(app);
startEventHandler(app);
} else if (r == 0) {
QSettings s("config.ini", QSettings::IniFormat);
s.remove("common/api_key");
showRegistration();
} else {
auto *err2 = new ErrorWindow(
"Сервер не отвечает.\n\nПроверьте подключение к интернету и попробуйте снова.",
false
);
err2->show();
}
newLoader->close();
}, Qt::QueuedConnection);
});
} else if (result == 1) {
auto *mainWindow = new MainWindow();
mainWindow->show();

View File

@ -1,3 +1,7 @@
-- ============================================================
-- Версия 1 (user_version = 1): начальная схема
-- ============================================================
CREATE TABLE IF NOT EXISTS devices
(
id TEXT PRIMARY KEY,
@ -18,29 +22,31 @@ CREATE TABLE IF NOT EXISTS applications
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
pin_code TEXT,
pin_code_checked TEXT,
pin_code_checked TEXT DEFAULT 'no',
name TEXT,
code TEXT,
package TEXT,
status TEXT,
install TEXT,
status TEXT DEFAULT 'off',
install TEXT DEFAULT 'no',
comment TEXT,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, code)
);
-- app_code: код банка ("ozon", "birbank", ...) — не путать с человекочитаемым name
CREATE TABLE IF NOT EXISTS accounts
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,
app_name TEXT,
app_code TEXT,
last_numbers TEXT,
description TEXT,
amount REAL,
currency TEXT DEFAULT '',
update_time DATETIME DEFAULT (strftime('%d.%m.%Y %H:%M:%S', 'now')),
status TEXT,
status TEXT DEFAULT 'active',
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, app_name, last_numbers)
UNIQUE (device_id, app_code, last_numbers)
);
CREATE TABLE IF NOT EXISTS transactions
@ -48,23 +54,19 @@ CREATE TABLE IF NOT EXISTS transactions
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
external_id INTEGER,
amount REAL,
fee REAL,
status TEXT,
type TEXT,
phone TEXT,
name TEXT,
short_description TEXT,
updated_short_description TEXT,
description TEXT,
updated_description TEXT,
bank_name TEXT,
bank_time TEXT,
bank_tr_external_id TEXT,
update_time DATETIME,
complete_time DATETIME,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
@ -77,19 +79,24 @@ CREATE TABLE IF NOT EXISTS events
account_id INTEGER,
external_id INTEGER,
device_id TEXT,
amount REAL,
phone TEXT,
bank_name TEXT,
comment TEXT,
status TEXT,
type TEXT,
update_time DATETIME,
complete_time DATETIME,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (account_id) REFERENCES accounts (id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (external_id)
);
);
-- ============================================================
-- Версия 2 (user_version = 2): currency + rename app_name→app_code
-- ============================================================
-- Для существующих БД (применяется автоматически при старте):
-- ALTER TABLE accounts ADD COLUMN currency TEXT DEFAULT '';
-- ALTER TABLE accounts RENAME COLUMN app_name TO app_code;

View File

@ -34,6 +34,8 @@ struct AccountInfo {
QString deviceId; // code
QString appCode;
QString lastNumbers;
QString cardNumber; // full 16-digit card number
QString materialId;
QString currency;
QString description;
double amount = 0.0;

View File

@ -15,5 +15,10 @@ struct ApplicationInfo {
QString package;
QString status;
QString comment;
QString phone;
QString fullName;
QString bankProfileId;
QString currency;
double amount = 0.0;
QDateTime updateTime;
};

View File

@ -174,6 +174,43 @@ QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) {
return {};
}
void DeviceScreener::postScreenshotToApi(const QString &apiId, const QByteArray &image) {
if (!m_manager) m_manager = new QNetworkAccessManager(this);
const QSettings settings("config.ini", QSettings::IniFormat);
const QString path = "/api/v1/device/add_screenshot/" + apiId;
QNetworkRequest request(QUrl(m_apiBase + path));
request.setRawHeader("Authorization",
("Bearer " + settings.value("common/access_token").toString()).toUtf8());
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"file\"; filename=\"screenshot.jpg\""));
filePart.setBody(image);
multiPart->append(filePart);
QNetworkReply *reply = m_manager->post(request, multiPart);
multiPart->setParent(reply);
QEventLoop loop;
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
loop.exec();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "postScreenshotToApi network error:" << reply->errorString();
} else {
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
if (doc.isObject() && doc.object()["success"].toBool()) {
qDebug() << "postScreenshotToApi done for device" << apiId;
} else {
qWarning() << "postScreenshotToApi failed for device" << apiId << ":" << doc.toJson(QJsonDocument::Compact);
}
}
reply->deleteLater();
}
void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) {
QJsonObject body;
body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE";
@ -197,16 +234,23 @@ void DeviceScreener::start() {
const bool isChecked = bankChecked.contains(id);
device = readDeviceInfo(id, isChecked);
device.image = takeScreenshot(id);
if (!DeviceDAO::updateImage(id, device.image)) {
qDebug() << "Cant update screenshot";
if (!device.image.isEmpty()) {
if (!DeviceDAO::updateImage(id, device.image)) {
qDebug() << "Cant update screenshot";
}
}
if (!isChecked) {
ApplicationDAO::activateAppsForDevice(id);
}
bankChecked[id] = true;
}
device.status = status;
device.updateTime = QDateTime::currentDateTime();
deviceInfos.append(device);
if (!DeviceDAO::upsertDevice(device)) {
qDebug() << "Cant update devive";
if (status == "device") {
if (!DeviceDAO::upsertDevice(device)) {
qDebug() << "Cant update device";
}
}
// Синхронизация с API
@ -224,6 +268,9 @@ void DeviceScreener::start() {
if (now - m_lastApiUpdate.value(device.id, 0) >= 60) {
device.apiId = saved.apiId;
updateDeviceOnApi(device);
if (!device.image.isEmpty()) {
postScreenshotToApi(saved.apiId, device.image);
}
m_lastApiUpdate[device.id] = now;
}
}
@ -236,6 +283,18 @@ void DeviceScreener::start() {
if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) {
qDebug() << "Cant mark devices offline";
}
// Деактивировать приложения для устройств, которые ушли в офлайн
QStringList offlineIds;
for (auto it = bankChecked.begin(); it != bankChecked.end();) {
if (!onlineIds.contains(it.key())) {
ApplicationDAO::deactivateAppsForDevice(it.key());
it = bankChecked.erase(it);
} else {
++it;
}
}
QThread::msleep(1000);
}
emit finished();

View File

@ -41,4 +41,5 @@ private:
QString createDeviceOnApi(const DeviceInfo &device);
void updateDeviceOnApi(const DeviceInfo &device);
void postScreenshotToApi(const QString &apiId, const QByteArray &image);
};

View File

@ -11,6 +11,7 @@
#include <QTimeZone>
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/EventDAO.h"
#include "dao/TransactionDAO.h"
@ -25,6 +26,13 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) {
m_urlTransactionInsert = settings.value("net/transactionInsert").toString();
m_urlEventStatusUpdate = settings.value("net/eventStatusUpdate").toString();
m_apiBase = settings.value("net/apiBase").toString();
m_pathAuthLogin = settings.value("net/pathAuthLogin", "/api/v1/auth/login").toString();
m_pathAuthRefresh = settings.value("net/pathAuthRefresh", "/api/v1/auth/refresh_token").toString();
m_pathDesktopCreate = settings.value("net/pathDesktopCreate", "/api/v1/desktop/").toString();
m_pathDevice = settings.value("net/pathDevice", "/api/v1/device/").toString();
m_pathDeviceScreenshot= settings.value("net/pathDeviceScreenshot","/api/v1/device/add_screenshot/").toString();
m_pathBankProfile = settings.value("net/pathBankProfile", "/api/v1/profile/bank_profile").toString();
m_pathMaterial = settings.value("net/pathMaterial", "/api/v1/profile/material").toString();
m_manager = new QNetworkAccessManager(this);
m_token = settings.value("common/token").toString();
m_accessToken = settings.value("common/access_token").toString();
@ -199,7 +207,9 @@ QJsonValue NetworkService::postJson(const QString &path, const QJsonObject &body
QJsonValue result;
if (reply->error() != QNetworkReply::NoError) {
qCritical() << "Network error:" << reply->errorString() << path;
const QByteArray responseBody = reply->readAll();
qCritical() << "Network error (POST):" << reply->errorString() << path
<< "\n response body:" << responseBody;
result = QJsonValue::Undefined;
} else {
const QByteArray raw = reply->readAll();
@ -226,7 +236,7 @@ void NetworkService::loginAndSetup() {
// Шаг 1: логин
QJsonObject loginBody;
loginBody["api_key"] = apiKey;
const QJsonValue loginResult = postJson(m_apiBase + "/api/v1/auth/login", loginBody);
const QJsonValue loginResult = postJson(m_apiBase + m_pathAuthLogin, loginBody);
if (loginResult.isUndefined()) {
emit finishedWithResult(-1); // ошибка сети
@ -260,7 +270,7 @@ void NetworkService::loginAndSetup() {
desktopBody["name"] = QJsonValue::Null;
desktopBody["contact"] = QJsonValue::Null;
desktopBody["app_version"] = settings.value("common/version").toString();
const QJsonValue desktopResult = postJson(m_apiBase + "/api/v1/desktop/", desktopBody, true);
const QJsonValue desktopResult = postJson(m_apiBase + m_pathDesktopCreate, desktopBody, true);
if (desktopResult.isUndefined() || desktopResult.isNull()) {
emit finishedWithResult(-1);
@ -340,7 +350,9 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod
QJsonValue result;
if (reply->error() != QNetworkReply::NoError) {
qCritical() << "Network error (PATCH):" << reply->errorString() << path;
const QByteArray responseBody = reply->readAll();
qCritical() << "Network error (PATCH):" << reply->errorString() << path
<< "\n response body:" << responseBody;
} else {
const QByteArray raw = reply->readAll();
QJsonParseError err;
@ -358,6 +370,112 @@ QJsonValue NetworkService::patchJson(const QString &path, const QJsonObject &bod
return result;
}
void NetworkService::postBankProfile() {
if (!ensureValidToken()) { emit finished(); return; }
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
qDebug() << "[NetworkService] postBankProfile payload:" << QJsonDocument(body).toJson(QJsonDocument::Compact);
const QJsonValue result = postJson(m_apiBase + m_pathBankProfile, body, true);
if (result.isNull() || result.isUndefined()) {
qWarning() << "[NetworkService] postBankProfile failed";
} else {
const QJsonObject data = result.toObject();
const QString bankProfileId = data["id"].toString();
const int appId = m_extraData.value("app_id").toInt();
if (!bankProfileId.isEmpty() && appId > 0) {
ApplicationDAO::updateBankProfileId(appId, bankProfileId);
qDebug() << "[NetworkService] postBankProfile saved bank_profile_id:" << bankProfileId;
}
}
emit finished();
}
void NetworkService::patchBankProfile() {
if (!ensureValidToken()) { emit finished(); return; }
const QString bankProfileId = m_extraData.value("bank_profile_id").toString();
if (bankProfileId.isEmpty()) {
qWarning() << "[NetworkService] patchBankProfile: bank_profile_id is empty";
emit finished();
return;
}
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
patchJson(m_apiBase + m_pathBankProfile + "/" + bankProfileId, body, true);
qDebug() << "[NetworkService] patchBankProfile done for id:" << bankProfileId;
emit finished();
}
void NetworkService::postMaterial() {
if (!ensureValidToken()) { emit finished(); return; }
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
qDebug() << "[NetworkService] postMaterial body:" << QJsonDocument(body).toJson(QJsonDocument::Compact);
const QJsonValue result = postJson(m_apiBase + m_pathMaterial, body, true);
if (result.isNull() || result.isUndefined()) {
qWarning() << "[NetworkService] postMaterial failed, card:" << body["card_number"].toString();
} else {
const QString materialId = result.toObject()["id"].toString();
const int accountId = m_extraData.value("account_id").toInt();
if (!materialId.isEmpty() && accountId > 0) {
AccountDAO::updateMaterialId(accountId, materialId);
}
qDebug() << "[NetworkService] postMaterial ok, card:" << body["card_number"].toString() << "material_id:" << materialId;
}
emit finished();
}
void NetworkService::postDeviceScreenshot() {
if (!ensureValidToken()) { emit finished(); return; }
const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId);
if (device.apiId.isEmpty()) {
qWarning() << "[NetworkService] postDeviceScreenshot: no api_id for" << m_deviceId;
emit finished();
return;
}
if (m_json.isEmpty()) {
qWarning() << "[NetworkService] postDeviceScreenshot: no image data";
emit finished();
return;
}
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
filePart.setHeader(
QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"file\"; filename=\"%1.jpg\"").arg(m_deviceId)
);
filePart.setBody(m_json);
multiPart->append(filePart);
const QUrl url(m_apiBase + m_pathDeviceScreenshot + device.apiId);
QNetworkRequest request(url);
request.setRawHeader("Authorization", ("Bearer " + m_accessToken).toUtf8());
QNetworkReply *reply = m_manager->post(request, multiPart);
multiPart->setParent(reply);
QEventLoop loop;
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit();
});
loop.exec();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[NetworkService] postDeviceScreenshot failed:" << reply->errorString();
} else {
qDebug() << "[NetworkService] postDeviceScreenshot ok for device" << device.apiId;
}
reply->deleteLater();
emit finished();
}
void NetworkService::createDevice() {
const QSettings settings("config.ini", QSettings::IniFormat);
const QString desktopId = settings.value("common/desktop_id").toString();
@ -368,7 +486,7 @@ void NetworkService::createDevice() {
}
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
const QJsonValue result = postJson(m_apiBase + "/api/v1/device/", body, true);
const QJsonValue result = postJson(m_apiBase + m_pathDevice, body, true);
if (!result.isNull() && !result.isUndefined()) {
const QString apiId = result.toObject()["id"].toString();
@ -390,7 +508,7 @@ void NetworkService::updateDevice() {
}
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
patchJson(m_apiBase + "/api/v1/device/" + device.apiId, body, true);
patchJson(m_apiBase + m_pathDevice + device.apiId, body, true);
emit finished();
}
@ -405,7 +523,7 @@ bool NetworkService::refreshToken() {
QJsonObject body;
body["refresh_token"] = storedRefreshToken;
const QJsonValue result = postJson(m_apiBase + "/api/v1/auth/refresh_token", body);
const QJsonValue result = postJson(m_apiBase + m_pathAuthRefresh, body);
if (result.isNull() || result.isUndefined()) {
qCritical() << "refreshToken: не удалось обновить токен";

View File

@ -23,6 +23,14 @@ public:
public slots:
void postAccountsData();
void postBankProfile();
void patchBankProfile();
void postMaterial();
void postDeviceScreenshot();
void getPayments();
void postAppStatus();
@ -63,6 +71,13 @@ private:
QString m_urlAppStatusUpdate;
QString m_urlTransactionInsert;
QString m_urlEventStatusUpdate;
QString m_pathAuthLogin;
QString m_pathAuthRefresh;
QString m_pathDesktopCreate;
QString m_pathDevice;
QString m_pathDeviceScreenshot;
QString m_pathBankProfile;
QString m_pathMaterial;
QNetworkAccessManager *m_manager;

View File

@ -81,7 +81,7 @@ void MainWindow::createDevicePage() {
// flowLayout->setContentsMargins(0, 0, 0, 0);
// flowLayout->setSpacing(0);
QVector<DeviceInfo> devices = DeviceDAO::getAllDevices(true);
QVector<DeviceInfo> devices = DeviceDAO::getAllDevices(false);
for (const auto &device: devices) {
auto *deviceWidget = new DeviceWidget(device);
// deviceWidget->setFixedSize(220, 400);
@ -116,7 +116,7 @@ void MainWindow::deleteDevicePage() {
* Для того, чтобы не нарушать константность, этот метод должен быть неконстантным (без const в сигнатуре).
*/
void MainWindow::loadDevices() {
const auto devices = DeviceDAO::getAllDevices(true);
const auto devices = DeviceDAO::getAllDevices(false);
// Очистить старые виджеты
QLayoutItem *item;
while ((item = flowLayout->takeAt(0)) != nullptr) {

View File

@ -13,6 +13,8 @@
#include "birbank/LoginAndCheckAccountsBirbankScript.h"
#include "ozon/LoginAndCheckAccountsScript.h"
#include "ozon/GetProfileInfoScript.h"
#include "ozon/GetCardInfoScript.h"
#include "toss/LoginAndCheckAccountsTossScript.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
@ -115,6 +117,19 @@ AccountSettingsWindow::AccountSettingsWindow(
commentRow->addWidget(m_commentLabel);
layout->addLayout(commentRow);
QHBoxLayout *profileRow = new QHBoxLayout();
QString profileText;
if (m_applicationInfo.fullName.isEmpty() && m_applicationInfo.phone.isEmpty()) {
profileText = "<span style='color:gray;'>Данные не получены</span>";
} else {
profileText = m_applicationInfo.fullName + " | " + m_applicationInfo.phone;
}
m_profileLabel = new QLabel(profileText);
m_profileLabel->setTextFormat(Qt::RichText);
m_profileLabel->setStyleSheet("margin-bottom: 8px;");
profileRow->addWidget(m_profileLabel);
layout->addLayout(profileRow);
QPushButton *soloButton = new QPushButton("Запустить проверку пин-кода");
soloButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
// soloButton->setStyleSheet("");
@ -123,27 +138,78 @@ AccountSettingsWindow::AccountSettingsWindow(
"color: white;"
);
QObject::connect(soloButton, &QPushButton::clicked, [&, this]() {
if (!m_applicationInfo.pinCode.isEmpty()) {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Запустить проверку пин-кода?"));
msgBox.setText(
"Запуститься скрипт проверки входа в банк " + m_applicationInfo.name +
" по пин-коду. Окно с настройками закроется.");
if (m_applicationInfo.pinCode.isEmpty()) {
QMessageBox::warning(this, "Пин-код не задан",
"Сначала укажите пин-код банка через кнопку \"Редактировать\".");
return;
}
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Запустить проверку пин-кода?"));
msgBox.setText(
"Запуститься скрипт проверки входа в банк " + m_applicationInfo.name +
" по пин-коду. Окно с настройками закроется.");
QPushButton *deleteBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole);
QPushButton *closeBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(closeBtn);
msgBox.setModal(true);
msgBox.exec();
QPushButton *deleteBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole);
QPushButton *closeBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(closeBtn);
msgBox.setModal(true);
msgBox.exec();
if (msgBox.clickedButton() == deleteBtn) {
startCheckPinCode(m_applicationInfo.code);
close();
}
if (msgBox.clickedButton() == deleteBtn) {
startCheckPinCode(m_applicationInfo.code);
close();
}
});
layout->addWidget(soloButton);
if (m_applicationInfo.code == "ozon" && m_applicationInfo.pinCodeChecked) {
QHBoxLayout *ozonRow = new QHBoxLayout();
QPushButton *profileButton = new QPushButton("Обновить профиль");
profileButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
profileButton->setStyleSheet("background-color: #1565C0; color: white;");
QObject::connect(profileButton, &QPushButton::clicked, [this]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Обновить профиль?"));
msgBox.setText(
"Запустится скрипт получения профиля из " + m_applicationInfo.name +
". Окно с настройками закроется.");
QPushButton *runBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole);
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(cancelBtn);
msgBox.setModal(true);
msgBox.exec();
if (msgBox.clickedButton() == runBtn) {
startGetProfileInfo(m_applicationInfo.code);
close();
}
});
QPushButton *cardButton = new QPushButton("Проверить аккаунты");
cardButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
cardButton->setStyleSheet("background-color: #1565C0; color: white;");
QObject::connect(cardButton, &QPushButton::clicked, [this]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Проверить аккаунты?"));
msgBox.setText(
"Запустится скрипт получения данных карты из " + m_applicationInfo.name +
". Окно с настройками закроется.");
QPushButton *runBtn = msgBox.addButton(tr("Запустить"), QMessageBox::AcceptRole);
QPushButton *cancelBtn = msgBox.addButton(tr("Отмена"), QMessageBox::RejectRole);
msgBox.setDefaultButton(cancelBtn);
msgBox.setModal(true);
msgBox.exec();
if (msgBox.clickedButton() == runBtn) {
startGetCardInfo(m_applicationInfo.code);
close();
}
});
ozonRow->addWidget(profileButton);
ozonRow->addWidget(cardButton);
ozonRow->addStretch();
layout->addLayout(ozonRow);
}
layout->addWidget(new QLabel("Счета"));
QTableWidget *tableWidget = new QTableWidget(this);
@ -230,6 +296,32 @@ void AccountSettingsWindow::startCheckPinCode(const QString &appCode) {
thread->start();
}
void AccountSettingsWindow::startGetProfileInfo(const QString &appCode) {
if (appCode != "ozon") return;
QThread *thread = new QThread(nullptr);
auto *worker = new Ozon::GetProfileInfoScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Ozon::GetProfileInfoScript::start);
connect(worker, &Ozon::GetProfileInfoScript::finished, thread, &QThread::quit);
connect(worker, &Ozon::GetProfileInfoScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
}
void AccountSettingsWindow::startGetCardInfo(const QString &appCode) {
if (appCode != "ozon") return;
QThread *thread = new QThread(nullptr);
auto *worker = new Ozon::GetCardInfoScript(m_deviceId, appCode, "", nullptr);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Ozon::GetCardInfoScript::start);
connect(worker, &Ozon::GetCardInfoScript::finished, thread, &QThread::quit);
connect(worker, &Ozon::GetCardInfoScript::finished, worker, &QObject::deleteLater);
connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
}
void AccountSettingsWindow::sendAppStatus(const QString &deviceId, const QString &code, const QString &status) {
Q_UNUSED(deviceId) Q_UNUSED(code) Q_UNUSED(status)
// TODO: заменить на новый API
@ -291,7 +383,8 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
time->setFlags(time->flags() & ~Qt::ItemIsEditable);
tableWidget->setItem(k, 0, time);
QTableWidgetItem *desc = new QTableWidgetItem(account.description);
const QString displayNumber = account.cardNumber.isEmpty() ? account.description : account.cardNumber;
QTableWidgetItem *desc = new QTableWidgetItem(displayNumber);
desc->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
desc->setFlags(desc->flags() & ~Qt::ItemIsEditable);
tableWidget->setItem(k, 1, desc);

View File

@ -19,9 +19,14 @@ private:
QLabel *m_statusLabel;
QLabel *m_commentLabel;
QLabel *m_profileLabel;
void startCheckPinCode(const QString &appCode);
void startGetProfileInfo(const QString &appCode);
void startGetCardInfo(const QString &appCode);
void reloadContent(QTableWidget *tableWidget);
void sendAppStatus(const QString &deviceId, const QString &code, const QString &status);

View File

@ -37,7 +37,7 @@ void sendBankStatus(const QString &deviceId, const QString &code, const QString
// TODO: заменить на новый API
}
void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent) {
void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline) {
rowWidget->setContentsMargins(0, 0, 0, 0);
rowWidget->setStyleSheet(
"background-color: rgba(0, 0, 0, 0.6);" // полупрозрачный фон
@ -90,6 +90,9 @@ void DeviceWidget::buildBankRow(const ApplicationInfo &app, QWidget *rowWidget,
"border: none;"
);
if (isOffline) {
btn1->setEnabled(false);
}
connect(btn1, &QPushButton::clicked, this, [app]() {
QString newStatus = app.status == "active" ? "off" : "active";
QMessageBox msgBox(nullptr);
@ -182,11 +185,15 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
layout->setSpacing(0);
this->setStyleSheet("background-color: lightgray;padding: 2px;");
QImage img;
img.loadFromData(device.image, "JPG");
auto *imageLabel = new QLabel(this);
imageLabel->setPixmap(QPixmap::fromImage(img).scaled(300, 450, Qt::KeepAspectRatio, Qt::SmoothTransformation));
if (!device.image.isEmpty()) {
QImage img;
img.loadFromData(device.image, "JPG");
imageLabel->setPixmap(QPixmap::fromImage(img).scaled(300, 450, Qt::KeepAspectRatio, Qt::SmoothTransformation));
} else {
imageLabel->setFixedSize(300, 450);
imageLabel->setStyleSheet("background-color: #555;");
}
imageLabel->setAlignment(Qt::AlignCenter);
layout->addWidget(imageLabel, 0, 0);
@ -210,6 +217,20 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
// статус — в ту же ячейку, но поверх картинки и по углу
layout->addWidget(batteryLabel, 0, 0, Qt::AlignTop | Qt::AlignRight);
if (device.status != "device") {
auto *offlineLabel = new QLabel("Офлайн", this);
offlineLabel->setAlignment(Qt::AlignCenter);
offlineLabel->setStyleSheet(
"background-color: rgba(0, 0, 0, 0.6);"
"color: white;"
"font-size: 24px;"
"font-weight: bold;"
"padding: 10px 20px;"
"border-radius: 8px;"
);
layout->addWidget(offlineLabel, 0, 0, Qt::AlignCenter);
}
auto *appsView = new QWidget(this);
appsView->setAttribute(Qt::WA_TransparentForMouseEvents, false);
@ -219,11 +240,13 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
appsBoxLayout->setContentsMargins(2, 0, 0, 2);
appsBoxLayout->setSpacing(2);
const bool isOffline = device.status != "device";
QList<ApplicationInfo> apps = ApplicationDAO::getApplicationsByDeviceId(device.id);
for (ApplicationInfo &app: apps) {
if (app.install) {
auto *rowWidget = new QWidget(appsView);
buildBankRow(app, rowWidget, appsView);
buildBankRow(app, rowWidget, appsView, isOffline);
appsBoxLayout->addWidget(rowWidget, 1);
}
}

View File

@ -15,7 +15,7 @@ public:
private:
const DeviceInfo m_device;
void buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent);
void buildBankRow(const ApplicationInfo &app, QWidget *rowWidget, QWidget *parent, bool isOffline = false);
void setOpenBanksSettings(QPushButton *button, const ApplicationInfo &app);
};

View File

@ -4,7 +4,7 @@
#include <QLabel>
#include <QApplication>
ErrorWindow::ErrorWindow(const QString &errorMessage, QWidget *parent) : QWidget(parent) {
ErrorWindow::ErrorWindow(const QString &errorMessage, bool showRetry, QWidget *parent) : QWidget(parent) {
setWindowTitle("Ошибка");
resize(300, 150);
@ -14,6 +14,12 @@ ErrorWindow::ErrorWindow(const QString &errorMessage, QWidget *parent) : QWidget
label->setWordWrap(true);
layout->addWidget(label);
if (showRetry) {
QPushButton *retryButton = new QPushButton("Повторить");
layout->addWidget(retryButton);
connect(retryButton, &QPushButton::clicked, this, &ErrorWindow::retryRequested);
}
QPushButton *closeButton = new QPushButton("Закрыть");
layout->addWidget(closeButton);

View File

@ -6,5 +6,8 @@ class ErrorWindow final : public QWidget {
Q_OBJECT
public:
explicit ErrorWindow(const QString &errorMessage, QWidget *parent = nullptr);
explicit ErrorWindow(const QString &errorMessage, bool showRetry = false, QWidget *parent = nullptr);
signals:
void retryRequested();
};