mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-31 13:54:34 -04:00
update UI and make refreshing price possible
This commit is contained in:
parent
9044f3239c
commit
34830bc9ab
32
app/Http/Controllers/Gateways/BlockonomicsController.php
Normal file
32
app/Http/Controllers/Gateways/BlockonomicsController.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://www.elastic.co/licensing/elastic-license
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Gateways;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request; // Import the Request class
|
||||||
|
use Illuminate\Support\Facades\Http; // Import the Http facade
|
||||||
|
|
||||||
|
class BlockonomicsController extends Controller
|
||||||
|
{
|
||||||
|
public function getBTCPrice(Request $request)
|
||||||
|
{
|
||||||
|
$currency = $request->query('currency');
|
||||||
|
$response = Http::get("https://www.blockonomics.co/api/price?currency={$currency}");
|
||||||
|
|
||||||
|
if ($response->successful()) {
|
||||||
|
return response()->json(['price' => $response->json('price')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json(['error' => 'Unable to fetch BTC price'], 500);
|
||||||
|
}
|
||||||
|
}
|
@ -111,10 +111,12 @@ class Blockonomics implements MethodInterface
|
|||||||
$data['gateway'] = $this->blockonomics;
|
$data['gateway'] = $this->blockonomics;
|
||||||
$data['amount'] = $data['total']['amount_with_fee'];
|
$data['amount'] = $data['total']['amount_with_fee'];
|
||||||
$data['currency'] = $this->blockonomics->client->getCurrencyCode();
|
$data['currency'] = $this->blockonomics->client->getCurrencyCode();
|
||||||
$btc_amount = $data['amount'] / $this->getBTCPrice();
|
$btc_price = $this->getBTCPrice();
|
||||||
|
$btc_amount = $data['amount'] / $btc_price;
|
||||||
$data['btc_amount'] = number_format($btc_amount, 10, '.', '');
|
$data['btc_amount'] = number_format($btc_amount, 10, '.', '');
|
||||||
$btc_address = $this->getBTCAddress();
|
$btc_address = $this->getBTCAddress();
|
||||||
$data['btc_address'] = $btc_address;
|
$data['btc_address'] = $btc_address;
|
||||||
|
$data['btc_price'] = $btc_price;
|
||||||
$data['invoice_id'] = $_invoice->invoice_id;
|
$data['invoice_id'] = $_invoice->invoice_id;
|
||||||
$data['invoice_number'] = $_invoice->invoice_number;
|
$data['invoice_number'] = $_invoice->invoice_number;
|
||||||
$data['end_time'] = $this->getTenMinutesCountDownEndTime();
|
$data['end_time'] = $this->getTenMinutesCountDownEndTime();
|
||||||
|
@ -10,21 +10,24 @@
|
|||||||
The <a id="link" href="{{ $invoice_redirect_url }}" target="_blank">invoice</a> will be marked as paid automatically once the payment is confirmed.
|
The <a id="link" href="{{ $invoice_redirect_url }}" target="_blank">invoice</a> will be marked as paid automatically once the payment is confirmed.
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="initial-state">
|
<div class="initial-state">
|
||||||
<div class="invoice-number">Invoice #{{$invoice_number}}</div>
|
<div class="invoice-info-wrapper">
|
||||||
<div>To pay, send bitcoin to this address:</div>
|
<div class="invoice-number">Invoice #{{$invoice_number}}</div>
|
||||||
|
<div class="invoice-amount">{{$amount}} {{$currency}}</div>
|
||||||
|
</div>
|
||||||
|
<b>To pay, send bitcoin to this address:</b>
|
||||||
<span class="input-wrapper">
|
<span class="input-wrapper">
|
||||||
<input class="full-width-input" id="btcAddress" value="{{$btc_address}}" readonly>
|
<input onclick='copyToClipboard("{{$btc_address}}", this, true)' class="full-width-input" id="btcAddress" value="{{$btc_address}}" readonly>
|
||||||
<img onclick='copyToClipboard("{{$btc_address}}", this)' src="{{ 'data:image/svg+xml;base64,' . base64_encode('<svg width="22" height="24" viewBox="0 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg" ><path d="M15.5 1H3.5C2.4 1 1.5 1.9 1.5 3V17H3.5V3H15.5V1ZM18.5 5H7.5C6.4 5 5.5 5.9 5.5 7V21C5.5 22.1 6.4 23 7.5 23H18.5C19.6 23 20.5 22.1 20.5 21V7C20.5 5.9 19.6 5 18.5 5ZM18.5 21H7.5V7H18.5V21Z" fill="#000"/></svg>') }}" class="icon" alt="Copy Icon">
|
<img onclick='copyToClipboard("{{$btc_address}}", this)' style="margin-right: 32px;" src="{{ 'data:image/svg+xml;base64,' . base64_encode('<svg width="22" height="24" viewBox="0 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg" ><path d="M15.5 1H3.5C2.4 1 1.5 1.9 1.5 3V17H3.5V3H15.5V1ZM18.5 5H7.5C6.4 5 5.5 5.9 5.5 7V21C5.5 22.1 6.4 23 7.5 23H18.5C19.6 23 20.5 22.1 20.5 21V7C20.5 5.9 19.6 5 18.5 5ZM18.5 21H7.5V7H18.5V21Z" fill="#000"/></svg>') }}" class="icon" alt="Copy Icon">
|
||||||
</span>
|
</span>
|
||||||
<div>Amount of bitcoin (BTC) to send:</div>
|
<b>Amount of bitcoin (BTC) to send:</b>
|
||||||
<span class="input-wrapper">
|
<span class="input-wrapper">
|
||||||
<div class="full-width-input">
|
<div class="full-width-input" id="btc-amount" onclick='copyToClipboard("{{$btc_amount}}", this, true)'>
|
||||||
{{$btc_amount}}
|
{{$btc_amount}}
|
||||||
</div>
|
</div>
|
||||||
<img onclick='copyToClipboard("{{$btc_amount}}", this)' src="{{ 'data:image/svg+xml;base64,' . base64_encode('<svg width="22" height="24" viewBox="0 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg" ><path d="M15.5 1H3.5C2.4 1 1.5 1.9 1.5 3V17H3.5V3H15.5V1ZM18.5 5H7.5C6.4 5 5.5 5.9 5.5 7V21C5.5 22.1 6.4 23 7.5 23H18.5C19.6 23 20.5 22.1 20.5 21V7C20.5 5.9 19.6 5 18.5 5ZM18.5 21H7.5V7H18.5V21Z" fill="#000"/></svg>') }}" class="icon" alt="Copy Icon">
|
<img onclick='copyToClipboard("{{$btc_amount}}", this)' src="{{ 'data:image/svg+xml;base64,' . base64_encode('<svg width="22" height="24" viewBox="0 0 22 24" fill="none" xmlns="http://www.w3.org/2000/svg" ><path d="M15.5 1H3.5C2.4 1 1.5 1.9 1.5 3V17H3.5V3H15.5V1ZM18.5 5H7.5C6.4 5 5.5 5.9 5.5 7V21C5.5 22.1 6.4 23 7.5 23H18.5C19.6 23 20.5 22.1 20.5 21V7C20.5 5.9 19.6 5 18.5 5ZM18.5 21H7.5V7H18.5V21Z" fill="#000"/></svg>') }}" class="icon" alt="Copy Icon">
|
||||||
<span class="icon-refresh"></span>
|
<span class="icon-refresh" onclick='refreshBTCPrice()'></span>
|
||||||
</span>
|
</span>
|
||||||
<span>1 BTC = {{$amount}} {{$currency}}, updates in <span id="countdown"></span></span>
|
<div class="btc-value">1 BTC = {{$btc_price}} {{$currency}}, updates in <span id="countdown"></span></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -41,19 +44,22 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Get the end time as a Unix timestamp (seconds)
|
// Define the startTimer function
|
||||||
const endTimeUnix = {{ $end_time }};
|
function startTimer(seconds) {
|
||||||
console.log("End time (Unix timestamp):", endTimeUnix); // For debugging
|
const countDownDate = new Date().getTime() + seconds * 1000;
|
||||||
|
document.getElementById("countdown").innerHTML = "10" + ":" + "00" + " min";
|
||||||
// Convert Unix timestamp to milliseconds for JavaScript Date
|
|
||||||
const countDownDate = endTimeUnix * 1000;
|
|
||||||
|
|
||||||
function updateCountdown() {
|
function updateCountdown() {
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
const distance = countDownDate - now;
|
const distance = countDownDate - now;
|
||||||
|
|
||||||
|
const isRefreshing = document.getElementsByClassName("btc-value")[0].innerHTML.includes("Refreshing");
|
||||||
|
if (isRefreshing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (distance < 0) {
|
if (distance < 0) {
|
||||||
document.getElementById("countdown").innerHTML = "EXPIRED";
|
refreshBTCPrice();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,10 +70,18 @@
|
|||||||
|
|
||||||
document.getElementById("countdown").innerHTML = formattedMinutes + ":" + formattedSeconds + " min";
|
document.getElementById("countdown").innerHTML = formattedMinutes + ":" + formattedSeconds + " min";
|
||||||
}
|
}
|
||||||
setInterval(updateCountdown, 1000);
|
|
||||||
|
// Clear any existing intervals to avoid multiple intervals running simultaneously
|
||||||
|
clearInterval(window.countdownInterval);
|
||||||
|
window.countdownInterval = setInterval(updateCountdown, 1000);
|
||||||
|
}
|
||||||
|
// Call startTimer initially with the desired countdown time
|
||||||
|
startTimer(600); // Start the timer for 10 minutes (600 seconds)
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
function copyToClipboard(text, element) {
|
function copyToClipboard(text, passedElement, shouldGrabNextElementSibling) {
|
||||||
|
const element = shouldGrabNextElementSibling ? passedElement.nextElementSibling : passedElement;
|
||||||
|
const originalIcon = element.src; // Store the original icon
|
||||||
const tempInput = document.createElement("input");
|
const tempInput = document.createElement("input");
|
||||||
tempInput.value = text;
|
tempInput.value = text;
|
||||||
document.body.appendChild(tempInput);
|
document.body.appendChild(tempInput);
|
||||||
@ -75,10 +89,7 @@
|
|||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
document.body.removeChild(tempInput);
|
document.body.removeChild(tempInput);
|
||||||
|
|
||||||
// Change the icon to the check icon
|
element.src = 'data:image/svg+xml;base64,' + btoa(`
|
||||||
const iconElement = element.querySelector('.icon');
|
|
||||||
const originalIcon = iconElement.src;
|
|
||||||
iconElement.src = 'data:image/svg+xml;base64,' + btoa(`
|
|
||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M4.04706 14C4.04706 8.55609 8.46025 4.1429 13.9042 4.1429C19.3482 4.1429 23.7613 8.55609 23.7613 14C23.7613 19.444 19.3482 23.8572 13.9042 23.8572C8.46025 23.8572 4.04706 19.444 4.04706 14Z" stroke="#000" stroke-width="2.19048" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M4.04706 14C4.04706 8.55609 8.46025 4.1429 13.9042 4.1429C19.3482 4.1429 23.7613 8.55609 23.7613 14C23.7613 19.444 19.3482 23.8572 13.9042 23.8572C8.46025 23.8572 4.04706 19.444 4.04706 14Z" stroke="#000" stroke-width="2.19048" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M9.52325 14L12.809 17.2858L18.2852 11.8096" stroke="#000" stroke-width="2.19048" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M9.52325 14L12.809 17.2858L18.2852 11.8096" stroke="#000" stroke-width="2.19048" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
@ -87,7 +98,7 @@
|
|||||||
|
|
||||||
// Change the icon back to the original after 5 seconds
|
// Change the icon back to the original after 5 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
iconElement.src = originalIcon;
|
element.src = originalIcon;
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
// Optionally, you can show a message to the user
|
// Optionally, you can show a message to the user
|
||||||
@ -95,6 +106,40 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
async function getBTCPrice() {
|
||||||
|
try {
|
||||||
|
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 fetch operation:', error);
|
||||||
|
// Handle error appropriately
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function 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) {
|
||||||
|
document.getElementsByClassName("btc-value")[0].innerHTML = "1 BTC = " + (newPrice || "N/A") + " {{$currency}}, updates in <span id='countdown'></span>";
|
||||||
|
const newBtcAmount = ({{$amount}} / newPrice).toFixed(10);
|
||||||
|
document.getElementById('btc-amount').textContent = newBtcAmount;
|
||||||
|
startTimer(600); // Restart timer for 10 minutes (600 seconds)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
refreshIcon.classList.remove('rotating');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var webSocketUrl = "{{ $websocket_url }}";
|
var webSocketUrl = "{{ $websocket_url }}";
|
||||||
const ws = new WebSocket(webSocketUrl);
|
const ws = new WebSocket(webSocketUrl);
|
||||||
@ -127,23 +172,39 @@
|
|||||||
|
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
.invoice-number {
|
.invoice-info-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.invoice-number {
|
||||||
|
width: 50%;
|
||||||
|
float: left;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.invoice-amount {
|
||||||
|
width: 50%;
|
||||||
float: right;
|
float: right;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.blockonomics-payment-wrapper {
|
.blockonomics-payment-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.initial-state {
|
.initial-state {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 12px;
|
width: 100%;
|
||||||
|
padding: 24px;
|
||||||
}
|
}
|
||||||
.input-wrapper {
|
.input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -153,22 +214,43 @@
|
|||||||
}
|
}
|
||||||
.full-width-input {
|
.full-width-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-weight: bold;
|
color: #444;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.icon {
|
.icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 28px;
|
||||||
}
|
}
|
||||||
.icon-refresh::before {
|
.icon-refresh::before {
|
||||||
content: '\27F3';
|
content: '\27F3';
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
width: 28px;
|
||||||
|
display: flex;
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.btc-value {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
@keyframes rotating {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.rotating {
|
||||||
|
animation: rotating 2s linear infinite;
|
||||||
}
|
}
|
||||||
/* .progress-message {
|
/* .progress-message {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
use App\Http\Controllers\SubscriptionStepsController;
|
use App\Http\Controllers\SubscriptionStepsController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Http\Controllers\Gateways\BlockonomicsController;
|
||||||
use App\Http\Controllers\BaseController;
|
use App\Http\Controllers\BaseController;
|
||||||
use App\Http\Controllers\BrevoController;
|
use App\Http\Controllers\BrevoController;
|
||||||
use App\Http\Controllers\PingController;
|
use App\Http\Controllers\PingController;
|
||||||
@ -460,5 +461,6 @@ Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook']
|
|||||||
Route::get('api/v1/protected_download/{hash}', [ProtectedDownloadController::class, 'index'])->name('protected_download')->middleware('throttle:300,1');
|
Route::get('api/v1/protected_download/{hash}', [ProtectedDownloadController::class, 'index'])->name('protected_download')->middleware('throttle:300,1');
|
||||||
Route::post('api/v1/ppcp/webhook', [PayPalPPCPPaymentDriver::class, 'processWebhookRequest'])->middleware('throttle:1000,1');
|
Route::post('api/v1/ppcp/webhook', [PayPalPPCPPaymentDriver::class, 'processWebhookRequest'])->middleware('throttle:1000,1');
|
||||||
Route::get('api/v1/blockonomics/callback', [BlockonomicsPaymentDriver::class, 'processWebhookRequest'])->middleware('throttle:1000,1');
|
Route::get('api/v1/blockonomics/callback', [BlockonomicsPaymentDriver::class, 'processWebhookRequest'])->middleware('throttle:1000,1');
|
||||||
|
Route::get('api/v1/get-btc-price', [BlockonomicsController::class, 'getBTCPrice'])->middleware('throttle:1000,1');
|
||||||
|
|
||||||
Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404');
|
Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404');
|
||||||
|
@ -5631,5 +5631,14 @@
|
|||||||
"method": [
|
"method": [
|
||||||
"GET"
|
"GET"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"get-btc-price": {
|
||||||
|
"name": "get-btc-price",
|
||||||
|
"domain": null,
|
||||||
|
"action": "App\\Http\\Controllers\\BlockonomicsController@getBTCPrice",
|
||||||
|
"uri": "api/v1/get-btc-price",
|
||||||
|
"method": [
|
||||||
|
"GET"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user