// ozon/pay_by_phone.js // // JS-порт Ozon::PayByPhoneScript. Логика 1:1 с C++ (см. // android/ozon/PayByPhoneScript.cpp). // // Контракт: // params.account — {id, materialId, appCode, deviceId, lastNumbers, currency} // params.phone — телефон получателя // params.bankName — название банка получателя (для "Другой банк") // params.amount — сумма перевода function run(host) { var account = host.params.account; var phone = host.params.phone; var bankName = host.params.bankName; var amount = host.params.amount; var device = host.dao.deviceGetById(account.deviceId); var app = host.dao.bankProfileGet(account.deviceId, account.appCode); if (!device || !device.id || !app || app.id < 0) { var msg = "Device or app not found: " + account.deviceId; host.log.error(msg); return msg; } var adbId = device.adbSerial; var counter = 0; host.dump.beginScope("ozon", "CREATE_TRANSACTION_PHONE", adbId, { materialId: account.materialId, amount: amount, phone: phone, }); try { host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter)); var adNode = host.parser.tryToFindAdBanner(); if (!adNode.isEmpty) { host.adb.makeTap(adbId, adNode.x, adNode.y); host.timer.sleep(1000); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter)); } if (!host.parser.isHomeScreen()) { if (!host.helpers.runAppAndGoToHomeScreen(adbId, app.package, app.pinCode, device.screenWidth, device.screenHeight)) { var err = "Cannot reach home screen: " + device.name + " (" + device.id + ")"; host.adb.tryToKillApplication(adbId, app.package); return err; } host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter)); } host.helpers.tryToCloseAllBannersOnHomePage(adbId, device.screenWidth, device.screenHeight); host.timer.sleep(500); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter)); var gp = host.parser.tryToFindGooglePlayBanner(device.screenWidth, device.screenHeight); if (!gp.isEmpty) { host.adb.makeTap(adbId, gp.x, gp.y); host.timer.sleep(1500); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter)); } var orderCard = host.parser.findTextNode("Заказать карту"); if (!orderCard.isEmpty) { host.adb.makeTap(adbId, Math.floor(device.screenWidth / 2), Math.floor(device.screenHeight / 20)); host.timer.sleep(1000); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(adbId, ++counter)); } var result = makePayment(host, adbId, app.package, app.pinCode, app.phone, device.screenWidth, device.screenHeight, account, phone, bankName, amount, counter); host.dump.endScope(result || ""); return result; } catch (e) { host.dump.endScope(String(e)); throw e; } } function makePayment(host, deviceId, packageName, pinCode, bankProfilePhone, width, height, account, phone, bankName, amount, counter) { function checkApp() { if (!host.helpers.isAppRunning(deviceId, packageName)) { return "Приложение было закрыто"; } return null; } // 1. "Перевести" var transferBtn = host.parser.findButtonNode("Перевести", false); if (transferBtn.isEmpty) return "Кнопка 'Перевести' не найдена на домашнем экране"; host.adb.makeTap(deviceId, transferBtn.x, transferBtn.y); host.timer.sleep(2000); // 2. Ждём экран "Платежи и переводы" с первым EditText var paymentsPageLoaded = false; for (var i = 0; i < 10; ++i) { var e = checkApp(); if (e) return e; host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height); if (!host.parser.findFirstEditText().isEmpty) { paymentsPageLoaded = true; break; } host.timer.sleep(1500); } if (!paymentsPageLoaded) return "Экран 'Платежи и переводы' не загрузился"; // 3. Тапаем "Телефон получателя" var phoneFieldTap = host.parser.findFirstEditText(); host.adb.makeTap(deviceId, phoneFieldTap.x, phoneFieldTap.y); host.timer.sleep(2000); // 4. Ждём "Кому перевести" var phoneScreenLoaded = false; for (var j = 0; j < 10; ++j) { var e2 = checkApp(); if (e2) return e2; host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height); var notNow = host.parser.findButtonNode("Не сейчас", false); if (!notNow.isEmpty) { host.adb.makeTap(deviceId, notNow.x, notNow.y); host.timer.sleep(1500); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); } if (!host.parser.findTextNode("Кому перевести").isEmpty) { phoneScreenLoaded = true; break; } host.timer.sleep(1500); } if (!phoneScreenLoaded) return "Экран 'Кому перевести' не загрузился"; // 5. Вводим телефон var phoneInput = {isEmpty: true}; for (var k = 0; k < 10; ++k) { var e3 = checkApp(); if (e3) return e3; host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height); phoneInput = host.parser.findFirstEditText(); if (!phoneInput.isEmpty) break; host.timer.sleep(1500); } if (phoneInput.isEmpty) return "Поле ввода телефона не найдено"; host.adb.makeTap(deviceId, phoneInput.x, phoneInput.y); host.timer.sleep(500); host.adb.inputText(deviceId, phone); host.timer.sleep(2000); // 6. Ждём экран "По номеру телефона" с обработкой выбора банка / последних var sumScreenLoaded = false; var bankSelected = false; var otherBankClicked = false; var freezePrev = "", freezeStreak = 0; var phoneDigits = phone.replace(/[^0-9]/g, ''); for (var attempt = 0; attempt < 20; ++attempt) { var e4 = checkApp(); if (e4) return e4; var xml = host.adb.getScreenDumpAsXml(deviceId, ++counter); host.parser.parseAndSaveXml(xml); host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height); if (host.helpers.isUsefulDump(xml)) { if (freezePrev && xml === freezePrev) { if (++freezeStreak >= 3) { host.log.warn("screen frozen"); break; } } else freezeStreak = 0; freezePrev = xml; } // Уже на экране суммы? var sumTitle = host.parser.findTextNode("По номеру телефона"); if (!sumTitle.isEmpty && sumTitle.y < height / 7) { sumScreenLoaded = true; break; } // Кнопка с этим телефоном (новый перевод / последние) var foundPhoneBtn = false; var allBtns = host.parser.findAllButtons(); for (var bi = 0; bi < allBtns.length; ++bi) { var btn = allBtns[bi]; var btnDigits = (btn.text || "").replace(/[^0-9]/g, ''); if (btnDigits.indexOf(phoneDigits) !== -1 || (btnDigits.length >= 10 && phoneDigits.indexOf(btnDigits.slice(-10)) !== -1)) { host.log.debug("Found button with phone: " + btn.text); host.adb.makeTap(deviceId, btn.x, btn.y); host.timer.sleep(2000); foundPhoneBtn = true; break; } } if (foundPhoneBtn) continue; // Экран выбора банка if (!bankSelected) { // Вариант 1: "Другой банк" (content-desc="other-bank-item") var foundOtherBank = false; var allNodes = host.parser.findAllNodes(); for (var ai = 0; ai < allNodes.length; ++ai) { var n = allNodes[ai]; if ((n.contentDesc || "").indexOf("other-bank-item") !== -1) { host.adb.makeTap(deviceId, n.x, n.y); host.timer.sleep(2000); foundOtherBank = true; otherBankClicked = true; break; } } if (foundOtherBank) continue; // Вариант 2: после клика на "Другой банк" — поиск + выбор if (otherBankClicked) { var bankInput = host.parser.findFirstEditText(); if (!bankInput.isEmpty) { // Переключаем IME до тапа — иначе layout сдвинется if (!host.adb.enableAdbIMEKeyboard(deviceId)) { return "Не удалось активировать ADB Keyboard (см. логи 'ime list -s')"; } host.timer.sleep(800); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); var freshBankInput = host.parser.findFirstEditText(); if (!freshBankInput.isEmpty) bankInput = freshBankInput; host.adb.makeTap(deviceId, bankInput.x, bankInput.y); host.timer.sleep(500); host.adb.inputAdbIMEText(deviceId, bankName); host.timer.sleep(2500); // Верификация host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); var verify = host.parser.findFirstEditText(); if (!verify.text) { host.log.warn("Поле банка пустое — ретрай"); host.adb.makeTap(deviceId, bankInput.x, bankInput.y); host.timer.sleep(500); host.adb.inputAdbIMEText(deviceId, bankName); host.timer.sleep(2500); } host.adb.disableAdbIMEKeyboard(deviceId); host.timer.sleep(2000); // Ищем банк в списке (Button с непустым content-desc, не служебный) var bankFound = false; for (var ba = 0; ba < 10; ++ba) { host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); var searchField = host.parser.findFirstEditText(); if (!searchField.text || !(searchField.text || "").trim()) { host.timer.sleep(1500); continue; } var all2 = host.parser.findAllNodes(); for (var ai2 = 0; ai2 < all2.length; ++ai2) { var node2 = all2[ai2]; if (node2.className === "android.widget.Button" && node2.contentDesc && node2.contentDesc !== "base-go-back-button" && node2.contentDesc !== "native-toolbar-close-button" && node2.contentDesc !== "clear-input-icon") { var tapX = node2.x1 + Math.floor(node2.width / 4); host.log.debug("Found bank: " + node2.contentDesc + " tapX=" + tapX); host.adb.makeTap(deviceId, tapX, node2.y); bankFound = true; break; } } if (bankFound) break; host.timer.sleep(1500); } if (!bankFound) return "Банк '" + bankName + "' не найден в списке"; bankSelected = true; host.timer.sleep(2000); continue; } } } host.timer.sleep(1500); } if (!sumScreenLoaded) return "Экран ввода суммы не загрузился"; // 7. Вводим сумму — берём верхний (минимальный y1) EditText с input___ var sumInput = {isEmpty: true}; for (var sa = 0; sa < 10; ++sa) { var e5 = checkApp(); if (e5) return e5; var candidates = []; var allNodes2 = host.parser.findAllNodes(); for (var ci = 0; ci < allNodes2.length; ++ci) { var nn = allNodes2[ci]; if (nn.className !== "android.widget.EditText") continue; if (nn.height <= 0 || nn.height > height * 4 / 5) continue; if (String(nn.resourceId || "").indexOf("input___") !== 0) continue; candidates.push(nn); } if (candidates.length > 0) { candidates.sort(function(a, b) { return a.y1 - b.y1; }); sumInput = candidates[0]; // верхний = amount break; } sumInput = host.parser.findFirstEditText(); if (!sumInput.isEmpty) break; host.timer.sleep(1500); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height); } if (sumInput.isEmpty) return "Поле ввода суммы не найдено"; host.adb.makeTap(deviceId, sumInput.x, sumInput.y); host.timer.sleep(500); host.adb.inputText(deviceId, String(Math.floor(amount))); host.timer.sleep(1000); host.adb.hideKeyboard(deviceId); host.timer.sleep(800); // 8. "Продолжить" var continueBtn = {isEmpty: true}; for (var ca = 0; ca < 5; ++ca) { host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); continueBtn = host.parser.findButtonNode("Продолжить", false); if (!continueBtn.isEmpty && continueBtn.height > 0) break; host.adb.hideKeyboard(deviceId); host.timer.sleep(1000); } if (continueBtn.isEmpty || continueBtn.height === 0) { return "Кнопка 'Продолжить' не найдена или перекрыта клавиатурой"; } host.adb.makeTap(deviceId, continueBtn.x, continueBtn.y); host.timer.sleep(2000); // 9. Ждём "Подтвердите перевод" var confirmLoaded = false; for (var cf = 0; cf < 10; ++cf) { var e6 = checkApp(); if (e6) return e6; host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height); if (!host.parser.findTextNode("Подтвердите перевод").isEmpty) { confirmLoaded = true; break; } host.timer.sleep(1500); } if (!confirmLoaded) return "Экран подтверждения не загрузился"; // 10. Тапаем "Перевести ..." var transferConfirmBtn = {isEmpty: true}; for (var tc = 0; tc < 10; ++tc) { var e7 = checkApp(); if (e7) return e7; transferConfirmBtn = host.parser.findButtonNode("Перевести", true); if (!transferConfirmBtn.isEmpty) break; host.timer.sleep(1500); host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height); } if (transferConfirmBtn.isEmpty) return "Кнопка 'Перевести ...' не найдена на экране подтверждения"; host.adb.makeTap(deviceId, transferConfirmBtn.x, transferConfirmBtn.y); host.timer.sleep(3000); // PIN после confirm host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); var pinNodes = host.parser.tryToFindPinCode(pinCode, "Введите ПИН"); if (pinNodes && pinNodes.length > 0) { host.helpers.inputPinCode(deviceId, pinNodes); host.timer.sleep(3000); } // 11. Ждём результат for (var w = 0; w < 15; ++w) { var e8 = checkApp(); if (e8) return e8; host.parser.parseAndSaveXml(host.adb.getScreenDumpAsXml(deviceId, ++counter)); host.helpers.closeGooglePlayBannerIfVisible(deviceId, width, height); var pinLater = host.parser.tryToFindPinCode(pinCode, "Введите ПИН"); if (pinLater && pinLater.length > 0) { host.helpers.inputPinCode(deviceId, pinLater); host.timer.sleep(3000); continue; } if (!host.parser.findTextNode("Успешно").isEmpty) { var tx = { accountId: account.id, amount: -amount, phone: phone, bankName: "ozon", type: "phone", status: "complete", timestamp: new Date().toISOString(), completeTime: new Date().toISOString(), bankTime: formatBankTime(new Date()), }; var txId = host.dao.transactionInsert(tx); host.log.debug("Saved tx to DB, id: " + txId); if (txId !== -1) { host.helpers.saveTransactionReceipt(deviceId, txId); var externalId = "ozon_" + bankProfilePhone + "_" + Math.floor(Date.now() / 1000); host.dao.transactionUpdateBankTrExternalId(txId, externalId); } host.adb.goBack(deviceId); host.timer.sleep(1000); host.adb.goBack(deviceId); host.timer.sleep(1000); return ""; } if (!host.parser.findTextNode("Не удалось").isEmpty) { host.adb.goBack(deviceId); host.timer.sleep(1000); host.adb.goBack(deviceId); host.timer.sleep(1000); return "Перевод не удался (экран 'Не удалось')"; } host.timer.sleep(2000); } return "Не дождались результата перевода"; } function formatBankTime(d) { var utc = d.getTime(); var msk = new Date(utc + 3 * 3600 * 1000); function pad(n) { return n < 10 ? '0' + n : '' + n; } return pad(msk.getUTCDate()) + "." + pad(msk.getUTCMonth() + 1) + "." + msk.getUTCFullYear() + ", " + pad(msk.getUTCHours()) + ":" + pad(msk.getUTCMinutes()); }