1
0
forked from BRT/arc

Add Ozon pay_to_new_number and pay_to_old_number XML assets

Included XML files for the "pay to new number" and "pay to old number" features in the Ozon platform.
This commit is contained in:
slava 2026-03-01 09:30:59 +07:00
parent 91f8e96fa6
commit afbf3a3afb
61 changed files with 7720 additions and 513 deletions

View File

@ -245,11 +245,16 @@ bool AdbUtils::enableAdbIMEKeyboard(const QString &deviceId) {
bool AdbUtils::inputAdbIMEText(const QString &deviceId, const QString &text) {
QProcess process;
QString command = QString(
"%1 -s %2 shell am broadcast -a ADB_INPUT_TEXT --es msg '%3'"
).arg(adbPath(), deviceId, text);
process.start(command);
process.start(adbPath(), {
"-s", deviceId,
"shell", "am", "broadcast",
"-a", "ADB_INPUT_TEXT",
"--es", "msg", text
});
process.waitForFinished();
qDebug() << "[AdbUtils] inputAdbIMEText result:" << process.exitCode()
<< "output:" << process.readAllStandardOutput().trimmed()
<< "error:" << process.readAllStandardError().trimmed();
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}

View File

@ -102,6 +102,13 @@ bool CommonScript::runAppAndGoToHomeScreen(
continue;
}
// Проверяем на баннер Google Play "Доступно обновление"
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(); !gpNode.isEmpty()) {
qDebug() << "[runAppAndGoToHomeScreen] Google Play update banner detected, closing...";
AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y());
QThread::msleep(1500);
continue;
}
// Проверяем, что это экран ввода пин-кода
if (!findAndInputPincode) {

View File

@ -256,6 +256,16 @@ void GetCardInfoScript::doStart() {
qDebug() << "[GetCardInfo] Card number found on attempt" << attempt + 1;
break;
}
// Закрываем баннер "Понятно" если он перекрывает экран
Node bannerBtn = xmlScreenParser.findTextNode("Понятно");
if (!bannerBtn.isEmpty()) {
qDebug() << "[GetCardInfo] Banner 'Понятно' detected, closing...";
AdbUtils::makeTap(m_deviceId, bannerBtn.x(), bannerBtn.y());
QThread::msleep(1000);
continue;
}
qDebug() << "[GetCardInfo] Card number not visible, attempt" << attempt + 1 << "/ 10";
}

View File

@ -1,10 +1,15 @@
#include "GetLastTransactionsScript.h"
#include <QProcess>
#include <QThread>
#include <QTimeZone>
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "time/DateUtils.h"
namespace Ozon {
@ -61,13 +66,29 @@ void GetLastTransactionsScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
// Скроллим вниз до кнопки "Все" и тапаем по ней
Node allBtn = swipeToButton(m_deviceId, "Все", m_counter,
device.screenWidth, device.screenHeight);
// Кнопка "Все" на домашнем экране находится у границы навбара (y ≈ 2190),
// поэтому сначала скроллим вниз, чтобы она поднялась выше навигационной панели
const int navBarTop = device.screenHeight - (device.screenHeight / 10); // ~90% высоты экрана
Node allBtn;
for (int i = 0; i < 10; ++i) {
// Сначала скроллим, потом ищем
const int x = device.screenWidth / 2;
const int offset = device.screenHeight / 4;
AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - offset, x, offset);
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
allBtn = xmlScreenParser.findButtonNode("Все", false);
if (!allBtn.isEmpty() && allBtn.y() > 0 && allBtn.y() < navBarTop) {
break;
}
allBtn = {};
qDebug() << "[GetLastTransactions] 'Все' not found above navbar, attempt" << i + 1 << "/ 10";
}
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;
}
@ -76,16 +97,206 @@ void GetLastTransactionsScript::doStart() {
AdbUtils::makeTap(m_deviceId, allBtn.x(), allBtn.y());
QThread::msleep(2000);
// Парсим транзакции за сегодня и вчера
// Ждём загрузки экрана "Операции"
bool loaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
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;
if (xmlScreenParser.isTransactionListScreen()) {
loaded = true;
break;
}
qDebug() << "[GetLastTransactions] Waiting for transaction list screen, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!loaded) {
m_error = "Transaction list screen not loaded: " + m_deviceId;
qWarning() << m_error;
emit finishedWithResult(m_error);
return;
}
// Ждём появления транзакций (могут подгружаться)
QList<Node> txButtons;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
txButtons = xmlScreenParser.findTransactionButtons(8);
if (!txButtons.isEmpty()) break;
qDebug() << "[GetLastTransactions] No transactions yet, attempt" << attempt + 1 << "/ 5";
QThread::msleep(1500);
}
qDebug() << "[GetLastTransactions] Found" << txButtons.size() << "transaction buttons";
// Находим accountId для сохранения транзакций
int accountId = -1;
const QList<AccountInfo> accounts = AccountDAO::getAccounts(m_deviceId, m_appCode);
if (!accounts.isEmpty()) {
accountId = accounts.first().id;
qDebug() << "[GetLastTransactions] Using accountId:" << accountId;
} else {
qWarning() << "[GetLastTransactions] No account found for" << m_deviceId << m_appCode;
}
// Для каждой транзакции: клик → берём дату из деталей → назад
QList<TransactionInfo> transactions;
for (int i = 0; i < txButtons.size(); ++i) {
const Node &btn = txButtons[i];
// Если кнопка внизу экрана (за навбаром) — скроллим до неё
const int navBarTop = device.screenHeight - (device.screenHeight / 10);
if (btn.y() <= 0 || btn.y() >= navBarTop) {
qDebug() << "[GetLastTransactions] Transaction" << i + 1 << "is off-screen (y=" << btn.y() << "), scrolling...";
for (int s = 0; s < 5; ++s) {
const int x = device.screenWidth / 2;
const int offset = device.screenHeight / 4;
AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight - offset, x, offset);
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
txButtons = xmlScreenParser.findTransactionButtons(8);
if (i < txButtons.size() && txButtons[i].y() > 0 && txButtons[i].y() < navBarTop) {
break;
}
}
if (i >= txButtons.size()) {
qWarning() << "[GetLastTransactions] Transaction" << i + 1 << "not found after scrolling";
break;
}
}
const Node &visibleBtn = txButtons[i];
TransactionInfo tx = xmlScreenParser.parseTransactionButtonText(visibleBtn.text);
qDebug() << "[GetLastTransactions] Tapping transaction" << i + 1 << ":" << visibleBtn.text.left(50);
AdbUtils::makeTap(m_deviceId, visibleBtn.x(), visibleBtn.y());
QThread::msleep(2000);
// Ждём загрузки деталей — ищем дату в шапке
TransactionInfo detail;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
detail = xmlScreenParser.parseTransactionDetail();
if (!detail.bankTime.isEmpty()) break;
qDebug() << "[GetLastTransactions] Detail not loaded, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
tx.bankName = "ozon";
tx.bankTime = detail.bankTime;
tx.status = TransactionStatus::Complete;
tx.timestamp = DateUtils::getUtcNow();
if (!detail.name.isEmpty()) tx.name = detail.name;
if (!detail.phone.isEmpty()) {
// Оставляем только цифры
QString cleanPhone;
for (const QChar &ch : detail.phone) {
if (ch.isDigit()) cleanPhone.append(ch);
}
tx.phone = cleanPhone;
tx.type = TransactionType::Phone;
}
// Чистим bankTime от unicode символов: заменяем все не-ASCII пробелы на обычные
if (!tx.bankTime.isEmpty()) {
QString dtStr = tx.bankTime;
for (int ci = 0; ci < dtStr.size(); ++ci) {
const QChar ch = dtStr.at(ci);
if (ch != ' ' && ch.category() == QChar::Separator_Space) {
dtStr[ci] = ' ';
}
}
tx.bankTime = dtStr;
const QTimeZone msk("Europe/Moscow");
QDateTime parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm");
if (!parsed.isValid()) {
parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm");
}
if (parsed.isValid()) {
parsed.setTimeZone(msk);
tx.completeTime = parsed;
}
}
qDebug() << "[GetLastTransactions]"
<< "date:" << tx.bankTime
<< "name:" << tx.description
<< "sender:" << tx.name
<< "phone:" << tx.phone
<< "amount:" << (tx.amount >= 0 ? "+" : "") + QString::number(tx.amount, 'f', 2);
// Сохраняем в БД если не существует
// Берём первый аккаунт для этого устройства и приложения
if (accountId != -1) {
tx.accountId = accountId;
if (TransactionDAO::insertTransactionIfNotExists(tx)) {
qDebug() << "[GetLastTransactions] Saved transaction to DB";
// Делаем скриншот документа транзакции
Node docBtn = xmlScreenParser.findButtonNode("Документы", false);
if (!docBtn.isEmpty()) {
qDebug() << "[GetLastTransactions] Tapping 'Документы'...";
AdbUtils::makeTap(m_deviceId, docBtn.x(), docBtn.y());
QThread::msleep(2000);
// Ждём появления кнопки "Поделиться"
bool docLoaded = false;
for (int da = 0; da < 10; ++da) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
Node shareBtn = xmlScreenParser.findNodeByContentDesc("Поделиться");
if (!shareBtn.isEmpty()) {
docLoaded = true;
break;
}
qDebug() << "[GetLastTransactions] Waiting for 'Поделиться', attempt" << da + 1 << "/ 10";
QThread::msleep(1500);
}
if (docLoaded) {
QThread::msleep(500);
QProcess screencapProc;
screencapProc.start("adb", QStringList() << "-s" << m_deviceId << "exec-out" << "screencap" << "-p");
screencapProc.waitForFinished(5000);
const QByteArray screenshot = screencapProc.readAllStandardOutput();
if (!screenshot.isEmpty()) {
// Сохраняем в БД
TransactionInfo saved = TransactionDAO::findByBankTimeAndAmount(
accountId, tx.bankName, tx.bankTime, tx.amount);
if (saved.id != -1) {
TransactionDAO::updateReceiptImage(saved.id, screenshot);
qDebug() << "[GetLastTransactions] Receipt saved to DB, txId:" << saved.id;
}
} else {
qWarning() << "[GetLastTransactions] Screenshot is empty";
}
} else {
qWarning() << "[GetLastTransactions] 'Поделиться' not found, skipping receipt";
}
// Возвращаемся из документов в детали транзакции
AdbUtils::goBack(m_deviceId);
QThread::msleep(1000);
}
} else {
qDebug() << "[GetLastTransactions] Transaction already exists or error";
}
}
transactions.append(tx);
// Возвращаемся в список
AdbUtils::goBack(m_deviceId);
QThread::msleep(1500);
// Ждём возврата на экран списка
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
if (xmlScreenParser.isTransactionListScreen()) break;
QThread::msleep(1000);
}
// Обновляем кнопки (после возврата координаты могут измениться)
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
txButtons = xmlScreenParser.findTransactionButtons(8);
}
AdbUtils::tryToKillApplication(m_deviceId, app.package);
emit finishedWithResult(m_error);
}

View File

