#include "BankSetupWizard.h" #include #include #include #include #include "ozon/LoginAndCheckAccountsScript.h" #include "ozon/GetProfileInfoScript.h" #include "ozon/GetCardInfoScript.h" #include "black/LoginAndCheckAccountsScript.h" #include "black/GetProfileInfoScript.h" #include "black/GetCardInfoScript.h" #include "dushanbe/LoginAndCheckAccountsScript.h" #include "dushanbe/GetProfileInfoScript.h" #include "dushanbe/GetCardInfoScript.h" #include "dao/BankProfileDAO.h" #include "dao/DeviceDAO.h" #include "dao/MaterialDAO.h" #include "net/NetworkService.h" BankSetupWizard::BankSetupWizard(QWidget *parent, const QString &deviceId, const BankProfileInfo &app) : QDialog(parent), m_deviceId(deviceId), m_app(app) { setWindowTitle("Настройка банка — " + app.name); setMinimumSize(500, 350); resize(520, 380); auto *mainLayout = new QVBoxLayout(this); m_stack = new QStackedWidget(this); m_stack->addWidget(createStep1()); // 0 m_stack->addWidget(createStep2()); // 1 m_stack->addWidget(createStep3()); // 2 m_stack->addWidget(createStep4Success()); // 3 m_stack->addWidget(createStep4Error()); // 4 m_stack->setCurrentIndex(0); mainLayout->addWidget(m_stack); pauseActiveProfiles(); } BankSetupWizard::~BankSetupWizard() { stopWorkerThread(); resumePausedProfiles(); } // ── Step 1: Intro ────────────────────────────────────────────────────────── QWidget *BankSetupWizard::createStep1() { auto *page = new QWidget; auto *layout = new QVBoxLayout(page); layout->setAlignment(Qt::AlignTop); auto *icon = new QLabel; QPixmap pixmap(QCoreApplication::applicationDirPath() + "/images/" + m_app.code + "_icon.png"); if (!pixmap.isNull()) icon->setPixmap(pixmap.scaled(48, 48, Qt::KeepAspectRatio, Qt::SmoothTransformation)); icon->setAlignment(Qt::AlignCenter); layout->addWidget(icon); auto *title = new QLabel("

" + m_app.name + "

"); title->setAlignment(Qt::AlignCenter); layout->addWidget(title); auto *desc = new QLabel( "Мастер поможет настроить банковский профиль для приложения " + m_app.name + ".

" "Будут выполнены следующие шаги:
" "1. Ввод пин-кода
" "2. Проверка входа в приложение
" "3. Получение данных профиля и карт" ); desc->setWordWrap(true); desc->setAlignment(Qt::AlignCenter); layout->addWidget(desc); layout->addStretch(); auto *btnLayout = new QHBoxLayout; btnLayout->addStretch(); auto *nextBtn = new QPushButton("Далее"); nextBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;"); connect(nextBtn, &QPushButton::clicked, this, [this]() { m_stack->setCurrentIndex(1); }); btnLayout->addWidget(nextBtn); layout->addLayout(btnLayout); return page; } // ── Step 2: PIN Code ─────────────────────────────────────────────────────── QWidget *BankSetupWizard::createStep2() { auto *page = new QWidget; auto *layout = new QVBoxLayout(page); layout->setAlignment(Qt::AlignTop); auto *title = new QLabel("

Введите пин-код

