first commit

This commit is contained in:
O K
2025-08-28 21:44:55 +03:00
commit a9dff0a52a
6 changed files with 852 additions and 0 deletions

12
config_uk.xml Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ibanpro</name>
<displayName><![CDATA[Iban.pro]]></displayName>
<version><![CDATA[1.0.0]]></version>
<description><![CDATA[Iban.pro]]></description>
<author><![CDATA[panariga]]></author>
<tab><![CDATA[advertising_marketing]]></tab>
<confirmUninstall><![CDATA[Are you sure about removing these details?]]></confirmUninstall>
<is_configurable>1</is_configurable>
<need_instance>0</need_instance>
</module>

View File

@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
// Ensure this file is not directly accessed
if (!defined('_PS_VERSION_')) {
exit;
}
/**
* @property \IbanPro $module
*/
class IbanProValidationModuleFrontController extends ModuleFrontController
{
/**
* Processes the incoming POST request for payment validation.
* Determines the payment method and delegates to the appropriate handler.
*
* @see FrontController::postProcess()
*/
public function postProcess(): void
{
// Set the template to render the form for redirection
if (
!Validate::isLoadedObject($this->context->cart) ||
!Validate::isLoadedObject($this->context->customer) ||
!Validate::isLoadedObject($this->context->currency) ||
!$this->module->active // Ensure the module is active
) {
PrestaShopLogger::addLog(
'banProValidation: Initial validation failed. Cart ID: ' . (int)$this->context->cart->id .
' Customer ID: ' . (int)$this->context->customer->id . ' Module Active: ' . (int)$this->module->active,
3
);
Tools::redirect('index.php?controller=order&step=1');
return;
}
$this->processOrderCreation();
}
/**
* Processes the online payment flow.
* This method orchestrates the steps for creating an order and redirecting to LiqPay.
*/
protected function processOrderCreation(): void
{
$this->validateCartAndModuleAccess();
$order = $this->createOrder();
Tools::redirect($this->context->link->getPageLink('order-confirmation', true, $this->context->language->id, [
'id_cart' => (int)$this->context->cart->id,
'id_module' => (int)$this->module->id,
'id_order' => (int)$order->id,
'key' => $this->context->customer->secure_key,
]));
}
/**
* Validates the cart and checks if the payment module is authorized for the current cart.
* Redirects to the order page if validation fails.
*/
protected function validateCartAndModuleAccess(): void
{
// Check cart validity (redundant if init is robust, but good for defensive programming)
if (
(int)$this->context->cart->id_customer === 0 ||
(int)$this->context->cart->id_address_delivery === 0 ||
(int)$this->context->cart->id_address_invoice === 0 ||
(int)$this->context->cart->id === 0 // Check for valid cart ID (not null or 0)
) {
PrestaShopLogger::addLog(
'IbanProValidation: Invalid cart details during validation. Cart ID: ' . (int)$this->context->cart->id,
3
);
Tools::redirect('index.php?controller=order&step=1');
}
// Check that this payment option is still available
$authorized = false;
foreach (Module::getPaymentModules() as $module) {
if ($module['name'] === $this->module->name) {
$authorized = true;
break;
}
}
if (!$authorized) {
PrestaShopLogger::addLog(
'IbanProValidation: Payment method is not authorized for cart ' . (int)$this->context->cart->id,
3
);
// Using die() with l() is acceptable for payment module authorization errors.
die($this->module->l('This payment method is not available.', 'validation'));
}
}
/**
* Creates a new PrestaShop order for the current cart.
*
* @return \Order The newly created order object.
* @throws PrestaShopException If order creation fails.
*/
protected function createOrder(): Order
{
try {
// Validate and create the order
$this->module->validateOrder(
(int)$this->context->cart->id,
Configuration::get('IBANTRANSFER_OS_CREATION'), // Use the appropriate pending status for your module
(float)$this->context->cart->getOrderTotal(true, Cart::BOTH),
$this->module->paymentMethodName, // Payment method name for order history
null, // Message
null, // Extra vars
(int)$this->context->currency->id,
false, // Don't convert currency
$this->context->customer->secure_key
);
// After validateOrder, the module's currentOrder property holds the new order ID
$order = new Order((int)$this->module->currentOrder);
if (!Validate::isLoadedObject($order)) {
throw new PrestaShopException('Failed to load the newly created order.');
}
return $order;
} catch (Throwable $e) {
PrestaShopLogger::addLog(
'IbanProValidation: Order creation failed for cart ' . (int)$this->context->cart->id . '. Error: ' . $e->getMessage() . ' Trace: ' . $e->getTraceAsString(),
4
);
Tools::redirect('index.php?controller=order&step=1'); // Redirect to order page on failure
// In a real scenario, you might want to display a more user-friendly error or log to the user.
}
}
}

