diff --git a/app/PaymentDrivers/Blockonomics/Blockonomics.php b/app/PaymentDrivers/Blockonomics/Blockonomics.php index b2e5d8888703..bade8e445947 100644 --- a/app/PaymentDrivers/Blockonomics/Blockonomics.php +++ b/app/PaymentDrivers/Blockonomics/Blockonomics.php @@ -13,19 +13,20 @@ namespace App\PaymentDrivers\Blockonomics; use App\Models\Payment; -use App\Models\PaymentType; -use App\Models\GatewayType; use App\Models\SystemLog; -use App\PaymentDrivers\BlockonomicsPaymentDriver; -use App\Utils\Traits\MakesHash; -use App\PaymentDrivers\Common\MethodInterface; -use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; -use App\Exceptions\PaymentFailed; +use App\Models\GatewayType; +use App\Models\PaymentType; use App\Jobs\Util\SystemLogger; -use App\Jobs\Mail\PaymentFailureMailer; +use App\Utils\Traits\MakesHash; +use App\Exceptions\PaymentFailed; use Illuminate\Support\Facades\Http; +use App\Jobs\Mail\PaymentFailureMailer; +use App\PaymentDrivers\Common\MethodInterface; +use App\PaymentDrivers\BlockonomicsPaymentDriver; +use App\PaymentDrivers\Common\LivewireMethodInterface; +use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; -class Blockonomics implements MethodInterface +class Blockonomics implements LivewireMethodInterface { use MakesHash; @@ -48,13 +49,16 @@ class Blockonomics implements MethodInterface public function getBTCAddress(): string { - $api_key = $this->blockonomics->api_key; + $api_key = $this->blockonomics->company_gateway->getConfigField('apiKey'); + // $params = config('ninja.environment') == 'development' ? '?reset=1' : ''; $url = 'https://www.blockonomics.co/api/new_address'; $r = Http::withToken($api_key) ->post($url, []); + nlog($r->body()); + if($r->successful()) return $r->object()->address ?? 'Something went wrong'; @@ -71,8 +75,9 @@ class Blockonomics implements MethodInterface } - public function paymentView($data) + public function paymentData(array $data): array { + $btc_price = $this->getBTCPrice(); $btc_address = $this->getBTCAddress(); $fiat_amount = $data['total']['amount_with_fee']; @@ -86,6 +91,19 @@ class Blockonomics implements MethodInterface $data['btc_address'] = $btc_address; $data['btc_price'] = $btc_price; $data['invoice_number'] = $_invoice->invoice_number; + + return $data; + } + + public function livewirePaymentView(array $data): string + { + return 'gateways.blockonomics.pay_livewire'; + } + + public function paymentView($data) + { + $data = $this->paymentData($data); + return render('gateways.blockonomics.pay', $data); } diff --git a/app/PaymentDrivers/BlockonomicsPaymentDriver.php b/app/PaymentDrivers/BlockonomicsPaymentDriver.php index c49256727248..a53ddc4bb612 100644 --- a/app/PaymentDrivers/BlockonomicsPaymentDriver.php +++ b/app/PaymentDrivers/BlockonomicsPaymentDriver.php @@ -43,19 +43,14 @@ class BlockonomicsPaymentDriver extends BaseDriver GatewayType::CRYPTO => Blockonomics::class, //maps GatewayType => Implementation class ]; - public const SYSTEM_LOG_TYPE = SystemLog::TYPE_CHECKOUT; //define a constant for your gateway ie TYPE_YOUR_CUSTOM_GATEWAY - set the const in the SystemLog model + public const SYSTEM_LOG_TYPE = SystemLog::TYPE_BLOCKONOMICS; //define a constant for your gateway ie TYPE_YOUR_CUSTOM_GATEWAY - set the const in the SystemLog model public $BASE_URL = 'https://www.blockonomics.co'; public $NEW_ADDRESS_URL = 'https://www.blockonomics.co/api/new_address'; public $PRICE_URL = 'https://www.blockonomics.co/api/price'; - public $api_key; - public $callback_secret; - public function init() { - $this->api_key = $this->company_gateway->getConfigField('apiKey'); - $this->callback_secret = $this->company_gateway->getConfigField('callbackSecret'); return $this; /* This is where you boot the gateway with your auth credentials*/ } diff --git a/public/build/assets/blockonomics-b9e5b3c6.js b/public/build/assets/blockonomics-b9e5b3c6.js new file mode 100644 index 000000000000..d8d528a973f8 --- /dev/null +++ b/public/build/assets/blockonomics-b9e5b3c6.js @@ -0,0 +1,14 @@ +import{i as h,w as y}from"./wait-8f4ae121.js";/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */class p{constructor(){this.copyToClipboard=this.copyToClipboard.bind(this),this.refreshBTCPrice=this.refreshBTCPrice.bind(this)}copyToClipboard(r,c,a){const e=a?c.nextElementSibling:c,n=e.src,o=document.createElement("input"),s=document.getElementById(r),{value:t,innerText:i}=s||{},d=t||i;o.value=d,document.body.appendChild(o),o.select(),document.execCommand("copy"),document.body.removeChild(o),e.src="data:image/svg+xml;base64,"+btoa(` + + + + + `),setTimeout(()=>{e.src=n},5e3)}async refreshBTCPrice(){const r=document.querySelector(".icon-refresh");r.classList.add("rotating"),document.getElementsByClassName("btc-value")[0].innerHTML="Refreshing...";try{const c=await getBTCPrice();if(c){const a=document.querySelector('meta[name="currency"]').content;document.getElementsByClassName("btc-value")[0].innerHTML="1 BTC = "+(c||"N/A")+" "+a+", updates in ";const e=(document.querySelector('meta[name="amount"]').content/c).toFixed(10);document.querySelector('input[name="btc_price"]').value=c,document.querySelector('input[name="btc_amount"]').value=e,document.getElementById("btc-amount").textContent=e;const n=document.querySelector('meta[name="btc_address"]').content,o=document.getElementById("qr-code-link"),s=document.getElementById("open-in-wallet-link");o.href=`bitcoin:${n}?amount=${e}`,s.href=`bitcoin:${n}?amount=${e}`,fetchAndDisplayQRCode(e),startTimer(600)}}finally{r.classList.remove("rotating")}}handle(){window.copyToClipboard=this.copyToClipboard,window.refreshBTCPrice=this.refreshBTCPrice;const r=e=>{const n=new Date().getTime()+e*1e3;document.getElementById("countdown").innerHTML="10:00 min";const o=()=>{const s=new Date().getTime(),t=n-s;if(document.getElementsByClassName("btc-value")[0].innerHTML.includes("Refreshing"))return;if(t<0){refreshBTCPrice();return}const d=Math.floor(t%(1e3*60*60)/(1e3*60)),l=Math.floor(t%(1e3*60)/1e3),u=String(d).padStart(2,"0"),w=String(l).padStart(2,"0");document.getElementById("countdown").innerHTML=u+":"+w+" min"};clearInterval(window.countdownInterval),window.countdownInterval=setInterval(o,1e3)},c=()=>{const n=`wss://www.blockonomics.co/payment/${document.querySelector('meta[name="btc_address"]').content}`,o=new WebSocket(n);o.onmessage=function(s){const t=JSON.parse(s.data);console.log("Payment status:",t.status);const i=t.status===0,d=t.status===1;(i||d)&&(document.querySelector('input[name="txid"]').value=t.txid||"",document.getElementById("server-response").submit())}},a=async(e=null)=>{try{const n=document.querySelector('meta[name="btc_address"]').content,s=encodeURIComponent(`bitcoin:${n}?amount=${e||"{{$btc_amount}}"}`),t=await fetch(`/api/v1/get-blockonomics-qr-code?qr_string=${s}`);if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);const i=await t.text();document.getElementById("qrcode-container").innerHTML=i}catch(n){console.error("Error fetching QR code:",n),document.getElementById("qrcode-container").textContent="Error loading QR code"}};r(600),c(),a()}}function m(){new p().handle(),window.bootBlockonomics=m}h()?m():y("#blockonomics-payment").then(()=>m()); diff --git a/public/build/manifest.json b/public/build/manifest.json index ac6f6be45f7f..e314eb6bc8d2 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -85,6 +85,14 @@ "isEntry": true, "src": "resources/js/clients/payments/authorize-credit-card-payment.js" }, + "resources/js/clients/payments/blockonomics.js": { + "file": "assets/blockonomics-b9e5b3c6.js", + "imports": [ + "_wait-8f4ae121.js" + ], + "isEntry": true, + "src": "resources/js/clients/payments/blockonomics.js" + }, "resources/js/clients/payments/braintree-credit-card.js": { "file": "assets/braintree-credit-card-60bd8878.js", "imports": [ diff --git a/resources/js/clients/payments/blockonomics.js b/resources/js/clients/payments/blockonomics.js new file mode 100644 index 000000000000..49ac4fefce15 --- /dev/null +++ b/resources/js/clients/payments/blockonomics.js @@ -0,0 +1,185 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +import { wait, instant } from '../wait'; + +class Blockonomics { + + constructor() { + // Bind the method to the instance + this.copyToClipboard = this.copyToClipboard.bind(this); + this.refreshBTCPrice = this.refreshBTCPrice.bind(this); + + } + + copyToClipboard(elementId, passedElement, shouldGrabNextElementSibling) { + + const element = shouldGrabNextElementSibling ? passedElement.nextElementSibling : passedElement; + const originalIcon = element.src; // Store the original icon + + const tempInput = document.createElement("input"); + const elementWithId = document.getElementById(elementId); + const { value, innerText } = elementWithId || {}; + const text = value || innerText; + + tempInput.value = text; + document.body.appendChild(tempInput); + tempInput.select(); + document.execCommand("copy"); + document.body.removeChild(tempInput); + + element.src = 'data:image/svg+xml;base64,' + btoa(` + + + + + `); + + // Change the icon back to the original after 5 seconds + setTimeout(() => { + element.src = originalIcon; + }, 5000); + } + + async refreshBTCPrice() { + const refreshIcon = document.querySelector('.icon-refresh'); + refreshIcon.classList.add('rotating'); + document.getElementsByClassName("btc-value")[0].innerHTML = "Refreshing..."; + + try { + const newPrice = await getBTCPrice(); + if (newPrice) { + // Update the text content of the countdown span to the new bitcoin price + const currency = document.querySelector('meta[name="currency"]').content; + document.getElementsByClassName("btc-value")[0].innerHTML = "1 BTC = " + (newPrice || "N/A") + " " + currency + ", updates in "; + const newBtcAmount = (document.querySelector('meta[name="amount"]').content / newPrice).toFixed(10); + + // set the value of the input field and the text content of the span to the new bitcoin amount + document.querySelector('input[name="btc_price"]').value = newPrice; + document.querySelector('input[name="btc_amount"]').value = newBtcAmount; + document.getElementById('btc-amount').textContent = newBtcAmount; + + const btcAddress = document.querySelector('meta[name="btc_address"]').content; + + // set the href attribute of the link to the new bitcoin amount + const qrCodeLink = document.getElementById('qr-code-link'); + const openInWalletLink = document.getElementById('open-in-wallet-link'); + qrCodeLink.href = `bitcoin:${btcAddress}?amount=${newBtcAmount}`; + openInWalletLink.href = `bitcoin:${btcAddress}?amount=${newBtcAmount}`; + + // fetch and display the new QR code + fetchAndDisplayQRCode(newBtcAmount); + startTimer(600); // Restart timer for 10 minutes (600 seconds) + } + } finally { + refreshIcon.classList.remove('rotating'); + } + } + + + handle() { + window.copyToClipboard = this.copyToClipboard; + window.refreshBTCPrice = this.refreshBTCPrice; + + const startTimer = (seconds) => { + const countDownDate = new Date().getTime() + seconds * 1000; + document.getElementById("countdown").innerHTML = "10" + ":" + "00" + " min"; + + const updateCountdown = () => { + const now = new Date().getTime(); + const distance = countDownDate - now; + + const isRefreshing = document.getElementsByClassName("btc-value")[0].innerHTML.includes("Refreshing"); + if (isRefreshing) { + return; + } + + if (distance < 0) { + refreshBTCPrice(); + return; + } + + const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((distance % (1000 * 60)) / 1000); + const formattedMinutes = String(minutes).padStart(2, '0'); + const formattedSeconds = String(seconds).padStart(2, '0'); + document.getElementById("countdown").innerHTML = formattedMinutes + ":" + formattedSeconds + " min"; + } + + clearInterval(window.countdownInterval); + window.countdownInterval = setInterval(updateCountdown, 1000); + } + + const getBTCPrice = async () => { + try { + const currency = document.querySelector('meta[name="currency"]').content; + const response = await fetch(`/api/v1/get-btc-price?currency=${currency}`); // New endpoint to call server-side function + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const data = await response.json(); + return data.price; + } catch (error) { + console.error('There was a problem with the BTC price fetch operation:', error); + // Handle error appropriately + } + } + + const connectToWebsocket = () => { + const btcAddress = document.querySelector('meta[name="btc_address"]').content; + const webSocketUrl = `wss://www.blockonomics.co/payment/${btcAddress}`; + const ws = new WebSocket(webSocketUrl); + + ws.onmessage = function (event) { + const data = JSON.parse(event.data); + console.log('Payment status:', data.status); + const isPaymentUnconfirmed = data.status === 0; + const isPaymentPartiallyConfirmed = data.status === 1; + // TODO: Do we need to handle Payment confirmed status? + // This usually takes too long to happen, so we can just wait for the unconfirmed status? + if (isPaymentUnconfirmed || isPaymentPartiallyConfirmed) { + document.querySelector('input[name="txid"]').value = data.txid || ''; + document.getElementById('server-response').submit(); + } + } + }; + + const fetchAndDisplayQRCode = async (newBtcAmount = null) => { + try { + const btcAddress = document.querySelector('meta[name="btc_address"]').content; + const btcAmount = newBtcAmount || '{{$btc_amount}}'; + const qrString = encodeURIComponent(`bitcoin:${btcAddress}?amount=${btcAmount}`); + const response = await fetch(`/api/v1/get-blockonomics-qr-code?qr_string=${qrString}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const svgText = await response.text(); + document.getElementById('qrcode-container').innerHTML = svgText; + } catch (error) { + console.error('Error fetching QR code:', error); + document.getElementById('qrcode-container').textContent = 'Error loading QR code'; + } + }; + + startTimer(600); // Start timer for 10 minutes (600 seconds) + connectToWebsocket(); + fetchAndDisplayQRCode(); + + + } + +} + +function boot() { + new Blockonomics().handle(); + window.bootBlockonomics = boot; +} + +instant() ? boot() : wait('#blockonomics-payment').then(() => boot()); diff --git a/resources/views/portal/ninja2020/gateways/blockonomics/pay.blade.php b/resources/views/portal/ninja2020/gateways/blockonomics/pay.blade.php index 59e11dd368a1..2d5a1b8172b8 100644 --- a/resources/views/portal/ninja2020/gateways/blockonomics/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/blockonomics/pay.blade.php @@ -1,5 +1,13 @@ @extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_Crypto'), 'card_title' => ctrans('texts.payment_type_Crypto')]) +@section('gateway_head') + + + + + +@endsection + @section('gateway_content')
@@ -54,150 +62,6 @@ - - @endsection +@section('gateway_footer') + @vite('resources/js/clients/payments/blockonomics.js') +@endsection diff --git a/resources/views/portal/ninja2020/gateways/blockonomics/pay_livewire.blade.php b/resources/views/portal/ninja2020/gateways/blockonomics/pay_livewire.blade.php new file mode 100644 index 000000000000..cd1d59f5dadb --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/blockonomics/pay_livewire.blade.php @@ -0,0 +1,186 @@ +
+ + + + + + + +
+
+
+
Invoice #{{$invoice_number}}
+
{{$amount}} {{$currency}}
+
+
+
+
Scan
+ + +
+
+
+ Open in Wallet +
+
+
Copy
+ To pay, send bitcoin to this address: + + + ') }}" class="icon" alt="Copy Icon"> + + Amount of bitcoin (BTC) to send: + +
+ {{$btc_amount}} +
+ ') }}" class="icon" alt="Copy Icon"> +
+
+
1 BTC = {{$btc_price}} {{$currency}}, updates in
+ +
+
+
+
+ +
+ @csrf + + + + + + + + + + +
+ + + + +
+ +@assets + @vite('resources/js/clients/payments/blockonomics.js') +@endassets \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 7ce94baf88cb..8700bd76e249 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -55,6 +55,7 @@ export default defineConfig({ 'resources/js/clients/payment_methods/authorize-stripe-acss.js', 'resources/js/clients/payment_methods/authorize-powerboard-card.js', 'resources/js/clients/payments/powerboard-credit-card.js', + 'resources/js/clients/payments/blockonomics.js', ]), viteStaticCopy({ targets: [