"); title->setAlignment(Qt::AlignCenter); layout->addWidget(title); auto *desc = new QLabel("Укажите пин-код для входа в приложение " + m_app.name + "."); desc->setWordWrap(true); desc->setAlignment(Qt::AlignCenter); layout->addWidget(desc); layout->addSpacing(16); m_pinCodeEdit = new QLineEdit; m_pinCodeEdit->setPlaceholderText("Пин-код"); m_pinCodeEdit->setMaxLength(10); m_pinCodeEdit->setAlignment(Qt::AlignCenter); m_pinCodeEdit->setFixedWidth(200); auto *editLayout = new QHBoxLayout; editLayout->addStretch(); editLayout->addWidget(m_pinCodeEdit); editLayout->addStretch(); layout->addLayout(editLayout); layout->addStretch(); auto *btnLayout = new QHBoxLayout; auto *backBtn = new QPushButton("Назад"); connect(backBtn, &QPushButton::clicked, this, [this]() { m_stack->setCurrentIndex(0); }); btnLayout->addWidget(backBtn); btnLayout->addStretch(); auto *nextBtn = new QPushButton("Далее"); nextBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;"); connect(nextBtn, &QPushButton::clicked, this, [this]() { if (m_pinCodeEdit->text().trimmed().isEmpty()) return; m_app.pinCode = m_pinCodeEdit->text().trimmed(); m_stack->setCurrentIndex(2); startVerification(); }); btnLayout->addWidget(nextBtn); layout->addLayout(btnLayout); return page; } // ── Step 3: Progress ─────────────────────────────────────────────────────── QWidget *BankSetupWizard::createStep3() { auto *page = new QWidget; auto *layout = new QVBoxLayout(page); layout->setAlignment(Qt::AlignTop); auto *title = new QLabel("

Идёт проверка банка

"); title->setAlignment(Qt::AlignCenter); layout->addWidget(title); auto *desc = new QLabel( "Пожалуйста, не закрывайте приложение и не трогайте телефон.\n" "Проверка может занять некоторое время." ); desc->setWordWrap(true); desc->setAlignment(Qt::AlignCenter); desc->setStyleSheet("color: gray; margin-bottom: 12px;"); layout->addWidget(desc); m_progressBar = new QProgressBar; m_progressBar->setRange(0, 100); m_progressBar->setValue(0); m_progressBar->setTextVisible(true); layout->addWidget(m_progressBar); m_progressLabel = new QLabel("Подготовка..."); m_progressLabel->setAlignment(Qt::AlignCenter); m_progressLabel->setStyleSheet("margin-top: 8px;"); layout->addWidget(m_progressLabel); layout->addStretch(); auto *btnLayout = new QHBoxLayout; m_backBtn3 = new QPushButton("Назад"); connect(m_backBtn3, &QPushButton::clicked, this, [this]() { stopWorkerThread(); m_progressBar->setValue(0); m_progressLabel->setText("Подготовка..."); m_stack->setCurrentIndex(1); }); btnLayout->addWidget(m_backBtn3); btnLayout->addStretch(); layout->addLayout(btnLayout); return page; } // ── Step 4a: Success ─────────────────────────────────────────────────────── QWidget *BankSetupWizard::createStep4Success() { m_successPage = new QWidget; auto *layout = new QVBoxLayout(m_successPage); layout->setAlignment(Qt::AlignTop); auto *title = new QLabel("

Проверка прошла успешно

"); title->setAlignment(Qt::AlignCenter); layout->addWidget(title); m_resultLabel = new QLabel; m_resultLabel->setWordWrap(true); m_resultLabel->setTextFormat(Qt::RichText); layout->addWidget(m_resultLabel); auto *cardsTitle = new QLabel("Найденные карты:"); layout->addWidget(cardsTitle); m_cardsTable = new QTableWidget; m_cardsTable->setSelectionMode(QAbstractItemView::NoSelection); m_cardsTable->setEditTriggers(QAbstractItemView::NoEditTriggers); m_cardsTable->setFocusPolicy(Qt::NoFocus); m_cardsTable->setColumnCount(3); m_cardsTable->setHorizontalHeaderLabels({"Номер", "Сумма", "Валюта"}); m_cardsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); layout->addWidget(m_cardsTable); layout->addStretch(); auto *btnLayout = new QHBoxLayout; btnLayout->addStretch(); auto *finishBtn = new QPushButton("Завершить"); finishBtn->setStyleSheet("background-color: green; color: white; padding: 6px 24px;"); connect(finishBtn, &QPushButton::clicked, this, &QDialog::accept); btnLayout->addWidget(finishBtn); layout->addLayout(btnLayout); return m_successPage; } // ── Step 4b: Error ───────────────────────────────────────────────────────── QWidget *BankSetupWizard::createStep4Error() { m_errorPage = new QWidget; auto *layout = new QVBoxLayout(m_errorPage); layout->setAlignment(Qt::AlignTop); auto *title = new QLabel("

