1
0
forked from BRT/arc
arc/android/ozon/PayByPhoneScript.cpp

658 lines
32 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "PayByPhoneScript.h"
#include <QProcess>
#include <QRegularExpression>
#include <QThread>
#include <QTimeZone>
#include <utility>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dump/TaskDumpRecorder.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "time/DateUtils.h"
#include <QCoreApplication>
static QString recognizeTextFromImage(const QByteArray &pngData) {
auto *api = new tesseract::TessBaseAPI();
if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) {
qWarning() << "[OCR] Failed to init Tesseract";
delete api;
return {};
}
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
static_cast<size_t>(pngData.size()));
if (!pix) {
qWarning() << "[OCR] Failed to read image";
api->End();
delete api;
return {};
}
api->SetImage(pix);
char *text = api->GetUTF8Text();
QString result = QString::fromUtf8(text).trimmed();
delete[] text;
pixDestroy(&pix);
api->End();
delete api;
return result;
}
static QString extractTransactionId(const QString &ocrText) {
// "ID операции B606513042211200000120011700501"
// "ID операции po-3140eb01-002c-5000-8000-033396b637e1"
// или разбит на 2 строки
QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-zА-Яа-яЁё0-9\-]+(?:[^\S\n]+[A-Za-zА-Яа-яЁё0-9\-]+)?))");
if (auto m = reId.match(ocrText); m.hasMatch()) {
QString id = m.captured(1);
id.remove(' ');
id.remove(QChar(0x00A0));
return id;
}
// "№ Ф-2026-25235663"
QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))");
if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1);
return {};
}
namespace Ozon {
static QString sanitizeBankName(const QString &raw) {
// "Озон банк (Ozon)" -> "Озон банк"; убираем содержимое круглых скобок
// вместе с предшествующими пробелами и обрезаем края.
QString s = raw;
s.remove(QRegularExpression(R"(\s*\([^)]*\))"));
return s.trimmed();
}
PayByPhoneScript::PayByPhoneScript(
MaterialInfo account,
QString phone,
QString bankName,
const double amount,
QObject *parent
) : CommonScript(parent),
m_account(std::move(account)), m_phone(std::move(phone)),
m_bankName(sanitizeBankName(bankName)), m_amount(amount) {
}
PayByPhoneScript::~PayByPhoneScript() = default;
void PayByPhoneScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_account.deviceId;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
const QString &adbId = device.adbSerial;
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}, app.bankProfileId, app.phone, device.id, app.appVersion};
TaskDumpRecorder::Scope dumpScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
// Закрываем рекламный bottom sheet
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[PayByPhone] Ad banner detected, closing...";
AdbUtils::makeTap(adbId, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[PayByPhone] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(adbId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.name + " (" + device.id + ")";
qWarning() << m_error;
AdbUtils::tryToKillApplication(adbId, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
// Закрываем баннеры
tryToCloseAllBannersOnHomePage(adbId, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
// Закрываем баннер Google Play "Доступно обновление"
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(device.screenWidth, device.screenHeight); !gpNode.isEmpty()) {
qDebug() << "[PayByPhone] Google Play update banner detected, closing...";
AdbUtils::makeTap(adbId, gpNode.x(), gpNode.y());
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
// Закрываем баннер "Заказать карту" — тапаем сверху экрана
if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) {
qDebug() << "[PayByPhone] 'Заказать карту' banner detected, closing...";
AdbUtils::makeTap(adbId, device.screenWidth / 2, device.screenHeight / 20);
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
makePayment(device.adbSerial, app.package, app.pinCode, app.phone, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}
void PayByPhoneScript::makePayment(
const QString &deviceId,
const QString &packageName,
const QString &pinCode,
const QString &bankProfilePhone,
const int width,
const int height
) {
// Макрос-проверка: приложение живо?
auto checkApp = [&]() -> bool {
if (!isAppRunning(deviceId, packageName)) {
m_error = "Приложение было закрыто";
qWarning() << "[PayByPhone]" << m_error;
return false;
}
return true;
};
// 1. Нажимаем "Перевести" на домашнем экране.
// Внимание: поверх правой части кнопки сидит referral-бейдж "badge icon за друга"
// (bounds типа [550,90][602,140]), который перекрывает центр кнопки. Тап в центр
// открывает промо "До 3 000 за рекомендацию". Тапаем в левую четверть, чтобы
// гарантированно попасть мимо бейджа.
Node transferBtn = xmlScreenParser.findButtonNode("Перевести", false);
if (transferBtn.isEmpty()) {
m_error = "Кнопка 'Перевести' не найдена на домашнем экране";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// Тапаем в нижнюю часть кнопки (там, где визуально надпись "Перевести"),
// ниже бейджа "за друга" (он заканчивается на y≈140).
const int transferTapX = transferBtn.x();
const int transferTapY = transferBtn.y1() + transferBtn.height() * 4 / 5;
qDebug() << "[PayByPhone] Tapping 'Перевести' at" << transferTapX << "," << transferTapY
<< "(below 'за друга' badge)";
AdbUtils::makeTap(deviceId, transferTapX, transferTapY);
QThread::msleep(2000);
// 2. Ждём экран "Платежи и переводы" с полем "Телефон получателя"
bool paymentsPageLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
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) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
// Закрываем диалог "Разрешить доступ к контактам"
Node notNowBtn = xmlScreenParser.findButtonNode("Не сейчас", false);
if (!notNowBtn.isEmpty()) {
qDebug() << "[PayByPhone] Contacts permission dialog detected, tapping 'Не сейчас'...";
AdbUtils::makeTap(deviceId, notNowBtn.x(), notNowBtn.y());
QThread::msleep(1500);
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) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
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;
FreezeDetector freezeCheck;
for (int attempt = 0; attempt < 20; ++attempt) {
if (!checkApp()) return;
const QString currentDumpXml = AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter);
xmlScreenParser.parseAndSaveXml(currentDumpXml);
closeGooglePlayBannerIfVisible(deviceId, width, height);
if (freezeCheck.noticeAndCheckFrozen(currentDumpXml)) {
qWarning() << "[PayByPhone] 4 identical dumps in a row — screen frozen, aborting";
break;
}
// Проверяем: уже на экране суммы?
Node sumTitle = xmlScreenParser.findTextNode("По номеру телефона");
if (!sumTitle.isEmpty() && sumTitle.y() < height / 7) {
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 пустой).
// На устройствах с маленьким экраном кнопка лежит в конце виртуализированного
// списка и приходит с bounds=[0,0][0,0] — Node::isEmpty это не ловит, тап
// улетает в (0,0). Скроллим список вниз, пока bounds не станут ненулевыми
// (после первого скролла кнопка обычно материализуется у нижнего края, но
// тап по ней работает — это её настоящая позиция в layout).
auto findOtherBank = [&]() -> Node {
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (n.contentDesc.contains("other-bank-item")) return n;
}
return {};
};
bool foundOtherBank = false;
Node node = findOtherBank();
for (int scrollAttempt = 0;
!node.isEmpty() && (node.width() == 0 || node.height() == 0) && scrollAttempt < 6;
++scrollAttempt) {
qDebug() << "[PayByPhone] 'Другой банк' has collapsed bounds, scrolling list down, attempt"
<< scrollAttempt + 1 << "/ 6";
AdbUtils::makeSwipe(deviceId, width / 2, height * 3 / 4, width / 2, height / 4);
QThread::msleep(800);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
node = findOtherBank();
}
if (!node.isEmpty() && node.width() > 0 && node.height() > 0) {
qDebug() << "[PayByPhone] Tapping 'Другой банк' (other-bank-item) at"
<< node.x() << "," << node.y();
AdbUtils::makeTap(deviceId, node.x(), node.y());
QThread::msleep(2000);
foundOtherBank = true;
otherBankClicked = true;
}
if (foundOtherBank) continue;
// Вариант 2: уже на экране поиска банка (EditText) — только после клика на "Другой банк"
if (otherBankClicked) {
// После перехода на экран поиска банка поле ввода прячется под верхним
// тулбаром (рендерится Svelte WebView'ом). Свайпаем сверху вниз, чтобы
// подтянуть содержимое и сделать поле физически видимым/активным.
qDebug() << "[PayByPhone] Scrolling up to reveal hidden bank search input...";
AdbUtils::makeSwipe(deviceId, width / 2, height / 4, width / 2, height * 3 / 4);
QThread::msleep(800);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
Node bankInput = xmlScreenParser.findFirstEditText();
if (!bankInput.isEmpty()) {
qDebug() << "[PayByPhone] Bank search screen detected, entering bank name...";
// Переключаем IME ДО тапа по полю — иначе смена IME после тапа
// закроет системную клавиатуру и WebView резайзится, из-за чего
// сохранённые координаты bankInput уезжают и повторный тап мимо.
if (!AdbUtils::enableAdbIMEKeyboard(deviceId)) {
m_error = "Не удалось активировать ADB Keyboard (см. логи 'ime list -s' / default_input_method)";
qWarning() << "[PayByPhone]" << m_error;
return;
}
QThread::msleep(800);
// Перечитываем XML — layout с ADBKeyboard (без UI) отличается от layout
// с системной клавиатурой. Берём актуальные bounds поля.
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node freshBankInput = xmlScreenParser.findFirstEditText();
if (!freshBankInput.isEmpty()) bankInput = freshBankInput;
AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y());
QThread::msleep(500);
AdbUtils::inputAdbIMEText(deviceId, m_bankName);
QThread::msleep(2500);
// Верифицируем, что текст действительно попал в поле
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node verifyInput = xmlScreenParser.findFirstEditText();
if (verifyInput.text.isEmpty()) {
qWarning() << "[PayByPhone] Поле банка пустое после inputAdbIMEText — ретраим";
AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y());
QThread::msleep(500);
AdbUtils::inputAdbIMEText(deviceId, m_bankName);
QThread::msleep(2500);
}
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));
// Сначала проверяем, что поле поиска реально содержит введённый текст —
// иначе мы тапаем по первому попавшемуся банку из несфильтрованного списка.
Node searchField = xmlScreenParser.findFirstEditText();
if (searchField.text.trimmed().isEmpty()) {
qDebug() << "[PayByPhone] Search field still empty, waiting for filter, attempt" << ba + 1 << "/ 10";
QThread::msleep(1500);
continue;
}
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") {
const int tapX = node.x1() + node.width() / 4;
qDebug() << "[PayByPhone] Found bank in list, tapping:" << node.contentDesc << "tapX:" << tapX << "y:" << node.y();
AdbUtils::makeTap(deviceId, tapX, 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. Вводим сумму
// На экране "По номеру телефона" обычно 2 EditText: сумма (сверху) и
// сообщение получателю (снизу). Оба имеют rid="input___xxxx" в Svelte UI.
// Берём верхний (минимальный y1) — это amount.
Node sumInput;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
QList<Node> candidateInputs;
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (n.className != "android.widget.EditText") continue;
if (n.height() <= 0 || n.height() > height * 4 / 5) continue;
if (!n.resourceId.startsWith("input___")) continue;
candidateInputs.append(n);
}
if (!candidateInputs.isEmpty()) {
std::sort(candidateInputs.begin(), candidateInputs.end(),
[](const Node &a, const Node &b) { return a.y1() < b.y1(); });
sumInput = candidateInputs.first(); // верхний = amount
break;
}
// Старая логика — если нет ни одного "input___": берём любой EditText.
sumInput = xmlScreenParser.findFirstEditText();
if (!sumInput.isEmpty()) break;
qDebug() << "[PayByPhone] Waiting for sum input field, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
}
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);
// Закрываем системную софт-клавиатуру, иначе она перекрывает "Продолжить"
// (кнопка уезжает за нижнюю границу web_view_holder и имеет нулевую высоту).
AdbUtils::hideKeyboard(deviceId);
QThread::msleep(800);
// 6. Нажимаем "Продолжить" (ретраим, пока bounds перестанут быть нулевыми)
Node continueBtn;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
continueBtn = xmlScreenParser.findButtonNode("Продолжить", false);
if (!continueBtn.isEmpty() && continueBtn.height() > 0) break;
qDebug() << "[PayByPhone] Waiting for tappable 'Продолжить', attempt" << attempt + 1 << "/ 5"
<< "(bounds collapsed — клавиатура ещё видна?)";
AdbUtils::hideKeyboard(deviceId);
QThread::msleep(1000);
}
if (continueBtn.isEmpty() || continueBtn.height() == 0) {
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) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
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;
}
// 8. Нажимаем "Перевести ..." (ищем по первому слову)
Node transferConfirmBtn;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
transferConfirmBtn = xmlScreenParser.findButtonNode("Перевести", true);
if (!transferConfirmBtn.isEmpty()) break;
qDebug() << "[PayByPhone] Waiting for 'Перевести ...' button, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
}
if (transferConfirmBtn.isEmpty()) {
m_error = "Кнопка 'Перевести ...' не найдена на экране подтверждения";
qWarning() << "[PayByPhone]" << m_error;
return;
}
qDebug() << "[PayByPhone] Tapping confirm button:" << transferConfirmBtn.text;
AdbUtils::makeTap(deviceId, transferConfirmBtn.x(), transferConfirmBtn.y());
QThread::msleep(3000);
// 8.5. Проверяем экран ввода PIN-кода
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
!pinNodes.isEmpty()) {
qDebug() << "[PayByPhone] PIN code screen detected after confirm, entering PIN...";
inputPinCode(deviceId, pinNodes);
QThread::msleep(3000);
}
// 9. Ждём результат: "Успешно" или "Не удалось"
for (int attempt = 0; attempt < 15; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
// Если PIN-код появился позже — вводим
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
!pinNodes.isEmpty()) {
qDebug() << "[PayByPhone] PIN code screen detected (retry), entering PIN...";
inputPinCode(deviceId, pinNodes);
QThread::msleep(3000);
continue;
}
if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) {
qDebug() << "[PayByPhone] Payment successful!";
m_error.clear();
// Сохраняем транзакцию в БД
TransactionInfo tx;
tx.accountId = m_account.id;
tx.amount = -m_amount;
tx.phone = m_phone;
tx.bankName = "ozon";
tx.type = TransactionType::Phone;
tx.status = TransactionStatus::Complete;
tx.timestamp = DateUtils::getUtcNow();
tx.completeTime = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow"));
tx.bankTime = tx.completeTime.toString("dd.MM.yyyy, HH:mm");
const int txId = TransactionDAO::insertTransaction(tx);
qDebug() << "[PayByPhone] Saved transaction to DB, id:" << txId;
// Скриншот экрана "Успешно" — отдаём как receipt-image и в EventHandler.
// Раньше дополнительно тапали "Документы" + ждали "Поделиться" + OCR
// ради bank_transaction_id, но это медленно (до 30 сек ожидания) и
// нестабильно. Теперь полагаемся на fallback-ID из phone+timestamp.
QString externalId;
if (txId != -1) {
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
if (!screenshot.isEmpty()) {
m_resultScreenshot = screenshot;
emit screenshotReady(m_resultScreenshot);
TransactionDAO::updateReceiptImage(txId, screenshot);
qDebug() << "[PayByPhone] Receipt saved to DB, txId:" << txId;
}
}
if (externalId.isEmpty() && txId != -1) {
externalId = "ozon_" + bankProfilePhone + "_" + QString::number(QDateTime::currentSecsSinceEpoch());
qDebug() << "[PayByPhone] Generated fallback transaction ID:" << externalId;
}
if (!externalId.isEmpty() && txId != -1) {
TransactionDAO::updateBankTrExternalId(txId, externalId);
qDebug() << "[PayByPhone] Transaction ID:" << externalId;
}
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
return;
}
if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) {
m_error = "Перевод не удался (экран 'Не удалось')";
qWarning() << "[PayByPhone]" << m_error;
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
return;
}
qDebug() << "[PayByPhone] Waiting for payment result, attempt" << attempt + 1 << "/ 15";
QThread::msleep(2000);
}
m_error = "Не дождались результата перевода";
qWarning() << "[PayByPhone]" << m_error;
}
} // namespace Ozon