369
ibanpro.php Normal file
View File

@@ -0,0 +1,369 @@
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
class IbanPro extends PaymentModule
{
public $paymentMethodName = 'IBAN';
public function __construct()
{
$this->name = 'ibanpro';
$this->tab = 'advertising_marketing';
$this->version = '1.0.0';
$this->author = 'panariga';
$this->need_instance = 0;
parent::__construct();
$this->displayName = $this->trans('Iban.pro');
$this->description = $this->trans('Iban.pro module for Ukraine');
$this->confirmUninstall = $this->trans('Are you sure about removing these details?');
$this->ps_versions_compliancy = array(
'min' => '1.7',
'max' => _PS_VERSION_,
);
}
public function install()
{
return parent::install() &&
$this->registerHook('displayOrderConfirmation') && $this->registerHook('paymentOptions');
}
public function uninstall()
{
if (!parent::uninstall()) {
return false;
}
return true;
}
/**
* Entry point for the module's configuration page.
* Handles form submission and displays the configuration form.
*/
public function getContent()
{
$output = '';
// Process form submission if it occurs
if (Tools::isSubmit('submitIbanTransferModule')) {
$output .= $this->postProcess();
}
// Display the configuration form
return $output . $this->renderForm();
}
/**
* Saves the configuration settings when the form is submitted.
*/
protected function postProcess()
{
if (Tools::isSubmit('submitIbanTransferModule')) {
// A list of configuration keys to update. This makes it easy to add more later.
$configKeys = [
'IBANTRANSFER_IBAN',
'IBANTRANSFER_RECEIVER_NAME',
'IBANTRANSFER_RECEIVER_CODE',
'IBANTRANSFER_DESCRIPTION_APPEND',
'IBANTRANSFER_API_TOKEN',
'IBANTRANSFER_OS_CREATION',
'IBANTRANSFER_OS_FULL_PAYMENT',
'IBANTRANSFER_OS_PARTIAL_PAYMENT',
];
foreach ($configKeys as $key) {
// We use Tools::getValue which sanitizes the input
Configuration::updateValue($key, Tools::getValue($key));
}
return $this->displayConfirmation($this->l('Settings have been updated successfully.'));
}
return '';
}
/**
* Renders the configuration form using PrestaShop's HelperForm.
*/
public function renderForm()
{
// Define the structure of the configuration form
$fields_form = [];
// === Fieldset 1: Bank Account Details ===
$fields_form[0]['form'] = [
'legend' => [
'title' => $this->l('Bank Account Details'),
'icon' => 'icon-university', // A more appropriate icon
],
'input' => [
[
'type' => 'text',
'label' => $this->l('IBAN Account'),
'name' => 'IBANTRANSFER_IBAN',
'required' => true,
'desc' => $this->l('Enter the full IBAN for receiving payments.'),
'class' => 'fixed-width-xxl',
],
[
'type' => 'text',
'label' => $this->l('Receiver Name'),
'name' => 'IBANTRANSFER_RECEIVER_NAME',
'required' => true,
'desc' => $this->l('Enter the full name of the account holder (individual or company).'),
'class' => 'fixed-width-xl',
],
[
'type' => 'text',
'label' => $this->l('Receiver ID Code (EDRPOU/TIN)'),
'name' => 'IBANTRANSFER_RECEIVER_CODE',
'required' => true,
'desc' => $this->l('Enter the unique identification code for the receiver.'),
'class' => 'fixed-width-lg',
],
[
'type' => 'text',
'label' => $this->l('Append to Payment Description'),
'name' => 'IBANTRANSFER_DESCRIPTION_APPEND',
'required' => false,
'desc' => $this->l('This text will be added after the default description (e.g., "Payment for order #..."). You can use {order_reference} or {cart_id} as placeholders.'),
'class' => 'fixed-width-xxl',
],
],
'submit' => [
'title' => $this->l('Save'),
'class' => 'btn btn-default pull-right',
],
];
// === Fieldset 2: Order Statuses Configuration ===
$order_states = OrderState::getOrderStates((int)$this->context->language->id);
$fields_form[1]['form'] = [
'legend' => [
'title' => $this->l('Order Statuses'),
'icon' => 'icon-cogs',
],
'input' => [
[
'type' => 'select',
'label' => $this->l('Order Status on Creation'),
'name' => 'IBANTRANSFER_OS_CREATION',
'options' => [
'query' => $order_states,
'id' => 'id_order_state',
'name' => 'name',
],
'desc' => $this->l('The status for a new order created with this payment method (e.g., "Awaiting bank wire payment").'),
],
[
'type' => 'select',
'label' => $this->l('Order Status on Full Payment'),
'name' => 'IBANTRANSFER_OS_FULL_PAYMENT',
'options' => [
'query' => $order_states,
'id' => 'id_order_state',
'name' => 'name',
],
'desc' => $this->l('The status when the full payment is confirmed (e.g., "Payment accepted").'),
],
[
'type' => 'select',
'label' => $this->l('Order Status on Partial Payment'),
'name' => 'IBANTRANSFER_OS_PARTIAL_PAYMENT',
'options' => [
'query' => $order_states,
'id' => 'id_order_state',
'name' => 'name',
],
'desc' => $this->l('Optional: The status if a partial payment is received (e.g., "On backorder (paid)").'),
],
],
'submit' => [
'title' => $this->l('Save'),
'class' => 'btn btn-default pull-right',
],
];
// === Fieldset 3: API Integration ===
$fields_form[2]['form'] = [
'legend' => [
'title' => $this->l('API Integration (iban.pro)'),
'icon' => 'icon-key',
],
'input' => [
[
'type' => 'password', // Using 'password' type hides the token
'label' => $this->l('iban.pro Access Token'),
'name' => 'IBANTRANSFER_API_TOKEN',
'required' => false,
'desc' => $this->l('Enter your API access token from iban.pro to generate QR codes.'),
'class' => 'fixed-width-xxl',
],
],
'submit' => [
'title' => $this->l('Save'),
'class' => 'btn btn-default pull-right',
],
];
// --- HelperForm Boilerplate ---
$helper = new HelperForm();
$helper->module = $this;
$helper->name_controller = $this->name;
$helper->token = Tools::getAdminTokenLite('AdminModules');
$helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name;
$helper->default_form_language = (int)Configuration::get('PS_LANG_DEFAULT');
$helper->title = $this->displayName;
$helper->show_toolbar = true;
$helper->toolbar_scroll = true;
$helper->submit_action = 'submitIbanTransferModule';
// Load current values from the database
$configFields = [
'IBANTRANSFER_IBAN',
'IBANTRANSFER_RECEIVER_NAME',
'IBANTRANSFER_RECEIVER_CODE',
'IBANTRANSFER_DESCRIPTION_APPEND',
'IBANTRANSFER_API_TOKEN',
'IBANTRANSFER_OS_CREATION',
'IBANTRANSFER_OS_FULL_PAYMENT',
'IBANTRANSFER_OS_PARTIAL_PAYMENT'
];
foreach ($configFields as $field) {
$helper->fields_value[$field] = Configuration::get($field);
}
return $helper->generateForm($fields_form);
}
public function hookdisplayOrderConfirmation($params)
{
$order = $params['order'];
if ($order->payment == $this->paymentMethodName) {
return $this->getTPL($params['order']);
}
}
public function getTPL(Order $order)
{
$address = new Address($order->id_address_invoice);
$total = $order->total_paid;
$description = $order->reference . '; оплата замовлення #' . $order->id . '; ' . $address->lastname . ' ' . $address->firstname;
$NBULink = $this->getNBUv2($total, $description);
if (trim(shell_exec('command -v qrencode'))) {
// 3. If it exists, generate the QR code.
// CRITICAL: Always use escapeshellarg() on variables passed to shell commands
// to prevent command injection vulnerabilities.
$escapedNBULink = escapeshellarg($NBULink);
$qr = shell_exec("qrencode -o - -s 4 -t SVG $escapedNBULink");
// 4. (Optional but recommended) Add a final check in case the command fails for other reasons.
if (empty($qr)) {
$qr = $this->context->smarty->fetch($this->local_path . 'views/templates/front/emptyQR.svg');
}
} else {
// 5. If 'qrencode' is not found, assign the placeholder.
$qr = $this->context->smarty->fetch($this->local_path . 'views/templates/front/emptyQR.svg');
}
$this->context->smarty->assign([
'reciever_name' => Configuration::get('IBANTRANSFER_RECEIVER_NAME'),
'iban' => Configuration::get('IBANTRANSFER_IBAN'),
'reciever_code' => Configuration::get('IBANTRANSFER_RECEIVER_CODE'),
'description' => $description . ' ' . Configuration::get('IBANTRANSFER_DESCRIPTION_APPEND'),
'total' => $total,
'NBULink' => $NBULink,
'qr' => $qr,
]);
return $this->display(__FILE__, '/views/templates/front/displayOrderConfirmation.tpl');
}
public function getNBUv2($amount, $description)
{
$display = '';
$ibanSUM = sprintf('UAH%s', number_format((float) $amount, 2, '.', ''));
$codeContentsV2 = implode("\n", array(
mb_convert_encoding('BCD', 'ASCII'),
mb_convert_encoding(sprintf('%03d', '2'), 'ASCII'),
mb_convert_encoding('2', 'ASCII'),
mb_convert_encoding('UCT', 'ASCII'),
'',
mb_convert_encoding($this->reciever_name, 'windows-1251'),
mb_convert_encoding(Configuration::get('IBANTRANSFER_IBAN'), 'ASCII'),
mb_convert_encoding($ibanSUM, 'ASCII'),
mb_convert_encoding($this->reciever_code, 'windows-1251'),
'',
'',
mb_convert_encoding($description, 'windows-1251'),
$display,
'',
));
return 'https://bank.gov.ua/qr/' . $this->base64url_encode($codeContentsV2);
}
public static function base64url_encode($data)
{
// First of all you should encode $data to Base64 string
$b64 = base64_encode($data);
// Make sure you get a valid result, otherwise, return FALSE, as the base64_encode() function do
if ($b64 === false) {
return false;
}
// Convert Base64 to Base64URL by replacing “+” with “-” and “/” with “_”
$url = strtr($b64, '+/', '-_');
// Remove padding character from the end of line and return the Base64URL result
return rtrim($url, '=');
}
public function hookPaymentOptions($params)
{
echo Configuration::get('IBANTRANSFER_IBAN');
if (!$this->active) {
return;
}
if (!Configuration::get('IBANTRANSFER_IBAN') || !Configuration::get('IBANTRANSFER_RECEIVER_CODE') || !Configuration::get('IBANTRANSFER_RECEIVER_NAME')) {
return;
}
$payment_options = [
$this->getPaymentOption(),
];
return $payment_options;
}
public function getPaymentOption()
{
$externalOption = new \PrestaShop\PrestaShop\Core\Payment\PaymentOption();
$externalOption->setCallToActionText($this->l('Оплата переказом за реквізитами IBAN'))
->setAction($this->context->link->getModuleLink($this->name, 'validation', ['methodPayment' => 'onlinePayment'], true))
// ->setInputs([ 'onlinePayment' => true, ])
->setAdditionalInformation($this->context->smarty->fetch('module:' . $this->name . '/views/templates/front/payment_info.tpl'))
// ->setLogo(Media::getMediaPath(_PS_MODULE_DIR_ . $this->name . '/payment.jpg'))
;
return $externalOption;
}
}