@ -15,7 +15,8 @@ LoginAndCheckAccountsScript::LoginAndCheckAccountsScript(
QString appCode,
QObject *parent
) : CommonScript(parent),
m_deviceId(std::move(deviceId)), m_appCode(std::move(appCode)) {
m_deviceId(std::move(deviceId)),
m_appCode(std::move(appCode)) {
}
LoginAndCheckAccountsScript::~LoginAndCheckAccountsScript() = default;

View File

@ -1,17 +1,15 @@
#include "PayByPhoneScript.h"
#include <QRandomGenerator>
#include <QProcess>
#include <QRegularExpression>
#include <QThread>
#include <utility>
#include "DatabaseManager.h"
#include "adb/AdbUtils.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "db/ApplicationInfo.h"
#include "db/DeviceInfo.h"
namespace Ozon {
@ -28,313 +26,310 @@ PayByPhoneScript::PayByPhoneScript(
PayByPhoneScript::~PayByPhoneScript() = default;
void PayByPhoneScript::makePayment(
const QString &deviceId,
const QString &pinCode,
int width,
int height
) {
// Оплата со страницы карты
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node payBtn = xmlScreenParser.findButtonNode("Оплатить", false);
if (!payBtn.isEmpty()) {
AdbUtils::makeTap(deviceId, payBtn.x(), payBtn.y());
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_pay_btn_not_found");
m_error = "Кнопка 'Оплатить' не нашлась";
qDebug() << m_error;
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
// TODO - По номеру карты В РСХБ и другие банки
Node payTitle = xmlScreenParser.findTextNode("Платежи и переводы");
if (!payTitle.isEmpty() && payTitle.y() < 300) {
Node payByPhoneBtn = xmlScreenParser.findButtonNode("По номеру телефона В РСХБ и через", false);
if (!payByPhoneBtn.isEmpty()) {
AdbUtils::makeTap(deviceId, payByPhoneBtn.x(), payByPhoneBtn.y());
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_not_found_pay_by_phone");
m_error = "Не смогли найти пункт 'По номеру телефона В РСХБ и через'";
qDebug() << m_error;
return;
}
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_go_to_payments");
m_error = "Не смогли перейти на экран 'Платежи и переводы'";
qDebug() << m_error;
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node payByPhoneTitle = xmlScreenParser.findTextNode("Перевод по телефону");
Node continueBtn = xmlScreenParser.findButtonNode("Продолжить", false);
if (!payByPhoneTitle.isEmpty() && payByPhoneTitle.y() < 300 && continueBtn.isEmpty()) {
Node inputPhone = xmlScreenParser.findFirstEditText();
AdbUtils::makeTap(deviceId, inputPhone.x(), inputPhone.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, m_phone);
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_find_pay_by_phone");
m_error = "Не смогли найти пункт 'Перевод по телефону'";
qDebug() << m_error;
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
QString payBankName = m_bankName == QStringLiteral("Россельхозбанк") ? "Россельхозбанк" : "Другой банк через СБП";
Node payOtherBankBtn = xmlScreenParser.findButtonNode(payBankName, false);
if (!payOtherBankBtn.isEmpty()) {
AdbUtils::makeTap(deviceId, payOtherBankBtn.x(), payOtherBankBtn.y());
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_tap_on_bank");
m_error = "Не смогли нажать на первичный выбор банка";
qDebug() << m_error;
return;
}
if (m_bankName != QStringLiteral("Россельхозбанк")) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
for (int i = 0; i < 5; ++i) {
Node allBanksTitle = xmlScreenParser.findTextNode("Банки участники");
if (!allBanksTitle.isEmpty() && allBanksTitle.y() < 300) {
Node inputBankName = xmlScreenParser.findFirstEditText();
AdbUtils::makeTap(deviceId, inputBankName.x(), inputBankName.y());
QThread::msleep(500);
AdbUtils::enableAdbIMEKeyboard(deviceId);
AdbUtils::inputAdbIMEText(deviceId, m_bankName);
AdbUtils::disableAdbIMEKeyboard(deviceId);
QThread::msleep(1000);
break;
} else {
qDebug() << "--- allBanksTitle not found, i: " << i;
}
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
for (int i = 0; i < 5; ++i) {
Node bankNameListItem = xmlScreenParser.findTextNode(m_bankName);
if (!bankNameListItem.isEmpty()) {
AdbUtils::makeTap(deviceId, bankNameListItem.x(), bankNameListItem.y());
QThread::msleep(1000);
break;
} else {
qDebug() << "--- bankNameListItem not found, i: " << i;
}
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
}
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
const bool isRshbBank = m_bankName == QStringLiteral("Россельхозбанк");
QString payByPhoneScreenTitleText = isRshbBank ? "Перевод клиенту РСХБ" : "Перевод по телефону";
Node payByPhoneScreenTitle = xmlScreenParser.findTextNode(payByPhoneScreenTitleText);
Node continuePayBtn = xmlScreenParser.findButtonNode("Продолжить", false);
if (isRshbBank ||
!payByPhoneScreenTitle.isEmpty() && payByPhoneScreenTitle.y() < 300 && !continuePayBtn.isEmpty()) {
Node sumBtn = xmlScreenParser.findButtonNode("0", false);
if (isRshbBank || !sumBtn.isEmpty()) {
if (!isRshbBank) {
AdbUtils::makeTap(deviceId, sumBtn.x(), sumBtn.y());
QThread::msleep(500);
}
AdbUtils::inputText(deviceId, QString::number(m_amount));
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node readyBtn = xmlScreenParser.findTextNode("Готово");
if (!readyBtn.isEmpty()) {
AdbUtils::makeTap(deviceId, readyBtn.x(), readyBtn.y());
QThread::msleep(500);
if (isRshbBank) {
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
continuePayBtn = xmlScreenParser.findButtonNode("Продолжить", false);
}
AdbUtils::makeTap(deviceId, continuePayBtn.x(), continuePayBtn.y());
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_tap_on_ready");
m_error = "Не смогли нажать 'Готово'";
qDebug() << m_error;
return;
}
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_tap_on_continue");
m_error = "Не смогли нажать 'Продолжить'";
qDebug() << m_error;
return;
}
} else {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_find_bank_title");
m_error = "Не смогли найти заголовок: " + payByPhoneScreenTitleText;
qDebug() << m_error;
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
bool inputedPinCode = false;
for (int i = 0; i < 5; ++i) {
// TODO кнопка содержит "Перевести 10 ₽" если будет от 1000 то скорее всвего будет "Перевести 1 000 ₽",
// TODO не ясно как форматирование будет, поэтому пока на начало зашьем
Node makePayBtn = xmlScreenParser.findButtonNode("Перевести ", true);
if (!makePayBtn.isEmpty()) {
AdbUtils::makeTap(deviceId, makePayBtn.x(), makePayBtn.y());
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
if (const QList<Node> pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите код доступа");
pincode.size() == 4) {
inputPinCode(deviceId, pincode);
inputedPinCode = true;
// После ввода пин-кода, ждем 3с
QThread::msleep(3000);
}
break;
} else {
qWarning() << "--- makePayBtn not found, i: " << i;
}
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
}
if (!isRshbBank && !inputedPinCode) {
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_cant_input_pin_code");
m_error = "Не смогли ввести пин-код!'";
qDebug() << m_error;
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
for (int i = 0; i < 10; ++i) {
Node moreDetailBtn = xmlScreenParser.findButtonNode("Детали операции", false);
if (!moreDetailBtn.isEmpty()) {
AdbUtils::makeTap(deviceId, moreDetailBtn.x(), moreDetailBtn.y());
QThread::msleep(200);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
// TODO считываем все данные по переводу
TransactionInfo transaction = xmlScreenParser.parseTransactionInfo(width, height);
if (isRshbBank) {
transaction.bankName = m_bankName;
}
transaction.accountId = m_account.id;
transaction.amount = -m_amount;
transaction.type = TransactionType::Phone;
transaction.id = TransactionDAO::insertTransaction(transaction);
if (transaction.id == -1) {
// FIXME alert!!
qDebug() << "Transaction has not inserted...";
}
// ждем 10 секунд смену статуса
for (int k = 0; k < 10; ++k) {
Node paymentCompleted = xmlScreenParser.findTextNode("Исполнен", 0, 0, width, height / 2);
if (!paymentCompleted.isEmpty()) {
transaction.status = TransactionStatus::Complete;
if (!TransactionDAO::updateTransactionStatus(transaction.id, TransactionStatus::Complete)) {
qDebug() << "Not updated: " << transaction.id;
}
// TODO обновляем данные
break;
}
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
}
postNewTransactionData(m_account, transaction);
AdbUtils::takeScreenshot(deviceId, "pay_by_phone_finish_transactions_" + transaction.id);
AdbUtils::takeXmlDump(deviceId, "pay_by_phone_finish_transactions_" + transaction.id);
break;
} else {
qWarning() << "--- moreDetailBtn not found, i: " << i;
}
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
}
Node homeBtn = xmlScreenParser.findButtonNode("Вернуться на главную", false);
AdbUtils::makeTap(deviceId, homeBtn.x(), homeBtn.y());
QThread::msleep(1000);
}
void PayByPhoneScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
const ApplicationInfo app = ApplicationDAO::getApplication(m_account.deviceId, m_account.appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found....";
qDebug() << m_error;
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_account.deviceId, ++m_counter));
if (!xmlScreenParser.isHomeScreen()) {
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode, device.screenWidth, device.screenHeight)) {
m_error = "Не смогли дойти до домашней страницы";
qDebug() << m_error;
emit finishedWithResult(m_error);
return;
}
}
if (goToAllProducts(device.id, device.screenWidth, device.screenHeight)) {
QList<QPair<AccountInfo, Node> > accounts = xmlScreenParser.parseAccountsInfo();
for (auto &[account, node]: accounts) {
account.deviceId = m_account.deviceId;
account.appCode = m_account.appCode;
account.status = account.amount > 0 ? AccountStatus::Active : AccountStatus::Off;
if (!AccountDAO::upsertAccount(account)) {
qDebug() << "Not updated: " << account.lastNumbers;
}
}
postAccountsData(accounts);
for (auto &[account, node]: accounts) {
// FIXME будем считать что числа последние везде уникальные
if (m_account.lastNumbers == account.lastNumbers) {
if (account.amount >= m_amount) {
AdbUtils::makeTap(device.id, node.x(), node.y());
QThread::msleep(1000);
} else {
AdbUtils::takeScreenshot(device.id, "pay_by_phone_not_enough_money_" + QString::number(m_account.id));
m_error = "Not enough money, id: " + QString::number(m_account.id)
+ ", amount: " + QString::number(account.amount)
+ ", trx amount: " + QString::number(m_amount);
m_error = "Device or app not found: " + m_account.deviceId;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
break;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
// Закрываем рекламный bottom sheet
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[PayByPhone] Ad banner detected, closing...";
AdbUtils::makeTap(device.id, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[PayByPhone] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.id, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.id;
qWarning() << m_error;
AdbUtils::tryToKillApplication(device.id, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Закрываем баннеры
tryToCloseAllBannersOnHomePage(device.id, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
// Закрываем баннер Google Play "Доступно обновление"
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(); !gpNode.isEmpty()) {
qDebug() << "[PayByPhone] Google Play update banner detected, closing...";
AdbUtils::makeTap(device.id, gpNode.x(), gpNode.y());
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
// Закрываем баннер "Заказать карту" — тапаем сверху экрана
if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) {
qDebug() << "[PayByPhone] 'Заказать карту' banner detected, closing...";
AdbUtils::makeTap(device.id, device.screenWidth / 2, 100);
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.id, ++m_counter));
}
makePayment(device.id, app.pinCode, device.screenWidth, device.screenHeight);
// AdbUtils::tryToKillApplication(device.id, app.package); TODO - надо ли это???
} else {
AdbUtils::takeScreenshot(device.id, "pay_by_phone_cant_open_all_products_" + QString::number(m_account.id));
AdbUtils::tryToKillApplication(device.id, app.package);
m_error = "Не смогли открыть 'Все продукты'";
qDebug() << m_error;
}
emit finishedWithResult(m_error);
}
void PayByPhoneScript::makePayment(
const QString &deviceId,
const QString &pinCode,
const int width,
const int height
) {
// 1. Нажимаем "Перевести" на домашнем экране
Node transferBtn = xmlScreenParser.findButtonNode("Перевести", false);
if (transferBtn.isEmpty()) {
m_error = "Кнопка 'Перевести' не найдена на домашнем экране";
qWarning() << "[PayByPhone]" << m_error;
return;
}
qDebug() << "[PayByPhone] Tapping 'Перевести'...";
AdbUtils::makeTap(deviceId, transferBtn.x(), transferBtn.y());
QThread::msleep(2000);
// 2. Ждём экран "Платежи и переводы" с полем "Телефон получателя"
bool paymentsPageLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node phoneField = xmlScreenParser.findFirstEditText();
if (!phoneField.isEmpty()) {
paymentsPageLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for payments page, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!paymentsPageLoaded) {
m_error = "Экран 'Платежи и переводы' не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 3. Тапаем на поле "Телефон получателя" чтобы перейти на экран "Кому перевести"
Node phoneFieldTap = xmlScreenParser.findFirstEditText();
qDebug() << "[PayByPhone] Tapping 'Телефон получателя'...";
AdbUtils::makeTap(deviceId, phoneFieldTap.x(), phoneFieldTap.y());
QThread::msleep(2000);
// 4. Ждём экран "Кому перевести"
bool phoneScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node title = xmlScreenParser.findTextNode("Кому перевести");
if (!title.isEmpty()) {
phoneScreenLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for 'Кому перевести', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!phoneScreenLoaded) {
m_error = "Экран 'Кому перевести' не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 5. Тапаем на EditText и вводим номер телефона
Node phoneInput;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
phoneInput = xmlScreenParser.findFirstEditText();
if (!phoneInput.isEmpty()) break;
qDebug() << "[PayByPhone] Waiting for phone input field, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (phoneInput.isEmpty()) {
m_error = "Поле ввода телефона не найдено";
qWarning() << "[PayByPhone]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, phoneInput.x(), phoneInput.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, m_phone);
QThread::msleep(2000);
// 6. Ждём экран "По номеру телефона", обрабатывая промежуточные экраны
bool sumScreenLoaded = false;
bool bankSelected = false;
bool otherBankClicked = false;
for (int attempt = 0; attempt < 20; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
// Проверяем: уже на экране суммы?
Node sumTitle = xmlScreenParser.findTextNode("По номеру телефона");
if (!sumTitle.isEmpty() && sumTitle.y() < 300) {
sumScreenLoaded = true;
break;
}
// Ищем кнопку с номером телефона (новый перевод / последние переводы)
QString phoneDigits = m_phone;
phoneDigits.remove(QRegularExpression("[^0-9]"));
bool foundPhoneBtn = false;
for (const Node &btn : xmlScreenParser.findAllButtons()) {
QString btnDigits = btn.text;
btnDigits.remove(QRegularExpression("[^0-9]"));
if (btnDigits.contains(phoneDigits) || phoneDigits.contains(btnDigits.right(10))) {
qDebug() << "[PayByPhone] Found button with phone number:" << btn.text;
AdbUtils::makeTap(deviceId, btn.x(), btn.y());
QThread::msleep(2000);
foundPhoneBtn = true;
break;
}
}
if (foundPhoneBtn) continue;
// Экран выбора банка: ищем "Другой банк" (текст) или EditText с hint "Название банка"
if (!bankSelected) {
// Вариант 1: кнопка "Другой банк" (content-desc="other-bank-item", text пустой)
bool foundOtherBank = false;
{
Node node;
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (n.contentDesc.contains("other-bank-item")) {
node = n;
break;
}
}
if (!node.isEmpty()) {
qDebug() << "[PayByPhone] Tapping 'Другой банк' (other-bank-item)...";
AdbUtils::makeTap(deviceId, node.x(), node.y());
QThread::msleep(2000);
foundOtherBank = true;
otherBankClicked = true;
}
}
if (foundOtherBank) continue;
// Вариант 2: уже на экране поиска банка (EditText) — только после клика на "Другой банк"
if (otherBankClicked) {
Node bankInput = xmlScreenParser.findFirstEditText();
if (!bankInput.isEmpty()) {
qDebug() << "[PayByPhone] Bank search screen detected, entering bank name...";
AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y());
QThread::msleep(500);
// Ввод кириллицы через ADB Keyboard
AdbUtils::enableAdbIMEKeyboard(deviceId);
QThread::msleep(500);
AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y());
QThread::msleep(500);
AdbUtils::inputAdbIMEText(deviceId, m_bankName);
QThread::msleep(2000);
AdbUtils::disableAdbIMEKeyboard(deviceId);
qDebug() << "[PayByPhone] Entered bank name via ADB IME:" << m_bankName;
QThread::msleep(2000);
// Ищем банк в списке и кликаем по нему (кнопка с числовым content-desc)
bool bankFound = false;
for (int ba = 0; ba < 10; ++ba) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
for (const Node &node : xmlScreenParser.findAllNodes()) {
if (node.className == "android.widget.Button"
&& !node.contentDesc.isEmpty()
&& node.contentDesc != "base-go-back-button"
&& node.contentDesc != "native-toolbar-close-button"
&& node.contentDesc != "clear-input-icon") {
qDebug() << "[PayByPhone] Found bank in list, tapping:" << node.contentDesc << "bounds:" << node.x() << node.y();
AdbUtils::makeTap(deviceId, node.x(), node.y());
bankFound = true;
break;
}
}
if (bankFound) break;
qDebug() << "[PayByPhone] Waiting for bank list, attempt" << ba + 1 << "/ 10";
QThread::msleep(1500);
}
if (!bankFound) {
m_error = "Банк '" + m_bankName + "' не найден в списке";
qWarning() << "[PayByPhone]" << m_error;
return;
}
bankSelected = true;
QThread::msleep(2000);
continue;
}
} // otherBankClicked
}
qDebug() << "[PayByPhone] Waiting for 'По номеру телефона', attempt" << attempt + 1 << "/ 20";
QThread::msleep(1500);
}
if (!sumScreenLoaded) {
m_error = "Экран ввода суммы не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 7. Вводим сумму
Node sumInput = xmlScreenParser.findFirstEditText();
if (sumInput.isEmpty()) {
m_error = "Поле ввода суммы не найдено";
qWarning() << "[PayByPhone]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, sumInput.x(), sumInput.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, QString::number(static_cast<int>(m_amount)));
QThread::msleep(1000);
// 6. Нажимаем "Продолжить"
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node continueBtn = xmlScreenParser.findButtonNode("Продолжить", false);
if (continueBtn.isEmpty()) {
m_error = "Кнопка 'Продолжить' не найдена";
qWarning() << "[PayByPhone]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, continueBtn.x(), continueBtn.y());
QThread::msleep(2000);
// 7. Ждём экран "Подтвердите перевод" с кнопкой "Перевести ..."
bool confirmScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node confirmTitle = xmlScreenParser.findTextNode("Подтвердите перевод");
if (!confirmTitle.isEmpty()) {
confirmScreenLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for 'Подтвердите перевод', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!confirmScreenLoaded) {
m_error = "Экран подтверждения не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
qDebug() << "[PayByPhone] Confirm screen loaded, stopping before confirmation";
}
} // namespace Ozon

View File

@ -157,6 +157,20 @@ Node ScreenXmlParser::findButtonNode(const QString &text, const bool contains) {
return {};
}
QList<Node> ScreenXmlParser::findAllButtons() {
QList<Node> buttons;
for (const Node &node : m_nodes) {
if (node.className == "android.widget.Button" && !node.text.isEmpty()) {
buttons.append(node);
}
}
return buttons;
}
const QList<Node> &ScreenXmlParser::findAllNodes() const {
return m_nodes;
}
Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc) {
for (const Node &node: m_nodes) {
if (node.clickable && node.contentDesc.trimmed() == contentDesc) {
@ -198,6 +212,25 @@ Node ScreenXmlParser::tryToFindAdBanner() {
return {};
}
Node ScreenXmlParser::tryToFindGooglePlayBanner() {
// Баннер Google Play "Доступно обновление" — ищем кнопку закрытия по content-desc
for (const Node &node : m_nodes) {
if (node.contentDesc.contains("Закрыть диалоговое окно") && node.clickable) {
return node;
}
}
// Альтернативно — по content-desc "Доступно обновление"
for (const Node &node : m_nodes) {
if (node.contentDesc.contains("Доступно обновление")) {
// Тапаем в верхнюю часть экрана (выше баннера), чтобы закрыть
Node tapNode;
tapNode.setBounds(540, 100, 541, 101);
return tapNode;
}
}
return {};
}
bool ScreenXmlParser::isProfileScreen() {
// Страница профиля Ozon: есть заголовок "Аккаунт" и кнопка "Выйти"
bool hasTitle = false;
@ -1223,6 +1256,110 @@ QString ScreenXmlParser::findFullCardNumber() {
return {};
}
bool ScreenXmlParser::isTransactionListScreen() {
// Экран "Операции": WebView с text="Операции" + наличие кнопок фильтров
for (const Node &node : m_nodes) {
if (node.className == "android.webkit.WebView" && node.text.trimmed() == "Операции") {
return true;
}
}
return false;
}
QList<Node> ScreenXmlParser::findTransactionButtons(const int maxCount) {
const QChar rubleSign(0x20BD); // ₽
QList<Node> buttons;
for (const Node &node : m_nodes) {
if (node.className != "android.widget.Button") continue;
const QString text = node.text.trimmed();
if (!text.contains(rubleSign)) continue;
// Пропускаем сводки "Расходы", "Доходы"
if (text.startsWith("Расходы") || text.startsWith("Доходы")) continue;
buttons.append(node);
if (buttons.size() >= maxCount) break;
}
return buttons;
}
TransactionInfo ScreenXmlParser::parseTransactionButtonText(const QString &text) {
const QChar rubleSign(0x20BD); // ₽
const QChar unicodeMinus(0x2212); //
const QChar nbsp(0x00A0); // non-breaking space
TransactionInfo info;
const int rubleIdx = text.indexOf(rubleSign);
if (rubleIdx < 0) return info;
int signIdx = -1;
bool isExpense = false;
for (int i = rubleIdx - 1; i >= 0; --i) {
const QChar ch = text.at(i);
if (ch == '+') { signIdx = i; isExpense = false; break; }
if (ch == '-' || ch == unicodeMinus) { signIdx = i; isExpense = true; break; }
}
if (signIdx < 0) return info;
info.description = text.left(signIdx).trimmed();
QString amountStr = text.mid(signIdx + 1, rubleIdx - signIdx - 1).trimmed();
amountStr.remove(QChar(' '));
amountStr.remove(nbsp);
amountStr.replace(',', '.');
info.amount = amountStr.toDouble();
if (isExpense) info.amount = -info.amount;
return info;
}
TransactionInfo ScreenXmlParser::parseTransactionDetail() {
TransactionInfo info;
for (const Node &node : m_nodes) {
if (node.className != "android.widget.TextView") continue;
const QString text = node.text.trimmed();
if (text.isEmpty()) continue;
// Дата+время: "15.01.2026, 15:55"
if (info.bankTime.isEmpty() && text.contains('.') && text.contains(':') && text.length() <= 25) {
info.bankTime = text;
continue;
}
// Телефон: "+7 (985) 553-28-96"
if (info.phone.isEmpty() && text.startsWith('+') && text.contains('(')) {
info.phone = text;
continue;
}
}
// Имя отправителя — TextView прямо перед телефоном (по y-координате чуть выше)
if (!info.phone.isEmpty()) {
// Ищем ноду телефона и берём ближайший TextView выше
int phoneY = -1;
for (const Node &node : m_nodes) {
if (node.className == "android.widget.TextView" && node.text.trimmed() == info.phone) {
phoneY = node.y();
break;
}
}
if (phoneY > 0) {
int bestY = -1;
for (const Node &node : m_nodes) {
if (node.className != "android.widget.TextView") continue;
const QString text = node.text.trimmed();
if (text.isEmpty() || text == info.bankTime || text == info.phone) continue;
if (node.y() > 0 && node.y() < phoneY && node.y() > bestY) {
bestY = node.y();
info.name = text;
}
}
}
}
return info;
}
void ScreenXmlParser::test() {
// TransactionInfo info2 = parseExpandedInfo(m_xml);
// qDebug().noquote().nospace() << convertTransactionToString(info2);

View File

@ -30,12 +30,18 @@ public:
Node findButtonNode(const QString &text, bool contains);
QList<Node> findAllButtons();
const QList<Node> &findAllNodes() const;
Node findNodeByContentDesc(const QString &contentDesc);
bool isHomeScreen();
Node tryToFindAdBanner();
Node tryToFindGooglePlayBanner();
// Returns a Button node whose text is exactly 4 digits (card last numbers)
Node findCardButtonOnHomeScreen();
@ -73,6 +79,17 @@ public:
// Finds a TextView with full card number (format "XXXX XXXX XXXX XXXX")
QString findFullCardNumber();
bool isTransactionListScreen();
// Возвращает кнопки транзакций на экране "Операции" (до maxCount штук)
QList<Node> findTransactionButtons(int maxCount);
// Парсит имя и сумму из текста кнопки: "NAME + 150 ₽ CATEGORY"
TransactionInfo parseTransactionButtonText(const QString &text);
// Парсит детали транзакции: дату, имя отправителя, телефон
TransactionInfo parseTransactionDetail();
void test();
private:

BIN
assets/ozon/card_banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 565 KiB

746
assets/ozon/card_banner.xml Normal file
View File

@ -0,0 +1,746 @@
<?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.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="" 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/action_bar_root"
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="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="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/container"
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/coordinator"
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 NAF="true" index="0" text=""
resource-id="ru.ozon.fintech.finance:id/touch_outside" 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="[0,0][1080,2400]" drawing-order="1" hint=""/>
<node index="1" text="" resource-id="ru.ozon.fintech.finance:id/design_bottom_sheet"
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,497][1080,2400]" drawing-order="2"
hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/base_bottom_sheet_top"
class="android.widget.LinearLayout" 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="[0,633][1080,2400]" drawing-order="1" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/base_bottom_sheet_dragger"
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,633][1080,691]" drawing-order="1" hint=""/>
<node index="2" text=""
resource-id="ru.ozon.fintech.finance:id/base_bottom_sheet_main_container"
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,691][1080,2337]" drawing-order="3" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/cbottom_recycler"
class="androidx.recyclerview.widget.RecyclerView"
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="[0,691][1080,2337]"
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,691][1080,1251]" 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,691][1080,1251]" 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="[42,691][1038,1251]"
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="[42,691][1038,1251]" 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="[42,691][1038,1251]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
class="android.widget.ImageView"
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,691][1038,1251]"
drawing-order="1" hint=""/>
</node>
</node>
</node>
</node>
</node>
<node index="1" 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,1251][1080,1476]" 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,1251][1080,1476]"
drawing-order="1" hint="">
<node index="0"
text="Бесплатная карта для доступа к товарам за 1 ₽ на Ozon"
resource-id="ru.ozon.fintech.finance:id/text_atom"
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,1293][1038,1455]"
drawing-order="1" hint=""/>
</node>
</node>
<node index="2" 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,1476][1080,1644]" drawing-order="3" 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,1476][1080,1644]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
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="[42,1497][1038,1623]"
drawing-order="1" hint="">
<node index="1" 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="[42,1497][168,1623]" 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="[42,1497][168,1623]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
class="android.widget.ImageView"
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,1497][168,1623]"
drawing-order="1" hint=""/>
</node>
</node>
<node index="2" 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="[210,1509][1038,1611]" drawing-order="3"
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="[210,1509][1038,1611]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
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="[210,1509][1038,1611]"
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="[210,1509][1038,1563]"
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="[210,1509][1038,1563]"
drawing-order="1" hint="">
<node index="0"
text="Копите звёзды за покупки вне Ozon"
resource-id="ru.ozon.fintech.finance:id/text_atom"
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="[210,1509][1038,1563]"
drawing-order="1" hint=""/>
</node>
</node>
<node index="1" 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="[210,1563][1038,1611]"
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="[210,1563][1038,1611]"
drawing-order="1" hint="">
<node index="0"
text="В любых магазинах и кафе"
resource-id="ru.ozon.fintech.finance:id/text_atom"
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="[210,1563][1038,1611]"
drawing-order="1" hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
<node index="3" 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,1644][1080,1842]" drawing-order="4" 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,1644][1080,1842]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
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="[42,1665][1038,1821]"
drawing-order="1" hint="">
<node index="1" 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="[42,1665][168,1791]" 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="[42,1665][168,1791]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
class="android.widget.ImageView"
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,1665][168,1791]"
drawing-order="1" hint=""/>
</node>
</node>
<node index="2" 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="[210,1665][1038,1821]" drawing-order="3"
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="[210,1665][1038,1821]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
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="[210,1665][1038,1821]"
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="[210,1665][1038,1773]"
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="[210,1665][1038,1773]"
drawing-order="1" hint="">
<node index="0"
text="Списывайте звёзды и получайте товары за 1 ₽"
resource-id="ru.ozon.fintech.finance:id/text_atom"
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="[210,1665][1038,1773]"
drawing-order="1" hint=""/>
</node>
</node>
<node index="1" 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="[210,1773][1038,1821]"
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="[210,1773][1038,1821]"
drawing-order="1" hint="">
<node index="0"
text="Выбирайте из любимых брендов на Ozon"
resource-id="ru.ozon.fintech.finance:id/text_atom"
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="[210,1773][1038,1821]"
drawing-order="1" hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
<node index="4" 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,1842][1080,2010]" drawing-order="5" 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,1842][1080,2010]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
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="[42,1863][1038,1989]"
drawing-order="1" hint="">
<node index="1" 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="[42,1863][168,1989]" 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="[42,1863][168,1989]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
class="android.widget.ImageView"
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,1863][168,1989]"
drawing-order="1" hint=""/>
</node>
</node>
<node index="2" 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="[210,1875][1038,1977]" drawing-order="3"
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="[210,1875][1038,1977]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
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="[210,1875][1038,1977]"
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="[210,1875][1038,1929]"
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="[210,1875][1038,1929]"
drawing-order="1" hint="">
<node index="0"
text="Пользуйтесь картой бесплатно"
resource-id="ru.ozon.fintech.finance:id/text_atom"
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="[210,1875][1038,1929]"
drawing-order="1" hint=""/>
</node>
</node>
<node index="1" 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="[210,1929][1038,1977]"
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="[210,1929][1038,1977]"
drawing-order="1" hint="">
<node index="0"
text="Без комиссии за выпуск и обслуживание"
resource-id="ru.ozon.fintech.finance:id/text_atom"
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="[210,1929][1038,1977]"
drawing-order="1" hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
<node index="5" 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,2010][1080,2241]" drawing-order="6" 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,2010][1080,2241]"
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="true"
enabled="true" focusable="true" focused="false"
scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[42,2052][1038,2199]"
drawing-order="1" hint="">
<node index="0" 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="[381,2099][699,2152]" drawing-order="1"
hint="">
<node index="0" text="Заказать карту"
resource-id="ru.ozon.fintech.finance:id/mainViewId"
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="[381,2099][699,2152]"
drawing-order="2" 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="[381,2099][699,2152]"
drawing-order="4" hint=""/>
<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="[381,2149][699,2152]"
drawing-order="3" hint=""/>
</node>
</node>
</node>
</node>
<node index="6" 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,2241][1080,2337]" drawing-order="7" 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,2241][1080,2337]"
drawing-order="1" hint="">
<node index="0" text="Условия программы лояльности"
resource-id="ru.ozon.fintech.finance:id/text_atom"
class="android.widget.TextView"
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,2241][1038,2295]"
drawing-order="1" hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</hierarchy>

