632 lines
29 KiB
C++
632 lines
29 KiB
C++
#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 {
|
||
|
||
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(std::move(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, {}};
|
||
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. Нажимаем "Перевести" на домашнем экране
|
||
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) {
|
||
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) {
|
||
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
|