View File

@@ -0,0 +1,323 @@
<div class="order-confirmation__details card bg-light border-1 mb-3" id="wire-payment-details">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex align-items-center">
<i class="material-icons me-2">account_balance</i>
<h5 class="mb-0">Реквізити для оплати замовлення</h5>
</div>
<div class="card-body">
<!-- ============== DYNAMIC CONTENT AREA ============== -->
<!-- This area will be populated by JavaScript based on device -->
<!-- 1. FOR DESKTOP: QR Code is primary -->
<div id="desktop-payment-section" class="d-none d-lg-block text-center p-3 mb-4 bg-light rounded border">
<p class="text-muted">Відскануйте QR-код у банківському додатку на телефоні. Усі реквізити та сума будуть
заповнені автоматично.</p>
<div class="my-3">
{$qr nofilter}
</div>
</div>
<!-- 2. FOR MOBILE: Deep links are primary -->
{* <div id="mobile-payment-section" class="d-lg-none mb-4">
<div class="text-center">
<h6 class="mb-2"><strong>Найшвидший спосіб на телефоні</strong></h6>
<p class="text-muted">Оберіть ваш банк для автоматичного відкриття додатку з реквізитами.</p>
</div>
<div id="mobile-bank-links-container" class="d-grid gap-2 mt-3">
<!-- Bank buttons will be injected here by JS -->
</div>
</div> *}
<!-- 3. FOR DESKTOP: Accordion with links is secondary -->
{* <div class="d-none d-lg-block mb-4">
<div class="accordion" id="desktopBankLinksAccordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
<i class="material-icons me-2">touch_app</i>
Оплата через посилання (для мобільних пристроїв)
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne"
data-bs-parent="#desktopBankLinksAccordion">
<div class="accordion-body">
<p class="text-muted">Ці посилання призначені для швидкої оплати на смартфоні. Ви можете надіслати їх
собі на телефон.</p>
<div id="desktop-bank-links-container">
<!-- Bank links list will be injected here by JS -->
</div>
</div>
</div>
</div>
</div>
</div> *}
<!-- ============== END DYNAMIC CONTENT AREA ============== -->
<!-- Separator & Manual Details -->
<div class="d-flex justify-content-center align-items-center py-2 border-bottom">
{* <div class="d-flex align-items-center">
<button id="copy-nbulink" class="d-none">{$NBULink}</button>
<a class="btn btn-sm btn-outline-secondary me-2" href="{$NBULink}" target="_blank" title="Копіювати">
Посилання НБУ <i class="material-icons">open_in_new</i>
</a>
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="copy-nbulink" title="Копіювати">
<i class="material-icons">content_copy</i>
</button>
</div> *}
<div class="d-flex align-items-center">
<button id="copy-nbulink" class="d-none">{$NBULink}</button>
<a class="btn btn-sm btn-outline-secondary me-2" href="{$NBULink}" target="_blank" title="Копіювати">
Посилання НБУ <i class="material-icons">open_in_new</i>
</a>
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="copy-nbulink" title="Копіювати">
<i class="material-icons">content_copy</i>
</button>
</div>
</div>
<div id="manual-payment-info">
<!-- The copy-pasteable details from the first variant go here unchanged -->
<div class="d-flex flex-wrap justify-content-between align-items-center py-2 border-bottom">
<span class="text-muted p-2">Отримувач:</span>
<div class="d-flex align-items-center">
<strong id="copy-receiver" class="me-2">{$reciever_name}</strong>
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="copy-receiver"
title="Копіювати">
<i class="material-icons">content_copy</i>
</button>
</div>
</div>
<div class="d-flex flex-wrap justify-content-between align-items-center py-2 border-bottom">
<span class="text-muted p-2">Код отримувача (ЄДРПОУ):</span>
<div class="d-flex align-items-center">
<strong id="copy-code" class="me-2 font-monospace">{$reciever_code}</strong>
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="copy-code" title="Копіювати">
<i class="material-icons">content_copy</i>
</button>
</div>
</div>
<div class="d-flex flex-wrap justify-content-between align-items-center py-2 border-bottom">
<span class="text-muted p-2">IBAN:</span>
<div class="d-flex align-items-center">
<strong id="copy-iban" class="me-2 font-monospace">{$iban}</strong>
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="copy-iban" title="Копіювати">
<i class="material-icons">content_copy</i>
</button>
</div>
</div>
<div class="d-flex flex-wrap justify-content-between align-items-center py-2 border-bottom">
<span class="text-muted p-2">Призначення платежу:</span>
<div class="d-flex align-items-center">
<strong id="copy-description" class="me-2">{$description}</strong>
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="copy-description"
title="Копіювати">
<i class="material-icons">content_copy</i>
</button>
</div>
</div>
<div class="d-flex flex-wrap justify-content-between align-items-center py-2">
<span class="text-muted p-2">Сума до сплати:</span>
<div class="d-flex align-items-center">
<strong id="copy-amount" class="h5 mb-0 text-primary">{$total|round:2}</strong><strong
class="h5 mb-0 me-2 text-primary">&nbsp;₴</strong>
<button class="btn btn-sm btn-outline-secondary copy-btn" data-copy-target="copy-amount" title="Копіювати">
<i class="material-icons">content_copy</i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{* --- CSS and JavaScript --- *}
<style>
{literal}
.copy-btn {
padding: 0.25rem 0.5rem;
line-height: 1;
border: 1px solid #ccc;
}
.copy-btn .material-icons-outlined {
font-size: 1rem;
vertical-align: middle;
}
.font-monospace {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.bank-logo {
height: 24px;
margin-right: 12px;
}
.bank-btn {
display: flex;
align-items-center;
justify-content: center;
font-size: 1.1rem;
padding: 0.75rem;
}
{/literal}
</style>
<!-- Step 1: Pass Smarty variables to JavaScript -->
<script>
const paymentDetails = {
iban: '{$iban|escape:'javascript':'UTF-8'}',
receiverName: '{$reciever_name|escape:'javascript':'UTF-8'}',
receiverCode: '{$reciever_code|escape:'javascript':'UTF-8'}',
description: '{$description|escape:'javascript':'UTF-8'}',
amount: '{$total|round:2}'
};
</script>
<!-- Step 2: Main JavaScript logic -->
<script>
{literal}
document.addEventListener('DOMContentLoaded', function() {
// The `paymentDetails` object is now available here.
// --- OS Detection ---
function getMobileOS() {
const ua = navigator.userAgent;
if (/android/i.test(ua)) { return "Android"; }
if (/iPad|iPhone|iPod/.test(ua)) { return "iOS"; }
return "Desktop";
}
const userOS = getMobileOS();
// --- Bank Configuration ---
const banks = [{
name: "Mono",
logo: `<svg class="bank-logo" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="256" cy="256" r="256" fill="black"></circle><path d="M256.001 120.477C182.285 120.477 123.719 179.043 123.719 252.759C123.719 326.475 182.285 385.041 256.001 385.041C329.717 385.041 388.283 326.475 388.283 252.759C388.283 179.043 329.717 120.477 256.001 120.477ZM281.425 291.637C281.425 311.237 266.331 326.331 246.731 326.331H214.243V179.187H246.731C266.331 179.187 281.425 194.281 281.425 213.881V291.637Z" fill="white"></path></svg>`,
packageAndroid: "com.ftband.mono",
packageIOS: "mono",
},
{
name: риват24",
logo: `<svg class="bank-logo" viewBox="0 0 512 512" fill="#84C634" xmlns="http://www.w3.org/2000/svg"><path d="M473.61,250.68c-12.4-58-47.3-107.1-97.5-139.7c-50.2-32.5-111-40.8-168.3-24.8c-57.3,16-105.4,55.5-135.5,110.1 C42.11,251.08,42,328.68,72.21,383.48c30.2,54.7,88.3,94.2,154.5,103.1c66.2,8.9,132.3-14.7,181.1-61.9 C456.61,429.38,485.91,308.68,473.61,250.68z M256.01,411.78c-85.8,0-155.4-69.6-155.4-155.4s69.6-155.4,155.4-155.4 s155.4,69.6,155.4,155.4C411.31,342.18,341.81,411.78,256.01,411.78z"></path><path d="M256,128.98c-70.1,0-127,56.9-127,127s56.9,127,127,127s127-56.9,127-127S326.1,128.98,256,128.98z M316.4,269.88 c-10,0-17.7-7.6-17.7-17.6v-25.2c0-9.9,7.8-17.6,17.7-17.6h25.4v60.3h-25.4V269.88z M213.3,209.58c9.9,0,17.6,7.8,17.6,17.6v50.3 c0,9.9-7.8,17.6-17.6,17.6h-37.4v-85.5h37.4V209.58z" fill="#FFF"></path></svg>`,
packageAndroid: "ua.privatbank.ap24",
packageIOS: "https://www.privat24.ua/rd/send_qr/",
},
{
name: "Пумб",
logo: `<svg class="bank-logo" viewBox="0 0 100 100" fill="#E6443A" xmlns="http://www.w3.org/2000/svg"><path d="M50 0a50 50 0 100 100A50 50 0 0050 0zm0 88a38 38 0 110-76 38 38 0 010 76z" fill="#fff"/><path d="M50 20a30 30 0 100 60 30 30 0 000-60zm-7 42h-6V38h19a12 12 0 110 24h-13z" fill="#fff"/></svg>`,
packageAndroid: "com.fuib.android.spot.online",
packageIOS: "pumb-online.app",
},
{
name: "Sense Bank",
logo: `<svg class="bank-logo" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="100" cy="100" r="100" fill="#262626"/><path d="M149.33 162.3L100 7.7 50.67 162.3H149.33zM100 37.9L129.8 141.7H70.2L100 37.9z" fill="#fff"/></svg>`,
packageAndroid: "ua.alfabank.mobile.android",
packageIOS: "ua.alfabank.mobile.ios",
},
{
name: "Ukrsibbank",
logo: `<svg class="bank-logo" viewBox="0 0 200 200" fill="#1C5B41" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h200v200H0z"/><path d="M100 30L30 100l70 70 70-70-70-70zm0 12l58 58-58 58-58-58 58-58z" fill="#fff"/></svg>`,
packageAndroid: "com.ukrsibbank.uso.android",
packageIOS: "com.ukrsibbank.ukrsibonline.new",
},
{
name: "Креді Агріколь",
logo: `<svg class="bank-logo" viewBox="0 0 100 100" fill="#007A33" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 50V0h10v10H10v30h30V10H20V0h20v10h10v30h30V10H60V0h20v10h10v30h10V0h10v50h-10v40H60V50H40v40H10V50H0zm90 40V50h-10v30H60v10h30zm-80 0V50H0v40h30V80H20V60h10v20H10z" fill="#fff"/></svg>`,
packageAndroid: "ua.creditagricole.mobile.app",
packageIOS: "ua.creditagricole.mobile.app",
},
{
name: "ОТП БАНК",
logo: `<svg class="bank-logo" viewBox="0 0 100 100" fill="#4CAF50" xmlns="http://www.w3.org/2000/svg"><path d="M50 0a50 50 0 100 100A50 50 0 0050 0zm0 88a38 38 0 110-76 38 38 0 010 76z" fill="#fff"/><path d="M69 31h-7l-12 24-12-24h-7l19 38 19-38z" fill="#4CAF50"/></svg>`,
packageAndroid: "ua.otpbank.android",
packageIOS: "otpbankua",
}
];
// --- Deep Link Generation ---
function constructBankUrl(bank, baseNbuLink, os) {
if (os === 'iOS') {
// Privat24 on iOS has a special redirector URL structure
if (bank.name === риват24') {
return bank.packageIOS + encodeURIComponent(baseNbuLink);
}
// Standard iOS custom scheme structure
return baseNbuLink.replace("https://", bank.packageIOS + "://");
}
if (os === 'Android') {
// Standard Android Intent structure
return baseNbuLink.replace("https://", "intent://") + `#Intent;scheme=https;package=${bank.packageAndroid};end`;
}
return baseNbuLink; // Fallback for desktop
}
// --- Render UI ---
// First, construct the standard NBU payment link. This is the base for all deep links.
const nbuBaseUrl = "https://bank.gov.ua/ua/iban-qr";
const nbuParams = new URLSearchParams({
iban: paymentDetails.iban,
amount: paymentDetails.amount, // NBU uses base units, e.g., 1500.00
purpose: paymentDetails.description,
});
const nbuLink = `${nbuBaseUrl}?${nbuParams.toString()}`;
// Get containers
const mobileLinksContainer = document.getElementById('mobile-bank-links-container');
const desktopLinksContainer = document.getElementById('desktop-bank-links-container');
if (userOS === "iOS" || userOS === "Android") {
let mobileHtml = '';
banks.forEach(bank => {
const bankUrl = constructBankUrl(bank, nbuLink, userOS);
mobileHtml += `
<a href="${bankUrl}" class="btn btn-outline-primary bank-btn" role="button">
${bank.logo}
<span>${bank.name}</span>
</a>
`;
});
mobileLinksContainer.innerHTML = mobileHtml;
} else { // Desktop
let desktopHtml = '<ul class="list-group list-group-flush">';
banks.forEach(bank => {
desktopHtml += `
<li class="list-group-item d-flex align-items-center">
${bank.logo}
<span>${bank.name}</span>
</li>
`;
});
desktopHtml += '</ul>';
// desktopLinksContainer.innerHTML = desktopHtml;
}
// --- Copy-to-Clipboard functionality ---
const copyButtons = document.querySelectorAll('.copy-btn');
copyButtons.forEach(button => {
button.addEventListener('click', () => {
const targetId = button.getAttribute('data-copy-target');
const textToCopy = document.getElementById(targetId).innerText;
navigator.clipboard.writeText(textToCopy).then(() => {
const originalIcon = button.innerHTML;
button.innerHTML = '<i class="material-icons">check</i>';
button.classList.add('btn-success');
setTimeout(() => {
button.innerHTML = originalIcon;
button.classList.remove('btn-success');
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
});
});
});
});
{/literal}
</script>

View File

@@ -0,0 +1,4 @@
<svg width="164" height="164" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" style="background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px;">
<text x="50" y="45" font-family="Arial, sans-serif" font-size="10" fill="#888" text-anchor="middle" dominant-baseline="central">QR-код</text>
<text x="50" y="60" font-family="Arial, sans-serif" font-size="10" fill="#888" text-anchor="middle" dominant-baseline="central">недоступний</text>
</svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@@ -0,0 +1,5 @@
<div class="alert alert-info" role="alert">
<p class="">Оплата за реквізитами на банківський рахунок.</p>
</div>