BIN
assets/ozon/google_play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

283
assets/ozon/google_play.xml Normal file
View File

@ -0,0 +1,283 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="com.android.vending"
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.LinearLayout" package="com.android.vending"
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="com.android.vending"
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="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.FrameLayout" package="com.android.vending" 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="com.android.vending" 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="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.FrameLayout" package="com.android.vending" 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="com.android.vending:id/0_resource_name_obfuscated"
class="android.view.ViewGroup" package="com.android.vending" 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,136][1080,2337]" drawing-order="1" hint="">
<node NAF="true" index="0" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.view.View" package="com.android.vending" content-desc=""
checkable="false" checked="false" clickable="true" enabled="true" focusable="true"
focused="false" scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[0,136][1080,2337]" drawing-order="1" hint=""/>
<node index="1" text="" resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.FrameLayout" package="com.android.vending" 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,1276][1080,2337]" drawing-order="2"
hint="">
<node index="0" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.LinearLayout" package="com.android.vending"
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,1276][1080,2337]" drawing-order="1" hint="">
<node index="0" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.ScrollView" package="com.android.vending"
content-desc="" checkable="false" checked="false" clickable="false"
enabled="true" focusable="true" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[0,1276][1080,2169]" drawing-order="2" hint="">
<node index="0" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.LinearLayout" package="com.android.vending"
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,1276][1080,2169]" drawing-order="1" hint="">
<node index="0" text="" resource-id=""
class="android.widget.FrameLayout" package="com.android.vending"
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,1276][1080,2169]" drawing-order="1"
hint="">
<node index="0" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.LinearLayout"
package="com.android.vending" 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,1276][1080,2169]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
class="android.widget.FrameLayout"
package="com.android.vending" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[63,1276][1080,1418]"
drawing-order="1" hint="">
<node index="0" text="" resource-id=""
class="android.widget.LinearLayout"
package="com.android.vending" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[63,1318][1080,1397]" drawing-order="1"
hint="">
<node index="0" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.ImageView"
package="com.android.vending" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[63,1326][126,1389]"
drawing-order="1" hint=""/>
<node index="1" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.ImageView"
package="com.android.vending"
content-desc="Google Play" checkable="false"
checked="false" clickable="false" enabled="true"
focusable="false" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[147,1326][352,1389]" drawing-order="2"
hint=""/>
</node>
<node index="1" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.FrameLayout"
package="com.android.vending" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[938,1276][1080,1418]" drawing-order="2"
hint="">
<node index="0" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.ImageView"
package="com.android.vending"
content-desc="Закрыть диалоговое окно с информацией об обновлении"
checkable="false" checked="false" clickable="true"
enabled="true" focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[938,1276][1080,1418]" drawing-order="1"
hint=""/>
</node>
</node>
<node index="1" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.FrameLayout"
package="com.android.vending"
content-desc="Доступно обновление&#10;" checkable="false"
checked="false" clickable="false" enabled="true"
focusable="true" focused="false" scrollable="false"
long-clickable="false" password="false" selected="false"
bounds="[63,1418][1080,1570]" drawing-order="2" hint=""/>
<node index="2" text="" resource-id=""
class="android.widget.LinearLayout"
package="com.android.vending" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[63,1570][1017,2169]"
drawing-order="3" hint="">
<node index="0"
text="Чтобы использовать приложение, установите его последнюю версию. Во время скачивания обновлений вы по-прежнему сможете им пользоваться."
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.TextView"
package="com.android.vending" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[63,1570][1017,1784]" drawing-order="1"
hint=""/>
<node index="1" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.view.ViewGroup"
package="com.android.vending" content-desc=""
checkable="false" checked="false" clickable="false"
enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[63,1810][1017,1957]" drawing-order="2"
hint="">
<node index="0" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.ImageView"
package="com.android.vending" content-desc=""
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[63,1810][210,1957]"
drawing-order="4" hint=""/>
<node index="1" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.view.View"
package="com.android.vending"
content-desc="Приложение: Ozon Банк: выгодные покупки&#10;Рейтинг: 4,2.&#10;13 МБ&#10;3+&#10;"
checkable="false" checked="false"
clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[252,1810][1017,1917]"
drawing-order="2" hint=""/>
</node>
<node index="2" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.FrameLayout"
package="com.android.vending"
content-desc="Что нового&#10;Последнее обновление: 15 февр. 2026г.&#10;"
checkable="false" checked="false" clickable="true"
enabled="true" focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[63,1983][1017,2169]" drawing-order="3"
hint="">
<node index="0" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.ImageView"
package="com.android.vending"
content-desc="Развернуть" checkable="false"
checked="false" clickable="true" enabled="true"
focusable="true" focused="false"
scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[954,2045][1017,2108]" drawing-order="2"
hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
<node index="2" text=""
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.LinearLayout" package="com.android.vending"
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,2169][1080,2337]" drawing-order="4" hint="">
<node index="0" text="" resource-id="" class="android.widget.LinearLayout"
package="com.android.vending" content-desc="" checkable="false"
checked="false" clickable="false" enabled="true" focusable="false"
focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[63,2169][1017,2337]"
drawing-order="1" hint="">
<node index="0" text=""
resource-id="com.android.vending:id/button_group"
class="android.view.ViewGroup" package="com.android.vending"
content-desc="" checkable="false" checked="false"
clickable="false" enabled="true" focusable="false" focused="false"
scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[63,2190][1017,2316]" drawing-order="1"
hint="">
<node index="0" text="Подробнее"
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.Button" package="com.android.vending"
content-desc="" checkable="false" checked="false"
clickable="true" enabled="true" focusable="true"
focused="false" scrollable="false" long-clickable="false"
password="false" selected="false" bounds="[63,2201][524,2306]"
drawing-order="1" hint=""/>
<node index="1" text="Обновить"
resource-id="com.android.vending:id/0_resource_name_obfuscated"
class="android.widget.Button" package="com.android.vending"
content-desc="" checkable="false" checked="false"
clickable="true" enabled="true" focusable="true"
focused="false" scrollable="false" long-clickable="false"
password="false" selected="false"
bounds="[556,2201][1017,2306]" drawing-order="2" hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</hierarchy>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
assets/ozon/select_bank.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