Ошибка

"); title->setAlignment(Qt::AlignCenter); layout->addWidget(title); m_errorText = new QLabel; m_errorText->setWordWrap(true); m_errorText->setStyleSheet("color: red; margin: 12px 0;"); m_errorText->setAlignment(Qt::AlignCenter); layout->addWidget(m_errorText); layout->addStretch(); auto *btnLayout = new QHBoxLayout; auto *backBtn = new QPushButton("Назад"); connect(backBtn, &QPushButton::clicked, this, [this]() { m_stack->setCurrentIndex(1); }); btnLayout->addWidget(backBtn); btnLayout->addStretch(); auto *restartBtn = new QPushButton("Начать заново"); restartBtn->setStyleSheet("background-color: orange; color: white; padding: 6px 24px;"); connect(restartBtn, &QPushButton::clicked, this, [this]() { // Очищаем данные из БД если ещё не отправлены на сервер const BankProfileInfo current = BankProfileDAO::getApplication(m_deviceId, m_app.code); if (current.id >= 0 && current.bankProfileId.isEmpty()) { MaterialDAO::deleteAccountsByApp(m_deviceId, m_app.code); BankProfileDAO::deleteProfile(m_deviceId, m_app.code); m_app.id = -1; m_app.pinCodeChecked = false; m_app.pinCode.clear(); } m_pinCodeEdit->clear(); m_progressBar->setValue(0); m_progressLabel->setText("Подготовка..."); m_stack->setCurrentIndex(0); }); btnLayout->addWidget(restartBtn); layout->addLayout(btnLayout); return m_errorPage; } // ── Pause/Resume active profiles ─────────────────────────────────────────── static void toggleProfileOnServer(const BankProfileInfo &app, bool enable) { if (app.bankProfileId.isEmpty()) return; auto *thread = new QThread; auto *net = new NetworkService; net->moveToThread(thread); net->addExtra("bank_profile_id", app.bankProfileId); net->addExtra("enable", enable); QObject::connect(thread, &QThread::started, net, &NetworkService::toggleBankProfile); QObject::connect(net, &NetworkService::finished, thread, &QThread::quit); QObject::connect(net, &NetworkService::finished, net, &QObject::deleteLater); QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); } void BankSetupWizard::pauseActiveProfiles() { const QList apps = BankProfileDAO::getApplicationsByDeviceId(m_deviceId); for (const BankProfileInfo &app : apps) { if (app.status == "active") { m_pausedProfiles.append(app.code); BankProfileDAO::update(app.id, app.pinCode, app.comment, "off"); toggleProfileOnServer(app, false); qDebug() << "[BankSetupWizard] Paused profile:" << app.code; } } } void BankSetupWizard::resumePausedProfiles() { for (const QString &code : m_pausedProfiles) { const BankProfileInfo app = BankProfileDAO::getApplication(m_deviceId, code); if (app.id >= 0 && app.status == "off") { BankProfileDAO::update(app.id, app.pinCode, app.comment, "active"); toggleProfileOnServer(app, true); qDebug() << "[BankSetupWizard] Resumed profile:" << app.code; } } m_pausedProfiles.clear(); } // ── Verification flow ────────────────────────────────────────────────────── void BankSetupWizard::setProgress(int value, const QString &text) { m_progressBar->setValue(value); m_progressLabel->setText(text); } void BankSetupWizard::stopWorkerThread() { if (!m_workerThread) return; QThread *t = m_workerThread; m_workerThread = nullptr; t->requestInterruption(); t->quit(); if (t->wait(3000)) { t->deleteLater(); return; } // Воркер залип (скорее всего в AdbUtils::startProcess / QProcess::waitForFinished). // Отсоединяем QThread от виджета, чтобы ~QWidget не удалил работающий поток // (это триггерит qFatal в ~QThread), и откладываем удаление до естественного завершения. qWarning() << "[BankSetupWizard] worker thread still running after 3s, detaching"; t->setParent(nullptr); connect(t, &QThread::finished, t, &QObject::deleteLater); } void BankSetupWizard::startVerification() { // Проверяем что устройство онлайн const DeviceInfo device = DeviceDAO::getDeviceById(m_deviceId); if (device.status != "device") { onVerificationError("Устройство не подключено.\nУбедитесь, что телефон подключён и попробуйте снова."); return; } // Save pin code to DB first via upsert m_app.status = "off"; m_app.install = true; BankProfileDAO::upsertApplication(m_app); // Re-read to get the id m_app = BankProfileDAO::getApplication(m_deviceId, m_app.code); setProgress(5, "Запуск проверки пин-кода..."); m_backBtn3->setEnabled(true); runPinCheck(); } void BankSetupWizard::runPinCheck() { stopWorkerThread(); m_workerThread = new QThread(this); const QString deviceId = m_deviceId; const QString appCode = m_app.code; const int appId = m_app.id; if (appCode == "ozon") { auto *worker = new Ozon::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, false, true); worker->moveToThread(m_workerThread); connect(m_workerThread, &QThread::started, worker, &Ozon::LoginAndCheckAccountsScript::start); connect(worker, &Ozon::LoginAndCheckAccountsScript::finishedWithResult, this, [this, appId, appCode](const QString &result) { if (!result.isEmpty()) { onVerificationError("Ошибка проверки пин-кода: " + result); return; } BankProfileDAO::updatePinCodeStatus(appId, "yes", ""); m_app.pinCodeChecked = true; setProgress(33, "Пин-код проверен. Получение профиля..."); runProfileCheck(); }, Qt::QueuedConnection); connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(worker, &Ozon::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit); } else if (appCode == "black") { auto *worker = new Black::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true); worker->moveToThread(m_workerThread); connect(m_workerThread, &QThread::started, worker, &Black::LoginAndCheckAccountsScript::start); connect(worker, &Black::LoginAndCheckAccountsScript::finishedWithResult, this, [this, appId](const QString &result) { if (!result.isEmpty()) { onVerificationError("Ошибка проверки пин-кода: " + result); return; } BankProfileDAO::updatePinCodeStatus(appId, "yes", ""); m_app.pinCodeChecked = true; setProgress(33, "Пин-код проверен. Получение профиля..."); runProfileCheck(); }, Qt::QueuedConnection); connect(worker, &Black::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(worker, &Black::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit); } else if (appCode == "dushanbe_city_bank") { auto *worker = new Dushanbe::LoginAndCheckAccountsScript(deviceId, appCode, nullptr, true); worker->moveToThread(m_workerThread); connect(m_workerThread, &QThread::started, worker, &Dushanbe::LoginAndCheckAccountsScript::start); connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finishedWithResult, this, [this, appId](const QString &result) { if (!result.isEmpty()) { onVerificationError("Ошибка проверки пин-кода: " + result); return; } BankProfileDAO::updatePinCodeStatus(appId, "yes", ""); m_app.pinCodeChecked = true; setProgress(33, "Пин-код проверен. Получение профиля..."); runProfileCheck(); }, Qt::QueuedConnection); connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, worker, &QObject::deleteLater); connect(worker, &Dushanbe::LoginAndCheckAccountsScript::finished, m_workerThread, &QThread::quit); } else { onVerificationError("Банк " + appCode + " не поддерживается для автоматической настройки."); return; } m_workerThread->start(); } void BankSetupWizard::runProfileCheck() { stopWorkerThread(); m_workerThread = new QThread(this); const QString deviceId = m_deviceId; const QString appCode = m_app.code; auto onProfileResult = [this, deviceId, appCode](const QString &result) { if (!result.isEmpty()) { onVerificationError("Ошибка получения профиля: " + result); return; } // Проверяем что bank_profile_id получен от сервера const BankProfileInfo updated = BankProfileDAO::getApplication(deviceId, appCode); if (updated.bankProfileId.isEmpty()) { onVerificationError("Не удалось зарегистрировать банковский профиль на сервере.\n" "Возможно, банк ещё не поддерживается сервером."); return; } setProgress(66, "Профиль получен. Проверка карт..."); runCardCheck(); }; if (appCode == "ozon") { auto *worker = new Ozon::GetProfileInfoScript(deviceId, appCode, nullptr); worker->moveToThread(m_workerThread); connect(m_workerThread, &QThread::started, worker, &Ozon::GetProfileInfoScript::start); connect(worker, &Ozon::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection); connect(worker, &Ozon::GetProfileInfoScript::finished, worker, &QObject::deleteLater); connect(worker, &Ozon::GetProfileInfoScript::finished, m_workerThread, &QThread::quit); } else if (appCode == "black") { auto *worker = new Black::GetProfileInfoScript(deviceId, appCode, nullptr); worker->moveToThread(m_workerThread); connect(m_workerThread, &QThread::started, worker, &Black::GetProfileInfoScript::start); connect(worker, &Black::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection); connect(worker, &Black::GetProfileInfoScript::finished, worker, &QObject::deleteLater); connect(worker, &Black::GetProfileInfoScript::finished, m_workerThread, &QThread::quit); } else if (appCode == "dushanbe_city_bank") { auto *worker = new Dushanbe::GetProfileInfoScript(deviceId, appCode, nullptr); worker->moveToThread(m_workerThread); connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetProfileInfoScript::start); connect(worker, &Dushanbe::GetProfileInfoScript::finishedWithResult, this, onProfileResult, Qt::QueuedConnection); connect(worker, &Dushanbe::GetProfileInfoScript::finished, worker, &QObject::deleteLater); connect(worker, &Dushanbe::GetProfileInfoScript::finished, m_workerThread, &QThread::quit); } else { onVerificationError("Банк " + appCode + " не поддерживается."); return; } m_workerThread->start(); } void BankSetupWizard::runCardCheck() { stopWorkerThread(); m_workerThread = new QThread(this); const QString deviceId = m_deviceId; const QString appCode = m_app.code; auto onCardResult = [this, deviceId, appCode](const QString &result) { if (!result.isEmpty()) { onVerificationError("Ошибка получения карт: " + result); return; } // Проверяем что хотя бы одна карта синхронизирована с сервером const QList materials = MaterialDAO::getAccounts(deviceId, appCode); bool hasSynced = false; for (const auto &m : materials) { if (!m.materialId.isEmpty()) { hasSynced = true; break; } } if (!hasSynced && !materials.isEmpty()) { onVerificationError("Не удалось отправить данные карт на сервер.\n" "Проверьте подключение к серверу."); return; } setProgress(100, "Готово!"); onVerificationSuccess(); }; if (appCode == "ozon") { auto *worker = new Ozon::GetCardInfoScript(deviceId, appCode, "", nullptr); worker->moveToThread(m_workerThread); connect(m_workerThread, &QThread::started, worker, &Ozon::GetCardInfoScript::start); connect(worker, &Ozon::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection); connect(worker, &Ozon::GetCardInfoScript::finished, worker, &QObject::deleteLater); connect(worker, &Ozon::GetCardInfoScript::finished, m_workerThread, &QThread::quit); } else if (appCode == "black") { auto *worker = new Black::GetCardInfoScript(deviceId, appCode, nullptr); worker->moveToThread(m_workerThread); connect(m_workerThread, &QThread::started, worker, &Black::GetCardInfoScript::start); connect(worker, &Black::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection); connect(worker, &Black::GetCardInfoScript::finished, worker, &QObject::deleteLater); connect(worker, &Black::GetCardInfoScript::finished, m_workerThread, &QThread::quit); } else if (appCode == "dushanbe_city_bank") { auto *worker = new Dushanbe::GetCardInfoScript(deviceId, appCode, nullptr); worker->moveToThread(m_workerThread); connect(m_workerThread, &QThread::started, worker, &Dushanbe::GetCardInfoScript::start); connect(worker, &Dushanbe::GetCardInfoScript::finishedWithResult, this, onCardResult, Qt::QueuedConnection); connect(worker, &Dushanbe::GetCardInfoScript::finished, worker, &QObject::deleteLater); connect(worker, &Dushanbe::GetCardInfoScript::finished, m_workerThread, &QThread::quit); } else { onVerificationError("Банк " + appCode + " не поддерживается."); return; } m_workerThread->start(); } void BankSetupWizard::onVerificationSuccess() { // Re-read profile from DB m_app = BankProfileDAO::getApplication(m_deviceId, m_app.code); QString profileHtml; if (!m_app.fullName.isEmpty()) profileHtml += "ФИО: " + m_app.fullName + "
"; if (!m_app.phone.isEmpty()) profileHtml += "Телефон: " + m_app.phone + "
"; if (!m_app.email.isEmpty()) profileHtml += "Email: " + m_app.email + "
"; if (profileHtml.isEmpty()) profileHtml = "Данные профиля не получены"; m_resultLabel->setText(profileHtml); // Load cards const QList cards = MaterialDAO::getAccounts(m_deviceId, m_app.code); m_cardsTable->setRowCount(cards.size()); for (int i = 0; i < cards.size(); ++i) { const MaterialInfo &c = cards[i]; const QString displayNumber = c.cardNumber.isEmpty() ? c.lastNumbers : c.cardNumber; m_cardsTable->setItem(i, 0, new QTableWidgetItem(displayNumber)); m_cardsTable->setItem(i, 1, new QTableWidgetItem(QString::number(c.amount))); m_cardsTable->setItem(i, 2, new QTableWidgetItem(c.currency)); } m_cardsTable->resizeColumnsToContents(); if (cards.isEmpty()) { m_cardsTable->setRowCount(1); auto *item = new QTableWidgetItem("Карты не найдены"); item->setForeground(QBrush(Qt::gray)); m_cardsTable->setItem(0, 0, item); m_cardsTable->setSpan(0, 0, 1, 3); } m_stack->setCurrentIndex(3); } static QString humanReadableError(const QString &error) { // Определяем этап QString stage; if (error.contains("пин-кода")) stage = "Проверка пин-кода: "; else if (error.contains("профиля")) stage = "Получение профиля: "; else if (error.contains("карт")) stage = "Получение карт: "; if (error.contains("Device or app not found")) return stage + "Устройство или приложение не найдено.\nУбедитесь, что телефон подключён и приложение установлено."; if (error.contains("Cant open the home screen") || error.contains("Cannot reach home screen")) return stage + "Не удалось открыть главный экран приложения.\nПопробуйте перезапустить приложение на телефоне и повторить."; if (error.contains("Cannot find user name") || error.contains("Cannot find Hola button")) return stage + "Не удалось найти данные профиля на главном экране.\nВозможно, приложение обновилось или экран выглядит иначе."; if (error.contains("Profile screen not detected") || error.contains("Profile page not detected")) return stage + "Не удалось открыть страницу профиля.\nПопробуйте повторить позже."; if (error.contains("Side menu not detected")) return stage + "Не удалось открыть боковое меню приложения.\nПопробуйте повторить."; if (error.contains("Cannot find Tu Informaci") || error.contains("Cannot find Tu CVU")) return stage + "Не удалось найти нужный раздел в меню приложения.\nВозможно, приложение обновилось."; if (error.contains("CVU screen not detected") || error.contains("Cannot parse CVU")) return stage + "Не удалось получить данные карты.\nПопробуйте повторить."; if (error.contains("No cards found")) return stage + "Карты не найдены на главном экране.\nУбедитесь, что в приложении есть привязанные карты."; if (error.contains("не поддерживается") || error.contains("не подключено")) return error; return stage + "Произошла непредвиденная ошибка.\nПопробуйте повторить позже."; } void BankSetupWizard::onVerificationError(const QString &error) { qWarning() << "[BankSetupWizard] Error:" << error; m_errorText->setText(humanReadableError(error)); m_stack->setCurrentIndex(4); }