518
assets/ozon/select_bank.xml Normal file
View File

@ -0,0 +1,518 @@
<?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 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="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,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,2337]"
drawing-order="0" hint="">
<node index="0" text="" resource-id="MFC2C@sbp"
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,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,252]"
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="[44,149][133,236]"
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="[372,165][708,223]"
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="[1021,191][1080,252]"
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,249][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="[42,280][1039,430]"
drawing-order="0" hint="">
<node index="0"
text="+7 (985) 553-28-96"
resource-id="select-contact-input"
class="android.widget.EditText"
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="[84,349][808,404]"
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="[42,280][1039,430]"
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="[84,307][488,351]"
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="[931,322][997,388]"
drawing-order="0"
hint=""/>
</node>
<node index="1"
text="В этом месяце можно перевести ещё 600 000₽ без комиссии"
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="[168,511][997,609]"
drawing-order="0" hint=""/>
<node index="2" text=""
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="internal-bank-item"
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,669][1039,829]"
drawing-order="0" hint=""/>
<node index="3" text=""
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="100000000008"
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,826][1039,987]"
drawing-order="0" hint=""/>
<node index="4" text=""
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="100000000004"
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,984][1039,1144]"
drawing-order="0" hint=""/>
<node index="5" text=""
resource-id=""
class="android.widget.Button"
package="ru.ozon.fintech.finance"
content-desc="other-bank-item"
checkable="false"
checked="false"
clickable="true"
enabled="true"
focusable="true"
focused="false"
scrollable="false"
long-clickable="false"
password="false"
selected="false"
bounds="[42,1141][1039,1302]"
drawing-order="0" hint=""/>
</node>
</node>
</node>
</node>
<node index="1" 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,2336][1080,2336]"
drawing-order="0" hint=""/>
<node index="2" 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="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: 167 KiB

View File

@ -0,0 +1,204 @@
<?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.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="" 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/action_bar_root"
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="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="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/container"
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/coordinator"
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 NAF="true" index="0" text=""
resource-id="ru.ozon.fintech.finance:id/touch_outside" 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="[0,0][1080,2400]" drawing-order="1" hint=""/>
<node index="1" text="" resource-id="ru.ozon.fintech.finance:id/design_bottom_sheet"
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,1589][1080,2400]" drawing-order="2"
hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/base_bottom_sheet_top"
class="android.widget.LinearLayout" 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="[0,1725][1080,2400]" drawing-order="1" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/base_bottom_sheet_dragger"
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,1725][1080,1783]" drawing-order="1" hint=""/>
<node index="2" text=""
resource-id="ru.ozon.fintech.finance:id/base_bottom_sheet_main_container"
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,1783][1080,2337]" drawing-order="3" hint="">
<node index="0" text=""
resource-id="ru.ozon.fintech.finance:id/cbottom_recycler"
class="androidx.recyclerview.widget.RecyclerView"
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="[0,1783][1080,2337]"
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,1783][1080,1966]" 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,1783][1080,1966]"
drawing-order="1" hint="">
<node index="0" text="Можете указывать любое имя при оплате"
resource-id="ru.ozon.fintech.finance:id/text_atom"
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,1783][1038,1945]"
drawing-order="1" hint=""/>
</node>
</node>
<node index="1" 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,1966][1080,2158]" 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,1966][1080,2158]"
drawing-order="1" hint="">
<node index="0"
text="Например, OZON CLIENT или ваше имя и фамилию латиницей"
resource-id="ru.ozon.fintech.finance:id/text_atom"
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,1966][1038,2074]"
drawing-order="1" hint=""/>
</node>
</node>
<node index="2" 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,2158][1080,2337]" drawing-order="3" 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,2158][1080,2337]"
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="true"
enabled="true" focusable="true" focused="false"
scrollable="false" long-clickable="false" password="false"
selected="false" bounds="[42,2158][1038,2274]"
drawing-order="1" hint="">
<node index="0" 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="[453,2189][626,2242]" drawing-order="1"
hint="">
<node index="0" text="Понятно"
resource-id="ru.ozon.fintech.finance:id/mainViewId"
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="[453,2189][626,2242]"
drawing-order="2" 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="[453,2189][626,2242]"
drawing-order="4" hint=""/>
<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="[453,2239][626,2242]"
drawing-order="3" hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</node>
</hierarchy>

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View File

@ -0,0 +1,100 @@
<?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="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="1" hint="">
<node index="0" 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="1" hint="">
<node index="0" 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,136][1080,283]" drawing-order="2" hint="">
<node index="0" text="Терехина_Г_Г_о_проведении_операции_ozonbank_document_24959706.pdf"
resource-id="ru.ozon.fintech.finance:id/root_title" 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,182][932,236]"
drawing-order="1" hint=""/>
<node index="1" text="" resource-id="" class="androidx.appcompat.widget.LinearLayoutCompat"
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="[932,136][1080,283]"
drawing-order="4" hint="">
<node NAF="true" index="0" text=""
resource-id="ru.ozon.fintech.finance:id/fintech_menu_close"
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="[932,146][1080,272]" drawing-order="1" hint=""/>
</node>
</node>
<node index="1" text="" resource-id="" class="androidx.viewpager.widget.ViewPager"
package="ru.ozon.fintech.finance" content-desc="" checkable="false" checked="false"
clickable="false" enabled="true" focusable="false" focused="false" scrollable="true"
long-clickable="false" password="false" selected="false" bounds="[0,283][1080,2337]"
drawing-order="1" hint="">
<node index="0" text="" resource-id="" class="androidx.recyclerview.widget.RecyclerView"
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="[0,283][1080,2337]"
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,283][1080,2337]"
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,283][1080,2337]" 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,283][1080,2337]" drawing-order="1" hint="">
<node index="0" text="" resource-id="ru.ozon.fintech.finance:id/pv_pdf"
class="android.widget.ImageView" 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,283][1080,2337]" drawing-order="1" hint=""/>
</node>
</node>
</node>
</node>
</node>
<node index="2" text="" resource-id="ru.ozon.fintech.finance:id/share_btn"
class="android.view.ViewGroup" 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,2148][1038,2295]" drawing-order="7" 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="[42,2148][1038,2295]" drawing-order="1" 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="[418,2190][663,2253]"
drawing-order="1" hint=""/>
</node>
</node>
</node>
</node>
</node>
</node>
</hierarchy>

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -0,0 +1,502 @@
<?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 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="15.01.2026, 15:55" 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="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,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="15.01.2026, 15:55"
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="[367,160][711,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="" 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,301][1039,771]"
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,301][1039,771]"
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="[105,364][619,425]"
drawing-order="0" hint=""/>
<node index="1" text="+150₽"
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="[105,441][333,530]"
drawing-order="0" hint=""/>
<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="[847,364][976,493]"
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="[847,364][976,493]"
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="[926,443][992,509]"
drawing-order="0" hint=""/>
</node>
<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="[105,556][976,609]"
drawing-order="0" hint="">
<node index="0"
text="CARD2CARD AMOBILE"
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="[105,553][556,611]"
drawing-order="0" hint=""/>
<node index="1" text=""
resource-id=""
class="android.widget.Image"
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="[564,574][632,598]"
drawing-order="0" hint=""/>
<node index="2" 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="[640,553][876,611]"
drawing-order="0" hint=""/>
</node>
<node index="4" 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="[105,627][509,685]"
drawing-order="0" hint=""/>
<node index="5" 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="[522,635][567,679]"
drawing-order="0" hint=""/>
</node>
</node>
<node 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="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,787][530,1008]"
drawing-order="0" hint=""/>
<node index="2" 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="[548,787][1039,1008]"
drawing-order="0" hint=""/>
<node index="3" text=""
resource-id="MFInlineVoc@self-service"
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,1005][1039,1018]"
drawing-order="0" hint=""/>
</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,2336][1080,2336]"
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="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: 100 KiB

View File

@ -0,0 +1,509 @@
<?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 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="13.01.2026, 12:42" 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="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,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="13.01.2026, 12:42"
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="[370,160][708,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="" 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,301][1039,916]"
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,301][1039,916]"
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="[105,364][619,425]"
drawing-order="0" hint=""/>
<node index="1" text="+100₽"
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="[105,441][336,530]"
drawing-order="0" hint=""/>
<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="[847,364][976,493]"
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="[847,364][976,493]"
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="[926,443][992,509]"
drawing-order="0" hint="">
<node index="0" text=""
resource-id=""
class="android.widget.Image"
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="[942,443][976,509]"
drawing-order="0"
hint=""/>
</node>
</node>
<node index="3" 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="[105,532][349,590]"
drawing-order="0" hint=""/>
<node index="4" text="" resource-id=""
class="android.widget.Image"
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="[367,548][435,574]"
drawing-order="0" hint=""/>
<node index="5" 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="[454,535][758,588]"
drawing-order="0" hint=""/>
<node index="6"
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="[105,603][627,661]"
drawing-order="0" hint=""/>
<node index="7"
text="+7 (985) 553-28-96"
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="[105,664][976,716]"
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="[341,756][976,853]"
drawing-order="0" hint=""/>
</node>
</node>
<node 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="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false"
bounds="[42,931][530,1152]"
drawing-order="0" hint=""/>
<node index="2" 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="[548,931][1039,1152]"
drawing-order="0" hint=""/>
<node index="3" text=""
resource-id="MFInlineVoc@self-service"
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,1147][1039,1160]"
drawing-order="0" hint=""/>
</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,2336][1080,2336]"
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="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>

View File

@ -2,13 +2,13 @@
list=ozon
[common]
access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyMjQ4MzM1fQ.gDA57WCWCCwdrxa84McLJLcIyV4RYLRQ7s9YT3AcFd4
access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjNDU1MjE1Ni1kMTc5LTRhNzktOWU5MS01ODcwMzhjOTVjZjAiLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyNDE4MDUwfQ.26k6Z22gMOzNFuS17Mx0d4dt8pnJQh9TIv0rrAk2Xro
api_key=0eee77b62013253e02e1a69bc6a505bb37757fb7
currencies=USD, KRW, AZN, RUB
desktop_id=b86eac8e-dc89-4a9e-a5e8-7f85db011391
profile_id=c4552156-d179-4a79-9e91-587038c95cf0
refresh_token=2cdf2bd7-9d5f-463f-98f4-fce79a9ee9a4
token_expires_at=1772248335
refresh_token=ff49cde7-4f3a-4efa-ad46-84514fc03bf6
token_expires_at=1772418050
version=0.0.1
[db]

View File

@ -271,6 +271,35 @@ void DatabaseManager::initSchema() {
q.exec("PRAGMA user_version = 7");
qDebug() << "[DB] Migration 7 applied";
}
// ── Версия 8: receipt_image в transactions ──────────────────────────────────
if (version < 8) {
db.transaction();
if (!columnExists(q, "transactions", "receipt_image")) {
q.exec("ALTER TABLE transactions ADD COLUMN receipt_image BLOB");
}
db.commit();
q.exec("PRAGMA user_version = 8");
qDebug() << "[DB] Migration 8 applied";
}
// ── Версия 9: таблица banks ───────────────────────────────────────────────
if (version < 9) {
db.transaction();
q.exec(R"(CREATE TABLE IF NOT EXISTS banks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
alias TEXT UNIQUE,
name TEXT,
nspk_name TEXT
))");
db.commit();
q.exec("PRAGMA user_version = 9");
qDebug() << "[DB] Migration 9 applied";
}
}
void DatabaseManager::closeConnection() {

Binary file not shown.

View File

@ -167,6 +167,30 @@ AccountInfo AccountDAO::getAccountById(const int accountId) {
return {};
}
QList<AccountInfo> AccountDAO::getAccountsByAppCode(const QString &appCode) {
QList<AccountInfo> accounts;
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency
FROM accounts
WHERE app_code = :app_code
)");
query.bindValue(":app_code", appCode);
if (!query.exec()) {
qWarning() << "Ошибка при получении accounts по app_code:" << query.lastError().text();
return accounts;
}
while (query.next()) {
accounts.append(parseAccount(query));
}
return accounts;
}
AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &appName, const QString &lastNumber) {
QSqlQuery query(DatabaseManager::instance().database());
@ -192,3 +216,27 @@ AccountInfo AccountDAO::findAppAccount(const QString &deviceId, const QString &a
return {};
}
AccountInfo AccountDAO::findByMaterialId(const QString &materialId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, app_code, last_numbers, card_number, material_id, amount, update_time, status, description, currency
FROM accounts
WHERE material_id = :material_id
LIMIT 1
)");
query.bindValue(":material_id", materialId);
if (!query.exec()) {
qWarning() << "Ошибка при поиске account по material_id:" << query.lastError().text();
return {};
}
if (query.next()) {
return parseAccount(query);
}
return {};
}

View File

@ -22,9 +22,13 @@ public:
[[nodiscard]] static AccountInfo getAccountById(int accountId);
[[nodiscard]] static QList<AccountInfo> getAccountsByAppCode(const QString &appCode);
[[nodiscard]] static AccountInfo findAppAccount(
const QString &deviceId,
const QString &appName,
const QString &lastNumber
);
[[nodiscard]] static AccountInfo findByMaterialId(const QString &materialId);
};

55
database/dao/BankDAO.cpp Normal file
View File

@ -0,0 +1,55 @@
#include "BankDAO.h"
#include <QSqlQuery>
#include <QSqlError>
#include <QVariant>
#include "DatabaseManager.h"
QList<BankInfo> BankDAO::getAll() {
QList<BankInfo> list;
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("SELECT * FROM banks ORDER BY alias");
if (!query.exec()) {
qWarning() << "Ошибка при получении banks:" << query.lastError().text();
return list;
}
while (query.next()) {
BankInfo bank;
bank.id = query.value("id").toInt();
bank.alias = query.value("alias").toString();
bank.name = query.value("name").toString();
bank.nspkName = query.value("nspk_name").toString();
list.append(bank);
}
return list;
}
void BankDAO::replaceAll(const QList<BankInfo> &banks) {
QSqlDatabase db = DatabaseManager::instance().database();
QSqlQuery query(db);
db.transaction();
query.exec("DELETE FROM banks");
query.prepare(R"(
INSERT INTO banks (alias, name, nspk_name)
VALUES (:alias, :name, :nspk_name)
)");
for (const BankInfo &bank : banks) {
query.bindValue(":alias", bank.alias);
query.bindValue(":name", bank.name);
query.bindValue(":nspk_name", bank.nspkName);
if (!query.exec()) {
qWarning() << "Ошибка при вставке bank:" << query.lastError().text();
}
}
db.commit();
}

11
database/dao/BankDAO.h Normal file
View File

@ -0,0 +1,11 @@
#pragma once
#include "db/BankInfo.h"
#include <QList>
class BankDAO {
public:
[[nodiscard]] static QList<BankInfo> getAll();
static void replaceAll(const QList<BankInfo> &banks);
};

View File

@ -11,7 +11,7 @@ EventInfo parseEvent(const QSqlQuery &query) {
event.id = query.value("id").toInt();
event.status = eventStatusFromString(query.value("status").toString());
event.type = eventTypeFromString(query.value("type").toString());
event.externalId = query.value("external_id").toInt();
event.externalId = query.value("external_id").toString();
event.accountId = query.value("account_id").toInt();
event.deviceId = query.value("device_id").toString();
event.amount = query.value("amount").toDouble();
@ -146,3 +146,20 @@ EventInfo EventDAO::getFirstWaitEventByDeviceId(const QString &key) {
}
return {};
}
bool EventDAO::existsActiveByExternalId(const QString &externalId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT 1 FROM events
WHERE external_id = :external_id
AND status IN ('wait', 'iPprogress')
LIMIT 1
)");
query.bindValue(":external_id", externalId);
if (!query.exec()) {
qCritical() << "Ошибка при existsActiveByExternalId:" << query.lastError().text();
return false;
}
return query.next();
}

View File

@ -15,4 +15,6 @@ public:
[[nodiscard]] static QList<EventInfo> getAllEvents(const EventStatus &status);
[[nodiscard]] static EventInfo getFirstWaitEventByDeviceId(const QString &key);
[[nodiscard]] static bool existsActiveByExternalId(const QString &externalId);
};

View File

@ -16,17 +16,19 @@ bool TransactionDAO::insertTransactionIfNotExists(const TransactionInfo &info) {
SELECT COUNT(*) FROM transactions
WHERE account_id = :account_id AND amount = :amount AND timestamp = :timestamp AND type = :type
)");
query.bindValue(":timestamp", DateUtils::toUtcString(info.timestamp));
} else {
query.prepare(R"(
SELECT COUNT(*) FROM transactions
WHERE account_id = :account_id AND amount = :amount AND bankTime = :bankTime AND type = :type
WHERE account_id = :account_id AND amount = :amount AND bank_time = :bank_time AND bank_name = :bank_name
)");
query.bindValue(":bank_time", info.bankTime);
query.bindValue(":bank_name", info.bankName);
}
query.bindValue(":account_id", info.accountId);
query.bindValue(":amount", info.amount);
query.bindValue(":type", transactionTypeToString(info.type));
query.bindValue(":timestamp", DateUtils::toUtcString(info.timestamp));
if (!query.exec() || !query.next()) {
qWarning() << "Ошибка при проверке существования transaction:" << query.lastError().text();
@ -140,6 +142,7 @@ TransactionInfo parseTransaction(const QSqlQuery &query) {
tx.updateTime = DateUtils::fromUtcString(query.value("update_time").toString());
tx.completeTime = DateUtils::fromUtcString(query.value("complete_time").toString());
tx.timestamp = DateUtils::fromUtcString(query.value("timestamp").toString());
tx.receiptImage = query.value("receipt_image").toByteArray();
return tx;
}
@ -344,3 +347,37 @@ bool TransactionDAO::updateExternalTransactionId(const int id, const int externa
}
return true;
}
bool TransactionDAO::updateReceiptImage(const int id, const QByteArray &image) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE transactions SET receipt_image = :image WHERE id = :id");
query.bindValue(":id", id);
query.bindValue(":image", image);
if (!query.exec()) {
qWarning() << "Ошибка при сохранении receipt_image:" << query.lastError().text();
return false;
}
return true;
}
TransactionInfo TransactionDAO::findByBankTimeAndAmount(
const int accountId, const QString &bankName, const QString &bankTime, const double amount
) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT * FROM transactions
WHERE account_id = :account_id AND bank_name = :bank_name
AND bank_time = :bank_time AND amount = :amount
LIMIT 1
)");
query.bindValue(":account_id", accountId);
query.bindValue(":bank_name", bankName);
query.bindValue(":bank_time", bankTime);
query.bindValue(":amount", amount);
if (!query.exec() || !query.next()) {
return {};
}
return parseTransaction(query);
}

View File

@ -26,4 +26,10 @@ public:
);
[[nodiscard]] static bool updateExternalTransactionId(int id, int externalId);
[[nodiscard]] static bool updateReceiptImage(int id, const QByteArray &image);
// Найти транзакцию по bank_name + bank_time + amount
[[nodiscard]] static TransactionInfo findByBankTimeAndAmount(
int accountId, const QString &bankName, const QString &bankTime, double amount);
};

65
docs/api/auth.md Normal file
View File

@ -0,0 +1,65 @@
# Auth API
[← Главная](../index.md)
Авторизация через API-ключ и управление токенами.
Все защищённые эндпоинты требуют заголовок:
```
Authorization: Bearer <access_token>
```
---
## POST `/api/v1/auth/login`
Получение токена по API-ключу.
**Тело запроса:**
```json
{
"api_key": "string"
}
```
**Ответ:**
```json
{
"success": true,
"data": {
"id": "uuid",
"access_token": "string",
"refresh_token": "string",
"expires_in": 3600.0
}
}
```
---
## POST `/api/v1/auth/refresh_token`
Обновление access_token по refresh_token.
**Тело запроса:**
```json
{
"refresh_token": "string"
}
```
**Ответ:** аналогичен `/login`.
---
## Хранение токенов в проекте
Токены сохраняются в `config.ini`:
| Ключ | Значение |
|------|----------|
| `common/access_token` | Bearer-токен для запросов |
| `common/refresh_token` | Для обновления |
| `common/token_expires_at` | Unix timestamp истечения |
Рефреш токена происходит автоматически за 30 минут до истечения — см. `NetworkService::scheduleNextRefresh()`.

111
docs/api/device.md Normal file
View File

@ -0,0 +1,111 @@
# Device & Desktop API
[← Главная](../index.md)
Управление десктопами (установками приложения) и устройствами (подключёнными Android телефонами).
---
## Desktop
Desktop — это одна установка ARCDeskProject. Каждый desktop содержит список устройств.
### GET `/api/v1/desktop/` 🔒
Получить все десктопы профиля.
### POST `/api/v1/desktop/` 🔒
Зарегистрировать новый десктоп.
**Тело запроса:**
```json
{
"blue_token": "string",
"name": "Мой компьютер",
"contact": "string",
"app_version": "1.0.0"
}
```
**Ответ** содержит `id` (UUID) и `unique_name`.
### PATCH `/api/v1/desktop/{desktop_id}` 🔒
Обновить данные десктопа (версию, session_time и т.д.).
---
## Device
Device — физическое Android-устройство, подключённое к десктопу через ADB.
### GET `/api/v1/device/{desktop_id}` 🔒
Получить все устройства десктопа.
### POST `/api/v1/device/` 🔒
Зарегистрировать новое устройство.
**Тело запроса:**
```json
{
"desktop_id": "uuid",
"status": "ONLINE",
"name": "Pixel 6",
"screen_width": 1080,
"screen_height": 2400,
"battery": 85
}
```
**`DeviceStatus`:** `"ONLINE"` | `"OFFLINE"`
**Ответ** содержит `id` (UUID) — сохраняется как `api_id` в таблице `devices`.
### PATCH `/api/v1/device/{device_id}` 🔒
Обновить статус, battery, update_time устройства.
```json
{
"status": "ONLINE",
"battery": 72,
"update_time": 1700000000
}
```
### POST `/api/v1/device/add_screenshot/{device_id}` 🔒
Загрузить скриншот экрана устройства.
**Тип запроса:** `multipart/form-data`
| Поле | Тип | Описание |
|------|-----|----------|
| `file` | binary | JPEG-скриншот |
**Ответ:**
```json
{
"success": true,
"data": {
"id": "uuid",
"device_id": "uuid",
"screenshot_url": "https://...",
"taken_at": "2024-01-01T12:00:00"
}
}
```
---
## Связь с кодом проекта
| API | Метод в коде |
|-----|-------------|
| `POST /api/v1/desktop/` | `NetworkService::loginAndSetup()` |
| `POST /api/v1/device/` | `NetworkService::createDevice()` |
| `PATCH /api/v1/device/{id}` | `NetworkService::updateDevice()` |
| `POST /api/v1/device/add_screenshot/{id}` | `NetworkService::postDeviceScreenshot()` |

127
docs/api/profile.md Normal file
View File

@ -0,0 +1,127 @@
# Profile API
[← Главная](../index.md)
Управление профилем, банковскими профилями и материалами (картами).
---
## GET `/api/v1/profile/current_profile` 🔒
Получить текущий профиль со всеми банковскими профилями.
**Ответ:**
```json
{
"success": true,
"data": {
"id": "uuid",
"api_key": "string",
"bank_profiles": [
{
"id": "uuid",
"bank_profile_external_id": 1,
"first_name": "string",
"last_name": "string",
"middle_name": "string",
"bank_name": "ozon",
"phone_number": "+7...",
"balance": "15000",
"state": "active",
"currency": 643,
"profile_id": "uuid",
"materials": [ ... ],
"created_at": "2024-01-01T00:00:00",
"updated_at": "2024-01-01T00:00:00"
}
]
}
}
```
---
## POST `/api/v1/profile/fetch_bank_profiles` 🔒
Получить список всех банковских профилей.
---
## POST `/api/v1/profile/fetch_materials` 🔒
Получить список всех материалов (карт).
---
## Bank Profile
### POST `/api/v1/profile/bank_profile` 🔒
Создать банковский профиль.
**Тело запроса:**
```json
{
"first_name": "Иван",
"last_name": "Иванов",
"middle_name": "Иванович",
"bank_name": "ozon",
"phone_number": "+79001234567",
"balance": "15000",
"state": "active",
"currency": 643
}
```
**Ответ** содержит `id` (UUID) — сохраняется как `bank_profile_id` в таблице `applications`.
### PATCH `/api/v1/profile/bank_profile/{bank_profile_id}` 🔒
Обновить данные банковского профиля (любые поля опциональны).
### PATCH `/api/v1/profile/bank_profile/{bank_profile_id}/{is_enable}` 🔒
Включить (`true`) / отключить (`false`) банковский профиль.
---
## Material (карта)
Material — это карта/счёт внутри банковского профиля.
### POST `/api/v1/profile/material` 🔒
Создать материал.
**Тело запроса:**
```json
{
"bank_profile_id": "uuid",
"card_number": "1234567890123456",
"name": "Основная карта"
}
```
**Ответ** содержит `id` (UUID) и `external_material_id` (int) — сохраняется как `material_id` в таблице `accounts`.
### PATCH `/api/v1/profile/material/{material_id}` 🔒
Обновить номер карты материала.
```json
{ "card_number": "1234567890123456" }
```
### PATCH `/api/v1/profile/material/{material_id}/{is_enable}` 🔒
Включить / отключить материал.
---
## Связь с кодом проекта
| API | Метод в коде |
|-----|-------------|
| `POST /api/v1/profile/bank_profile` | `NetworkService::postBankProfile()` |
| `PATCH /api/v1/profile/bank_profile/{id}` | `NetworkService::patchBankProfile()` |
| `POST /api/v1/profile/material` | `NetworkService::postMaterial()` |

181
docs/api/task.md Normal file
View File

@ -0,0 +1,181 @@
# Task API
[← Главная](../index.md)
Управление задачами: получение очереди, добавление задач, отправка результатов.
---
## GET `/api/v1/task/get_tasks` 🔒
Получить список задач в очереди для текущего профиля.
**Ответ:**
```json
{
"success": true,
"data": [
{
"bank_name": "ozon",
"material_id": 42,
"type": "FETCH_PROFILE",
"body": {}
},
{
"bank_name": "birbank",
"material_id": 7,
"type": "FETCH_TRANSACTIONS",
"body": {}
},
{
"bank_name": "ozon",
"material_id": 5,
"type": "CREATE_TRANSACTION",
"body": {
"amount": 1000,
"currency": 643,
"type": "bank",
"bank_participator_id": "string",
"extra": {}
}
}
]
}
```
**Типы задач:**
| `type` | Описание | `body` |
|--------|----------|--------|
| `FETCH_PROFILE` | Получить баланс и данные аккаунта | пустой `{}` |
| `FETCH_TRANSACTIONS` | Получить историю транзакций | пустой `{}` |
| `CREATE_TRANSACTION` | Выполнить перевод | `CreateTransactionBody` |
**`CreateTransactionBody`:**
```json
{
"amount": 1000,
"currency": 643,
"type": "bank",
"bank_participator_id": "string",
"extra": {}
}
```
- `currency` — ISO 4217 numeric код валюты (643 = RUB, 840 = USD, 944 = AZN, 410 = KRW)
- `type``"bank"` или `"top-up"`
---
## POST `/api/v1/task/task_result` 🔒
Отправить результат выполненной задачи.
Тело запроса зависит от типа задачи (discriminator — поле `type`).
### FETCH_PROFILE
```json
{
"type": "FETCH_PROFILE",
"material_id": 42,
"bank_name": "ozon",
"data": {
"bank_account_id": "string",
"currency": 643,
"amount": 15000,
"extra": {}
}
}
```
### FETCH_TRANSACTIONS
```json
{
"type": "FETCH_TRANSACTIONS",
"material_id": 7,
"bank_name": "birbank",
"data": [
{
"bank_transaction_id": "string",
"amount": 500,
"currency": 944,
"transaction_type": "bank",
"bank_participator_id": "string",
"status": "completed",
"extra": {},
"created_at": "2024-01-01T12:00:00"
}
]
}
```
### CREATE_TRANSACTION
```json
{
"type": "CREATE_TRANSACTION",
"material_id": 5,
"bank_name": "ozon",
"data": {
"bank_transaction_id": "string",
"amount": 1000,
"currency": 643,
"transaction_type": "bank",
"bank_participator_id": "string",
"status": "completed",
"extra": {}
}
}
```
**`TransactionStatusEnum`:** `"progress"` | `"completed"` | `"declined"`
---
## POST `/api/v1/task/add_task`
Добавить задачу в очередь (вызывается внешней системой, не клиентом).
---
## POST `/api/v1/task/add_bank`
Зарегистрировать банк с webhook-URL.
**Query параметры:**
| Параметр | Тип | Описание |
|----------|-----|----------|
| `bank_name` | string | Название банка |
| `webhook_url` | uri | URL для уведомлений |
---
## Ошибка выполнения задачи
Если задача не может быть выполнена (аккаунт не найден, скрипт упал), в поле `data` передаётся:
```json
{
"type": "FETCH_PROFILE",
"material_id": 42,
"bank_name": "ozon",
"data": {
"status": "error",
"description": "Account not found for material_id: 42"
}
}
```
---
## Связь с кодом проекта
| API | Метод в коде |
|-----|-------------|
| `GET /api/v1/task/get_tasks` | `NetworkService::getTasks()` |
| `POST /api/v1/task/task_result` | `NetworkService::postTaskResult()` |
Хелпер в `EventHandler`: `sendTaskResult(type, materialId, bankName, data)` — создаёт `NetworkService` в отдельном потоке и вызывает `postTaskResult()`.

237
docs/events.md Normal file
View File

@ -0,0 +1,237 @@
# События и задачи
[← Главная](index.md)
Описание полного цикла задачи: от получения из API до завершения и отчёта.
---
## Ключевые файлы
| Файл | Роль |
|------|------|
| `services/EventHandler.h/cpp` | Оркестратор: поллинг, сохранение в БД, запуск потоков |
| `services/net/NetworkService.h/cpp` | HTTP-клиент: `getTasks()`, `postTaskResult()` |
| `database/dao/EventDAO.h/cpp` | CRUD для таблицы `events` |
| `models/db/EventInfo.h` | Модель задачи |
---
## Жизненный цикл задачи
```
Wait → InProgress → Complete
↘ Error
```
---
## Схема работы
### 1. Получение задач (каждые 5 секунд)
```
QTimer (5s)
└─→ NetworkService::getTasks()
└─→ GET /api/v1/task/get_tasks (Bearer token)
└─→ signal tasksReceived(QJsonArray)
└─→ EventHandler::onTasksReceived()
```
Формат ответа API:
```json
{
"success": true,
"data": [
{ "bank_name": "ozon", "material_id": "c340cd2e-cefd-48d6-a813-c4be097ae493", "type": "FETCH_PROFILE", "body": {} },
{ "bank_name": "birbank", "material_id": "a1b2c3d4-0000-0000-0000-000000000001", "type": "FETCH_TRANSACTIONS", "body": {} },
{ "bank_name": "ozon", "material_id": "a1b2c3d4-0000-0000-0000-000000000002", "type": "CREATE_TRANSACTION", "body": { "amount": 1000, "currency": 643 } }
]
}
```
> `material_id` — UUID строка. Поддерживается также числовой формат для совместимости (парсится через `task["material_id"].isString() ? toString() : QString::number(toInt())`).
### 2. Обработка задачи (`onTasksReceived`)
```
Для каждой задачи из API:
├─ existsActiveByExternalId(material_id)?
│ YES → пропустить (дедупликация)
│ NO → продолжить
├─ AccountDAO::getAccountsByAppCode(bank_name)
│ найти аккаунт где materialId == material_id
НЕ НАЙДЕН → sendTaskResult(status=error) и пропустить
└─ EventDAO::insertEvent(status=Wait)
```
Маппинг типов задач:
| Тип в API | `EventType` в коде | Скрипт |
|-----------|--------------------|--------|
| `FETCH_PROFILE` | `FetchProfile` | `Ozon::LoginAndCheckAccountsScript` |
| `FETCH_TRANSACTIONS` | `FetchTransactions` | `GetLastDaysHistoryBirbankScript` |
| `CREATE_TRANSACTION` | `CreateTransaction` | *(не реализован)* |
### 3. Запуск задач (тот же `onTasksReceived`)
```
Для каждого онлайн устройства:
├─ m_threads[deviceId] запущен? → пропустить
│ (одно устройство = один поток одновременно)
├─ EventDAO::getFirstWaitEventByDeviceId(deviceId)
│ самая старая Wait-задача (ORDER BY timestamp ASC)
├─ ApplicationDAO::getApplication() → статус == "active"?
├─ EventDAO::updateEvent(id, InProgress)
└─ startThreadForTask(account, event)
```
### 4. Выполнение скрипта (QThread)
```
QThread
└─→ Script::start()
├─ ADB команды (adb shell, screenshot, xml dump)
├─ ScreenXmlParser (парсинг UI)
├─ Tesseract OCR (при необходимости)
└─ emit finishedWithResult(error или "")
└─→ EventHandler::updateEventThread()
```
> **Watchdog:** если скрипт не завершился за **5 минут** — поток принудительно завершается.
### 5. Завершение задачи (`updateEventThread`)
```
result == "" → EventDAO::updateEvent(Complete)
result != "" → EventDAO::updateEvent(Error, message)
└─→ sendEventStatus() (новый QThread)
└─→ POST /api/v1/event/status ⚠️ устаревший эндпоинт, требует замены на task_result
```
---
## Отправка результата задачи
Используется `POST /api/v1/task/task_result` через `NetworkService::postTaskResult()`.
### При ненайденном аккаунте (material_id не в БД)
```json
{
"type": "FETCH_PROFILE",
"material_id": 42,
"bank_name": "ozon",
"data": {
"status": "error",
"description": "Account not found for material_id: 42"
}
}
```
### При успешном выполнении — FETCH_PROFILE
```json
{
"type": "FETCH_PROFILE",
"material_id": 42,
"bank_name": "ozon",
"data": {
"bank_account_id": "string",
"currency": 643,
"amount": 15000,
"extra": {}
}
}
```
### При успешном выполнении — FETCH_TRANSACTIONS
```json
{
"type": "FETCH_TRANSACTIONS",
"material_id": 7,
"bank_name": "birbank",
"data": [
{
"bank_transaction_id": "string",
"amount": 500,
"currency": 944,
"transaction_type": "bank",
"bank_participator_id": "string",
"status": "completed",
"extra": {},
"created_at": "2024-01-01T12:00:00"
}
]
}
```
### При ошибке выполнения скрипта
```json
{
"type": "FETCH_PROFILE",
"material_id": 42,
"bank_name": "ozon",
"data": {
"status": "error",
"description": "Cant open the home screen: emulator-5554 ozon"
}
}
```
---
## Модель `EventInfo`
```cpp
struct EventInfo {
int id; // локальный ID в SQLite
int externalId; // material_id из API
int accountId; // FK → accounts
QString deviceId; // ADB device id
double amount;
QString phone;
QString bankName;
QString comment;
QByteArray screenshot; // последний скриншот экрана
EventStatus status; // Wait / InProgress / Complete / Error
EventType type; // FetchProfile / FetchTransactions / CreateTransaction
QDateTime timestamp;
QDateTime updateTime;
QDateTime completeTime;
};
```
---
## Дедупликация
`EventDAO::existsActiveByExternalId(int externalId)` — проверяет наличие записи
с `status IN ('wait', 'iPprogress')` для данного `external_id`.
Гарантирует что одна и та же задача не попадёт в очередь дважды
пока не завершится предыдущее выполнение.
---
## Ограничения
- **1 поток на устройство** — пока устройство занято, остальные задачи ждут следующего поллинга
- **Очерёдность** — внутри устройства задачи выполняются по `timestamp ASC`
- **Интервал поллинга** — 5 секунд (`m_pollTimer->setInterval`)
- **Пути API** — настраиваются в `config.ini`:
- `net/pathGetTasks` (дефолт `/api/v1/task/get_tasks`)
- `net/pathTaskResult` (дефолт `/api/v1/task/task_result`)

42
docs/index.md Normal file
View File

@ -0,0 +1,42 @@
# ARCDeskProject — Документация
Десктопное Qt6/C++ приложение для мониторинга и автоматизации Android банковских приложений через ADB.
---
## Оглавление
### Архитектура приложения
| Раздел | Описание |
|--------|----------|
| [События и задачи](events.md) | Очередь задач: получение из API, хранение в БД, выполнение скриптов |
### API Backend (`Adb Core Backend v1.0.0`)
| Раздел | Описание |
|--------|----------|
| [Авторизация](api/auth.md) | Логин, refresh token, хранение токенов |
| [Profile & Material](api/profile.md) | Банковские профили и карты (материалы) |
| [Device & Desktop](api/device.md) | Регистрация десктопов и Android-устройств, скриншоты |
| [Task](api/task.md) | Получение задач, отправка результатов |
---
## Общая архитектура приложения
```
MainWindow (Qt6 GUI)
├── DeviceScreener (QThread) — опрос ADB устройств, скриншоты
├── EventHandler (QThread) — обработка задач, запуск скриптов
└── NetworkService (QThread) — синхронизация с REST API (localhost:9999/api/v1/)
Скрипты: android/{rshb,birbank,toss,ozon,ziraat}/
Database: SQLite — DatabaseManager (singleton) + DAO-слой
```
## Технологии
- **Qt6** — Widgets, Network, Sql, Xml
- **C++17**, SQLite, ADB, Tesseract OCR
- CMake

View File

@ -18,6 +18,7 @@
#include "EventHandler.h"
#include "net/NetworkService.h"
#include "ozon/PayByPhoneScript.h"
#include "widget/loader/ErrorWindow.h"
#include "widget/loader/LoaderWindow.h"
#include "widget/loader/RegistrationWindow.h"
@ -67,6 +68,35 @@ void startEventHandler(const QCoreApplication &app) {
thread->start();
}
void startPayByPhoneTest() {
// Берём первый девайс с ozon аккаунтом
QList<AccountInfo> accounts = AccountDAO::getAccountsByAppCode("ozon");
if (accounts.isEmpty()) {
qWarning() << "[PayByPhoneTest] No ozon accounts found";
return;
}
AccountInfo account = accounts.first();
qDebug() << "[PayByPhoneTest] Using account:" << account.id << account.deviceId << account.lastNumbers;
auto *thread = new QThread(nullptr);
auto *worker = new Ozon::PayByPhoneScript(
account,
"+79855532896",
"Альфа-Банк",
150,
nullptr
);
worker->moveToThread(thread);
QObject::connect(thread, &QThread::started, worker, &Ozon::PayByPhoneScript::start);
QObject::connect(worker, &Ozon::PayByPhoneScript::finished, thread, &QThread::quit);
QObject::connect(worker, &Ozon::PayByPhoneScript::finished, worker, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
QObject::connect(worker, &Ozon::PayByPhoneScript::finishedWithResult, [](const QString &result) {
qDebug() << "[PayByPhoneTest] Result:" << result;
});
thread->start();
}
void startNetworkService(const QCoreApplication &app) {
auto *paymentThread = new QThread;
auto *networkService = new NetworkService;
@ -179,6 +209,7 @@ int main(int argc, char *argv[]) {
startNetworkService(app);
startDeviceScreener(app);
startEventHandler(app);
startPayByPhoneTest();
}, Qt::QueuedConnection);
};
@ -206,6 +237,7 @@ int main(int argc, char *argv[]) {
startNetworkService(app);
startDeviceScreener(app);
startEventHandler(app);
startPayByPhoneTest();
} else if (r == 0) {
QSettings s("config.ini", QSettings::IniFormat);
s.remove("common/api_key");
@ -226,6 +258,7 @@ int main(int argc, char *argv[]) {
startNetworkService(app);
startDeviceScreener(app);
startEventHandler(app);
startPayByPhoneTest();
} else if (result == 0) {
QSettings s("config.ini", QSettings::IniFormat);
s.remove("common/api_key");

10
models/db/BankInfo.h Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include <QString>
struct BankInfo {
int id = -1;
QString alias;
QString name;
QString nspkName;
};

View File

@ -12,11 +12,9 @@ enum class EventStatus {
};
enum class EventType {
Phone,
Card,
Balance,
History,
FetchProfile,
FetchTransactions,
CreateTransaction,
Unknown
};
@ -41,27 +39,23 @@ inline EventStatus eventStatusFromString(const QString &str) {
inline QString eventTypeToString(const EventType type) {
switch (type) {
case EventType::Phone: return "phone";
case EventType::Card: return "card";
case EventType::Balance: return "balance";
case EventType::History: return "history";
case EventType::FetchProfile: return "fetch_profile";
case EventType::FetchProfile: return "FETCH_PROFILE";
case EventType::FetchTransactions: return "FETCH_TRANSACTIONS";
case EventType::CreateTransaction: return "CREATE_TRANSACTION";
default: return "unknown";
}
}
inline EventType eventTypeFromString(const QString &str) {
if (str == "phone") return EventType::Phone;
if (str == "card") return EventType::Card;
if (str == "balance") return EventType::Balance;
if (str == "history") return EventType::History;
if (str == "fetch_profile") return EventType::FetchProfile;
if (str == "FETCH_PROFILE") return EventType::FetchProfile;
if (str == "FETCH_TRANSACTIONS") return EventType::FetchTransactions;
if (str == "CREATE_TRANSACTION") return EventType::CreateTransaction;
return EventType::Unknown;
}
struct EventInfo {
int id = -1;
int externalId = -1;
QString externalId;
int accountId = -1;
QString deviceId;
@ -73,6 +67,8 @@ struct EventInfo {
EventStatus status = EventStatus::Unknown;
EventType type = EventType::Unknown;
QByteArray screenshot;
QDateTime updateTime;
QDateTime completeTime;
QDateTime timestamp;

View File

@ -1,6 +1,7 @@
#pragma once
#include <QString>
#include <QByteArray>
#include <QDateTime>
enum class TransactionType {
@ -69,6 +70,8 @@ struct TransactionInfo {
QDateTime updateTime;
QDateTime completeTime;
QDateTime timestamp;
QByteArray receiptImage;
};
inline QString convertTransactionToString(const TransactionInfo &tx) {

View File

@ -1,6 +1,8 @@
#include "EventHandler.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QMap>
#include <QThread>
#include <QTimer>
@ -12,48 +14,124 @@
#include "db/ApplicationInfo.h"
#include "net/NetworkService.h"
#include "birbank/GetLastDaysHistoryBirbankScript.h"
#include "birbank/LoginAndCheckAccountsBirbankScript.h"
#include "ozon/GetLastTransactionsScript.h"
#include "ozon/LoginAndCheckAccountsScript.h"
#include "rshb/CommonScript.h"
#include "rshb/GetLastDaysHistoryScript.h"
#include "rshb/LoginAndCheckAccountsScript.h"
#include "rshb/PayByPhoneScript.h"
#include "toss/GetLastDaysHistoryTossScript.h"
#include "toss/LoginAndCheckAccountsTossScript.h"
#include "ziraat/GetLastDaysHistoryZiraatScript.h"
#include "ziraat/LoginAndCheckAccountsZiraatScript.h"
EventHandler::EventHandler(QObject *parent) : QObject(parent) {
m_checkTimer = new QTimer(this);
m_checkTimer->setInterval(1000);
connect(m_checkTimer, &QTimer::timeout, this, &EventHandler::checkAndRestart);
}
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName, const QJsonObject &data);
EventHandler::EventHandler(QObject *parent) : QObject(parent) {}
EventHandler::~EventHandler() = default;
void EventHandler::start() {
m_devices = DeviceDAO::getOnlineDeviceCodes();
m_network = new NetworkService(this);
connect(m_network, &NetworkService::tasksReceived, this, &EventHandler::onTasksReceived);
clearAll();
// Запустим сразу потоки для всех ключей
for (auto &key: m_devices) {
startThreadForKey(key);
m_pollTimer = new QTimer(this);
m_pollTimer->setInterval(5000);
connect(m_pollTimer, &QTimer::timeout, m_network, &NetworkService::getTasks);
m_pollTimer->start();
// Первый запрос сразу при старте
m_network->getTasks();
}
m_checkTimer->start();
void EventHandler::onTasksReceived(const QJsonArray &tasks) {
if (!m_running) return;
// 1. Сохраняем новые задачи в БД
for (const auto &taskVal : tasks) {
const QJsonObject task = taskVal.toObject();
const QString bankName = task["bank_name"].toString();
const QString materialId = task["material_id"].isString()
? task["material_id"].toString()
: QString::number(task["material_id"].toInt());
const QString typeStr = task["type"].toString();
const EventType eventType = eventTypeFromString(typeStr);
if (eventType == EventType::Unknown) {
qWarning() << "[EventHandler] Unknown task type:" << typeStr;
continue;
}
void EventHandler::checkAndRestart() {
if (!m_running) {
return;
}
m_devices = DeviceDAO::getOnlineDeviceCodes();
// Пропускаем если уже есть активная запись с таким external_id
if (EventDAO::existsActiveByExternalId(materialId)) continue;
for (const auto &key: m_devices) {
const QThread *thr = m_threads.value(key, nullptr);
if (!thr || !thr->isRunning()) {
startThreadForKey(key);
// Ищем аккаунт по material_id напрямую
const AccountInfo account = AccountDAO::findByMaterialId(materialId);
if (account.id == -1) {
qWarning() << "[EventHandler] Account not found for bank:" << bankName << "material_id:" << materialId;
QJsonObject data;
data["status"] = "error";
data["description"] = "Account not found for material_id: " + materialId;
sendTaskResult(typeStr, materialId, bankName, data);
continue;
}
EventInfo event;
event.status = EventStatus::Wait;
event.type = eventType;
event.externalId = materialId;
event.accountId = account.id;
event.deviceId = account.deviceId;
event.bankName = bankName;
event.timestamp = QDateTime::currentDateTimeUtc();
if (EventDAO::insertEvent(event) == -1) {
qCritical() << "[EventHandler] Failed to insert event, external_id:" << materialId;
}
}
void sendEventStatus(const QString &deviceId, const int externalId, const int eventId, const QString &status,
const QString &error) {
// 2. Для каждого онлайн устройства без активного потока — запускаем следующую задачу
const QStringList onlineDevices = DeviceDAO::getOnlineDeviceCodes();
for (const QString &deviceId : onlineDevices) {
if (const QThread *thr = m_threads.value(deviceId, nullptr); thr && thr->isRunning()) continue;
const EventInfo event = EventDAO::getFirstWaitEventByDeviceId(deviceId);
if (event.id == -1) continue;
const AccountInfo account = AccountDAO::getAccountById(event.accountId);
const ApplicationInfo app = ApplicationDAO::getApplication(deviceId, account.appCode);
if (app.status != "active") continue;
if (!EventDAO::updateEvent(event.id, EventStatus::InProgress, "")) {
qCritical() << "[EventHandler] Failed to set InProgress, event id:" << event.id;
continue;
}
startThreadForTask(account, event);
}
}
static void sendTaskResult(const QString &type, const QString &materialId, const QString &bankName,
const QJsonObject &data) {
QJsonObject obj;
obj["type"] = type;
obj["material_id"] = materialId; // UUID string
obj["bank_name"] = bankName;
obj["data"] = data;
auto *thread = new QThread;
auto *networkService = new NetworkService;
networkService->moveToThread(thread);
networkService->setPayload(QJsonDocument(obj).toJson());
QObject::connect(thread, &QThread::started, networkService, &NetworkService::postTaskResult);
QObject::connect(networkService, &NetworkService::finished, thread, &QThread::quit);
QObject::connect(networkService, &NetworkService::finished, networkService, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
static void sendEventStatus(const QString &deviceId, const int externalId, const int eventId,
const QString &status, const QString &error) {
QJsonObject obj;
obj["id"] = externalId;
obj["externalId"] = eventId;
@ -73,31 +151,19 @@ void sendEventStatus(const QString &deviceId, const int externalId, const int ev
}
void EventHandler::updateEventThread(const QString &key, const EventInfo &event, const QString &result) {
if (!result.isEmpty()) {
if (!EventDAO::updateEvent(event.id, EventStatus::Error, result)) {
qCritical() << "Cant update event: " << event.id;
const EventStatus newStatus = result.isEmpty() ? EventStatus::Complete : EventStatus::Error;
if (!EventDAO::updateEvent(event.id, newStatus, result)) {
qCritical() << "[EventHandler] Failed to update event:" << event.id;
} else {
sendEventStatus(
event.deviceId,
event.externalId,
event.id,
eventStatusToString(EventStatus::Error),
eventStatusToString(newStatus),
result
);
}
} else {
if (!EventDAO::updateEvent(event.id, EventStatus::Complete, "")) {
qCritical() << "Cant update event: " << event.id;
} else {
sendEventStatus(
event.deviceId,
event.externalId,
event.id,
eventStatusToString(EventStatus::Complete),
""
);
}
}
if (const auto tm = m_timers.take(key)) {
tm->stop();
@ -106,99 +172,59 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event,
m_threads.remove(key);
}
void EventHandler::startThreadForKey(const QString &key) {
const EventInfo event = EventDAO::getFirstWaitEventByDeviceId(key);
if (event.id == -1) {
return;
}
const AccountInfo account = AccountDAO::getAccountById(event.accountId);
const ApplicationInfo app = ApplicationDAO::getApplication(event.deviceId, account.appCode);
if (app.status != "active") {
return;
}
if (!EventDAO::updateEvent(event.id, EventStatus::InProgress, "")) {
qCritical() << "Cant update event: " << event.id;
return;
}
void EventHandler::startThreadForTask(const AccountInfo &account, const EventInfo &event) {
const auto thr = new QThread(this);
// qDebug() << "Start thread for key" << eventTypeToString(event.type);
if (event.type == EventType::History) {
if ("birbank" == account.appCode) {
auto *worker = new GetLastDaysHistoryBirbankScript(account);
const QString key = account.deviceId;
// Generic helper: подключает сигналы и переносит worker в поток
auto setup = [&](auto *worker) {
using W = std::remove_pointer_t<decltype(worker)>;
worker->moveToThread(thr);
connect(thr, &QThread::started, worker, &GetLastDaysHistoryBirbankScript::start);
connect(worker, &GetLastDaysHistoryBirbankScript::finished, thr, &QThread::quit);
connect(worker, &GetLastDaysHistoryBirbankScript::finished, worker, &QObject::deleteLater);
connect(thr, &QThread::started, worker, &W::start);
connect(worker, &W::finished, thr, &QThread::quit);
connect(worker, &W::finished, worker, &QObject::deleteLater);
connect(thr, &QThread::finished, thr, &QObject::deleteLater);
connect(worker, &GetLastDaysHistoryBirbankScript::finishedWithResult, this,
connect(worker, &W::finishedWithResult, this,
[this, key, event](const QString &result) {
updateEventThread(key, event, result);
}
);
}
// auto *worker = new Rshb::GetLastDaysHistoryScript(account);
// worker->moveToThread(thr);
//
// connect(thr, &QThread::started, worker, &Rshb::GetLastDaysHistoryScript::start);
// connect(worker, &Rshb::GetLastDaysHistoryScript::finished, thr, &QThread::quit);
// connect(worker, &Rshb::GetLastDaysHistoryScript::finished, worker, &QObject::deleteLater);
// connect(thr, &QThread::finished, thr, &QObject::deleteLater);
//
// connect(worker, &Rshb::GetLastDaysHistoryScript::finishedWithResult, this,
// [this, key, event](const QString &result) {
// updateEventThread(key, event, result);
// }
// );
}
};
const QString &appCode = account.appCode;
const QString &deviceId = account.deviceId;
if (event.type == EventType::FetchProfile) {
if ("ozon" == account.appCode) {
auto *worker = new Ozon::LoginAndCheckAccountsScript(event.deviceId, account.appCode);
worker->moveToThread(thr);
connect(thr, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, thr, &QThread::quit);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater);
connect(thr, &QThread::finished, thr, &QObject::deleteLater);
connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, this,
[this, key, event](const QString &result) {
updateEventThread(key, event, result);
}
);
}
if (appCode == "ozon") setup(new Ozon::LoginAndCheckAccountsScript(deviceId, appCode));
else if (appCode == "birbank") setup(new LoginAndCheckAccountsBirbankScript(deviceId, appCode));
else if (appCode == "rshb") setup(new Rshb::LoginAndCheckAccountsScript(deviceId, appCode));
else if (appCode == "toss") setup(new LoginAndCheckAccountsTossScript(deviceId, appCode));
else if (appCode == "ziraat") setup(new LoginAndCheckAccountsZiraatScript(deviceId, appCode));
else qWarning() << "[EventHandler] FETCH_PROFILE: unknown appCode:" << appCode;
}
if (event.type == EventType::Phone) {
// auto *worker = new Rshb::PayByPhoneScript(account, event.phone, event.bankName, event.amount);
// worker->moveToThread(thr);
//
// connect(thr, &QThread::started, worker, &Rshb::PayByPhoneScript::start);
// connect(worker, &Rshb::PayByPhoneScript::finished, thr, &QThread::quit);
// connect(worker, &Rshb::PayByPhoneScript::finished, worker, &QObject::deleteLater);
// connect(thr, &QThread::finished, thr, &QObject::deleteLater);
//
// connect(worker, &Rshb::PayByPhoneScript::finishedWithResult, this,
// [this, key, event](const QString &result) {
// updateEventThread(key, event, result);
// }
// );
if (event.type == EventType::FetchTransactions) {
if (appCode == "birbank") setup(new GetLastDaysHistoryBirbankScript(account));
else if (appCode == "ozon") setup(new Ozon::GetLastTransactionsScript(deviceId, appCode));
else if (appCode == "rshb") setup(new Rshb::GetLastDaysHistoryScript(account));
else if (appCode == "toss") setup(new GetLastDaysHistoryTossScript(account));
else if (appCode == "ziraat") setup(new GetLastDaysHistoryZiraatScript(account));
else qWarning() << "[EventHandler] FETCH_TRANSACTIONS: unknown appCode:" << appCode;
}
// Таймер на 5 минут — если не успел finish(), убить и перезапустить
if (event.type == EventType::CreateTransaction) {
// TODO: реализовать CREATE_TRANSACTION
}
// Watchdog-таймер: 5 минут — убиваем зависший поток
const auto timer = new QTimer(this);
timer->setSingleShot(true);
connect(timer, &QTimer::timeout, this, [this, key]() {
qWarning() << "Timeout for key" << key << "- killing stuck thread.";
qWarning() << "[EventHandler] Timeout for key" << key << "- killing stuck thread.";
if (QThread *thread = m_threads.value(key); thread && thread->isRunning()) {
thread->requestInterruption(); // мягкое прерывание
thread->requestInterruption();
if (!thread->wait(500)) {
qWarning() << "Hard terminate for key" << key;
qWarning() << "[EventHandler] Hard terminate for key" << key;
thread->terminate();
thread->wait();
}
@ -209,7 +235,6 @@ void EventHandler::startThreadForKey(const QString &key) {
m_threads.remove(key);
});
// Сохраняем и запускаем
m_threads[key] = thr;
m_timers[key] = timer;
@ -218,7 +243,6 @@ void EventHandler::startThreadForKey(const QString &key) {
}
void EventHandler::clearAll() {
// Останавливаем и удаляем старые
for (QThread *thread : m_threads) {
if (thread->isRunning()) {
thread->requestInterruption();
@ -238,6 +262,6 @@ void EventHandler::clearAll() {
void EventHandler::stop() {
m_running = false;
m_checkTimer->stop();
if (m_pollTimer) m_pollTimer->stop();
clearAll();
}

View File

@ -1,10 +1,14 @@
#pragma once
#include <QJsonArray>
#include <QMap>
#include <QObject>
#include <QTimer>
#include "db/AccountInfo.h"
#include "db/EventInfo.h"
class NetworkService;
class EventHandler final : public QObject {
Q_OBJECT
@ -22,19 +26,18 @@ signals:
void finished();
private slots:
void checkAndRestart(); // вызывается по таймеру
void onTasksReceived(const QJsonArray &tasks);
private:
bool m_running = true;
QMap<QString, QThread *> m_threads;
QMap<QString, QTimer *> m_timers;
QTimer *m_checkTimer = nullptr;
QStringList m_devices;
QTimer *m_pollTimer = nullptr;
NetworkService *m_network = nullptr;
void clearAll();
void updateEventThread(const QString &key, const EventInfo &event, const QString &result);
void startThreadForKey(const QString &key);
void startThreadForTask(const AccountInfo &account, const EventInfo &event);
};

View File

@ -33,6 +33,8 @@ NetworkService::NetworkService(QObject *parent) : QObject(parent) {
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_pathGetTasks = settings.value("net/pathGetTasks", "/api/v1/task/get_tasks").toString();
m_pathTaskResult = settings.value("net/pathTaskResult", "/api/v1/task/task_result").toString();
m_manager = new QNetworkAccessManager(this);
m_token = settings.value("common/token").toString();
m_accessToken = settings.value("common/access_token").toString();
@ -586,14 +588,60 @@ void NetworkService::scheduleNextRefresh() {
});
}
void NetworkService::getTasks() {
if (m_fetchingTasks) return;
m_fetchingTasks = true;
const QSettings settings("config.ini", QSettings::IniFormat);
const QString accessToken = settings.value("common/access_token").toString();
QUrl url(m_apiBase + m_pathGetTasks);
QNetworkRequest request(url);
request.setRawHeader("Authorization", ("Bearer " + accessToken).toUtf8());
QNetworkReply *reply = m_manager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
m_fetchingTasks = false;
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[NetworkService] getTasks error:" << reply->errorString();
return;
}
const QByteArray raw = reply->readAll();
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
qWarning() << "[NetworkService] getTasks: invalid JSON";
return;
}
const QJsonObject root = doc.object();
if (!root.value("success").toBool()) {
qWarning() << "[NetworkService] getTasks: success=false" << root;
return;
}
emit tasksReceived(root.value("data").toArray());
});
}
void NetworkService::postTaskResult() {
const QJsonObject body = QJsonDocument::fromJson(m_json).object();
const QJsonValue result = postJson(m_apiBase + m_pathTaskResult, body, true);
if (result.isNull() || result.isUndefined()) {
qWarning() << "[NetworkService] postTaskResult failed, body:" << QJsonDocument(body).toJson(QJsonDocument::Compact);
}
emit finished();
}
void NetworkService::start() {
m_running = true;
// Проверяем токен при старте и планируем рефреш по времени жизни
ensureValidToken();
scheduleNextRefresh();
// TODO: GET /api/v1/task/get_tasks (polling)
}
void NetworkService::stop() {

View File

@ -1,5 +1,6 @@
#pragma once
#include <qhttpmultipart.h>
#include <QJsonArray>
#include <QNetworkAccessManager>
#include <QObject>
#include <QMap>
@ -47,6 +48,10 @@ public slots:
void updateDevice();
void getTasks();
void postTaskResult();
void start();
void stop();
@ -54,6 +59,7 @@ signals:
void finished();
void finishedWithResult(int value);
void tokenRefreshFailed();
void tasksReceived(QJsonArray tasks);
private:
QByteArray m_json;
@ -78,6 +84,9 @@ private:
QString m_pathDeviceScreenshot;
QString m_pathBankProfile;
QString m_pathMaterial;
QString m_pathGetTasks;
QString m_pathTaskResult;
bool m_fetchingTasks = false;
QNetworkAccessManager *m_manager;

View File

@ -7,6 +7,7 @@
#include <QStackedWidget>
#include "TutorialWindow.h"
#include "bank/BankListWindow.h"
#include "db/DeviceInfo.h"
#include "device/DeviceWidget.h"
#include "dao/DeviceDAO.h"
@ -35,9 +36,11 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
QMenu *menu = menuBar()->addMenu("Меню");
auto *devicesPageAction = new QAction("Активные устройства", this);
// auto *accountPageAction = new QAction("Аккаунты", this);
auto *bankListAction = new QAction("Банки", this);
auto *tutorialPageAction = new QAction("Инструкция", this);
menu->addAction(devicesPageAction);
// menu->addAction(accountPageAction);
menu->addAction(bankListAction);
menu->addAction(tutorialPageAction);
connect(devicesPageAction, &QAction::triggered, this, [=]() {
@ -52,6 +55,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
// deleteDevicePage(); // освобождаем ресурсы
// });
connect(bankListAction, &QAction::triggered, this, [=]() {
if (!m_bankListWindow) {
m_bankListWindow = new BankListWindow(this);
}
m_bankListWindow->show();
m_bankListWindow->raise();
m_bankListWindow->activateWindow();
});
connect(tutorialPageAction, &QAction::triggered, this, [=]() {
if (!m_tutorialWindow) {
m_tutorialWindow = new TutorialWindow(this);

View File

@ -26,6 +26,7 @@ private:
QWidget *m_devicesWindow = nullptr;
QWidget *m_accountsWindow = nullptr;
QWidget *m_tutorialWindow = nullptr;
QWidget *m_bankListWindow = nullptr;
void createDevicePage();

View File

@ -15,9 +15,11 @@
#include "ozon/LoginAndCheckAccountsScript.h"
#include "ozon/GetProfileInfoScript.h"
#include "ozon/GetCardInfoScript.h"
#include "ozon/GetLastTransactionsScript.h"
#include "toss/LoginAndCheckAccountsTossScript.h"
#include "dao/AccountDAO.h"
#include "dao/ApplicationDAO.h"
#include "bank/TransactionWindow.h"
#include "device/EditBankAccountDialog.h"
#include "device/EditBankDialog.h"
#include "net/NetworkService.h"
@ -205,8 +207,29 @@ AccountSettingsWindow::AccountSettingsWindow(
}
});
QPushButton *transactionsButton = new QPushButton("Проверить транзакции");
transactionsButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
transactionsButton->setStyleSheet("background-color: #1565C0; color: white;");
QObject::connect(transactionsButton, &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) {
startGetLastTransactions(m_applicationInfo.code);
close();
}
});
ozonRow->addWidget(profileButton);
ozonRow->addWidget(cardButton);
ozonRow->addWidget(transactionsButton);
ozonRow->addStretch();
layout->addLayout(ozonRow);
}
@ -322,6 +345,19 @@ void AccountSettingsWindow::startGetCardInfo(const QString &appCode) {
thread->start();
}
void AccountSettingsWindow::startGetLastTransactions(const QString &appCode) {
if (appCode != "ozon") return;
QThread *thread = new QThread(nullptr);
auto *worker = new Ozon::GetLastTransactionsScript(m_deviceId, appCode, nullptr);
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Ozon::GetLastTransactionsScript::start);
connect(worker, &Ozon::GetLastTransactionsScript::finished, thread, &QThread::quit);
connect(worker, &Ozon::GetLastTransactionsScript::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
@ -356,7 +392,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
tableWidget->clearContents();
tableWidget->setRowCount(accounts.length());
tableWidget->setColumnCount(7);
tableWidget->setColumnCount(8);
tableWidget->setHorizontalHeaderLabels({
"Время обновления",
"Номер",
@ -364,6 +400,7 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
"Сумма",
"Валюта",
"Статус",
"",
""
});
@ -371,7 +408,6 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
tableWidget->setTextElideMode(Qt::ElideNone);
tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::ResizeToContents);
tableWidget->verticalHeader()->setSectionResizeMode(6, QHeaderView::ResizeToContents);
tableWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
@ -413,32 +449,35 @@ void AccountSettingsWindow::reloadContent(QTableWidget *tableWidget) {
tableWidget->setItem(k, 5, status);
auto *editBankAccountBtn = new QPushButton("Редактировать", tableWidget);
tableWidget->setCellWidget(k, 6, editBankAccountBtn);
auto *deleteBankAccountBtn = new QPushButton("Удалить", tableWidget);
deleteBankAccountBtn->setStyleSheet("background-color: red; color: white;");
tableWidget->setCellWidget(k, 6, deleteBankAccountBtn);
connect(editBankAccountBtn, &QPushButton::clicked, this, [this, tableWidget, account]() {
EditBankAccountDialog dlg(account, this);
if (dlg.exec() == QDialog::Accepted) {
const QString cardNumber = dlg.cardNumberValue();
const QString currency = dlg.currencyValue();
bool on = dlg.toggled();
if (!AccountDAO::updateAccountById(account.id, on ? "active" : "off", cardNumber, currency)) {
qDebug() << "Cant update bank account data";
} else {
connect(deleteBankAccountBtn, &QPushButton::clicked, this, [this, tableWidget, account]() {
QMessageBox msgBox(this);
msgBox.setWindowTitle("Удалить аккаунт?");
msgBox.setText("Вы уверены, что хотите удалить аккаунт " + account.lastNumbers + "?");
QPushButton *deleteBtn = msgBox.addButton("Удалить", QMessageBox::AcceptRole);
QPushButton *cancelBtn = msgBox.addButton("Отмена", QMessageBox::RejectRole);
msgBox.setDefaultButton(cancelBtn);
msgBox.exec();
if (msgBox.clickedButton() == deleteBtn) {
if (AccountDAO::deleteAccount(account.id)) {
reloadContent(tableWidget);
AccountInfo accountInfo;
accountInfo.currency = currency;
accountInfo.description = cardNumber;
accountInfo.amount = account.amount;
accountInfo.currency = currency;
accountInfo.appCode = m_applicationInfo.code;
accountInfo.deviceId = m_deviceId;
accountInfo.status = on ? AccountStatus::Active : AccountStatus::Off;
accountInfo.lastNumbers = cardNumber.right(4);
updateAccountData(accountInfo);
tableWidget->resizeColumnsToContents();
} else {
qWarning() << "Cant delete account" << account.id;
}
}
});
auto *transactionsBtn = new QPushButton("Транзакции", tableWidget);
transactionsBtn->setStyleSheet("background-color: #1565C0; color: white;");
tableWidget->setCellWidget(k, 7, transactionsBtn);
connect(transactionsBtn, &QPushButton::clicked, this, [this, account]() {
auto *txWindow = new TransactionWindow(account.id, this);
txWindow->show();
});
}
}

View File

@ -27,6 +27,8 @@ private:
void startGetCardInfo(const QString &appCode);
void startGetLastTransactions(const QString &appCode);
void reloadContent(QTableWidget *tableWidget);
void sendAppStatus(const QString &deviceId, const QString &code, const QString &status);

View File

@ -0,0 +1,117 @@
#include "BankListWindow.h"
#include <QHeaderView>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QMessageBox>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QPushButton>
#include <QVBoxLayout>
#include "dao/BankDAO.h"
BankListWindow::BankListWindow(QWidget *parent)
: QWidget(parent,
Qt::Window
| Qt::WindowTitleHint
| Qt::WindowSystemMenuHint
| Qt::WindowCloseButtonHint
| Qt::WindowMinMaxButtonsHint) {
setWindowTitle("Банки");
setMinimumSize(600, 400);
resize(700, 500);
auto *layout = new QVBoxLayout(this);
auto *topRow = new QHBoxLayout();
auto *syncBtn = new QPushButton("Синхронизировать");
syncBtn->setStyleSheet("background-color: #1565C0; color: white;");
syncBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
topRow->addWidget(syncBtn);
topRow->addStretch();
layout->addLayout(topRow);
connect(syncBtn, &QPushButton::clicked, this, &BankListWindow::syncBanks);
m_tableWidget = new QTableWidget(this);
m_tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_tableWidget->setSelectionMode(QAbstractItemView::SingleSelection);
m_tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows);
m_tableWidget->setColumnCount(3);
m_tableWidget->setHorizontalHeaderLabels({"Alias", "Название", "NSPK"});
m_tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_tableWidget->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
layout->addWidget(m_tableWidget);
reloadTable();
}
void BankListWindow::reloadTable() {
QList<BankInfo> banks = BankDAO::getAll();
m_tableWidget->setRowCount(banks.size());
for (int i = 0; i < banks.size(); ++i) {
const BankInfo &b = banks[i];
auto *alias = new QTableWidgetItem(b.alias);
alias->setFlags(alias->flags() & ~Qt::ItemIsEditable);
m_tableWidget->setItem(i, 0, alias);
auto *name = new QTableWidgetItem(b.name);
name->setFlags(name->flags() & ~Qt::ItemIsEditable);
m_tableWidget->setItem(i, 1, name);
auto *nspk = new QTableWidgetItem(b.nspkName);
nspk->setFlags(nspk->flags() & ~Qt::ItemIsEditable);
m_tableWidget->setItem(i, 2, nspk);
}
}
void BankListWindow::syncBanks() {
auto *manager = new QNetworkAccessManager(this);
QNetworkRequest request(QUrl("https://api.brtservice.io/api/rest/info/"));
QNetworkReply *reply = manager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, manager]() {
if (reply->error() != QNetworkReply::NoError) {
QMessageBox::warning(this, "Ошибка", "Не удалось загрузить банки: " + reply->errorString());
reply->deleteLater();
manager->deleteLater();
return;
}
const QByteArray data = reply->readAll();
const QJsonDocument doc = QJsonDocument::fromJson(data);
QJsonArray array;
if (doc.isArray()) {
array = doc.array();
} else if (doc.isObject() && doc.object().contains("data")) {
array = doc.object()["data"].toArray();
}
QList<BankInfo> banks;
for (const QJsonValue &val : array) {
QJsonObject obj = val.toObject();
BankInfo bank;
bank.alias = obj["alias"].toString();
bank.name = obj["name"].toString();
bank.nspkName = obj["nspk_name"].toString();
if (!bank.alias.isEmpty()) {
banks.append(bank);
}
}
BankDAO::replaceAll(banks);
reloadTable();
qDebug() << "[BankListWindow] Synced" << banks.size() << "banks";
reply->deleteLater();
manager->deleteLater();
});
}

View File

@ -0,0 +1,17 @@
#pragma once
#include <QWidget>
#include <QTableWidget>
class BankListWindow final : public QWidget {
Q_OBJECT
public:
explicit BankListWindow(QWidget *parent = nullptr);
private:
QTableWidget *m_tableWidget;
void reloadTable();
void syncBanks();
};

View File

@ -1,7 +1,10 @@
#include "TransactionWindow.h"
#include <qabstractitemview.h>
#include <QDialog>
#include <QHeaderView>
#include <QPixmap>
#include <QPushButton>
#include <QVBoxLayout>
#include <QLabel>
#include <QScrollArea>
@ -46,8 +49,7 @@ QTableWidgetItem *createTableWidget(const QString &text) {
TransactionWindow::TransactionWindow(const int accountId, QWidget *parent): QDialog(parent) {
setWindowTitle("Транзакции");
setMinimumSize(1000, 600); // минимальный размер
setMaximumSize(1000, 600); // максимальный размер
setMinimumSize(800, 600);
auto *mainLayout = new QVBoxLayout(this);
@ -101,27 +103,27 @@ TransactionWindow::TransactionWindow(const int accountId, QWidget *parent): QDia
tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);
tableWidget->setRowCount(transactions.length());
tableWidget->setColumnCount(6);
tableWidget->setColumnCount(7);
tableWidget->setHorizontalHeaderLabels({
"Дата и время",
"Сумма",
"Банк",
"Телефон",
"Статус",
"Описание"
"Описание",
""
});
tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
for (int i = 0; i < 5; ++i) {
tableWidget->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
}
tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch);
// tableWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
tableWidget->setItemDelegateForColumn(1, new AmountDelegate(tableWidget));
for (int k = 0; k < transactions.size(); ++k) {
TransactionInfo tr = transactions[k];
tableWidget->setItem(k, 0, createTableWidget(tr.timestamp.toString("dd.MM.yyyy HH:mm:ss")));
tableWidget->setItem(k, 0, createTableWidget(tr.bankTime));
tableWidget->setItem(k, 1, createTableWidget(QString::number(tr.amount)));
tableWidget->setItem(k, 2, createTableWidget(tr.bankName));
tableWidget->setItem(k, 3, createTableWidget(tr.phone));
@ -130,12 +132,42 @@ TransactionWindow::TransactionWindow(const int accountId, QWidget *parent): QDia
QTableWidgetItem *item = new QTableWidgetItem(tr.description);
item->setTextAlignment(Qt::AlignLeft | Qt::AlignTop);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
item->setSizeHint(QSize(100, tr.description.length() / 2)); // Минимальная высота ячейки для переноса
item->setSizeHint(QSize(100, tr.description.length() / 2));
tableWidget->setItem(k, 5, item);
if (!tr.receiptImage.isEmpty()) {
auto *docBtn = new QPushButton("Документы", tableWidget);
docBtn->setStyleSheet("background-color: #1565C0; color: white;");
tableWidget->setCellWidget(k, 6, docBtn);
connect(docBtn, &QPushButton::clicked, this, [this, tr]() {
QPixmap pixmap;
pixmap.loadFromData(tr.receiptImage);
if (pixmap.isNull()) return;
auto *imgDialog = new QDialog(this);
imgDialog->setWindowTitle("Документ");
imgDialog->setAttribute(Qt::WA_DeleteOnClose);
auto *dlgLayout = new QVBoxLayout(imgDialog);
auto *imgLabel = new QLabel(imgDialog);
imgLabel->setPixmap(pixmap.scaled(600, 1000, Qt::KeepAspectRatio, Qt::SmoothTransformation));
dlgLayout->addWidget(imgLabel);
imgDialog->resize(imgLabel->pixmap().size());
imgDialog->show();
});
}
}
layout->addWidget(tableWidget);
tableWidget->resizeColumnsToContents();
tableWidget->resizeRowsToContents();
// Подгоняем ширину окна под содержимое таблицы
int totalWidth = tableWidget->verticalHeader()->width() + tableWidget->frameWidth() * 2 + 20;
for (int i = 0; i < tableWidget->columnCount(); ++i) {
totalWidth += tableWidget->columnWidth(i);
}
resize(qMax(800, totalWidth), 600);
// for (int row = 0; row < tableWidget->rowCount(); ++row) {
// for (int col = 0; col < tableWidget->columnCount(); ++col) {
// QTableWidgetItem *item = tableWidget->item(row, col);