mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 14:40:36 +01:00
Cache static assets for one year and set the correct cache control header. Adds the cache busting version based on file content to asset references to invalidate the cache on change. ([further details on the approach](https://andrewlock.net/adding-cache-control-headers-to-static-files-in-asp-net-core/) and [why one year](https://ashton.codes/set-cache-control-max-age-1-year/)) Most of the changes are the additions of the `asp-append-version="true"` attribute, the main configuration change is in `Startup.cs`.
372 lines
15 KiB
Text
372 lines
15 KiB
Text
@addTagHelper *, BundlerMinifier.TagHelpers
|
|
@inject BTCPayServer.Services.LanguageService langService
|
|
@inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
|
|
@model PaymentModel
|
|
@{
|
|
Layout = null;
|
|
}
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
|
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
<META NAME="robots" CONTENT="noindex,nofollow">
|
|
<title>@Model.HtmlTitle</title>
|
|
|
|
<bundle name="wwwroot/bundles/checkout-bundle.min.css" asp-append-version="true" />
|
|
|
|
<script type="text/javascript">
|
|
var initialSrvModel = @Safe.Json(Model);
|
|
</script>
|
|
|
|
<bundle name="wwwroot/bundles/checkout-bundle.min.js" asp-append-version="true" />
|
|
<script>vex.defaultOptions.className = 'vex-theme-btcpay'</script>
|
|
|
|
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
|
|
{
|
|
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
|
}
|
|
|
|
@if (Model.IsModal)
|
|
{
|
|
<style type="text/css">
|
|
body {
|
|
background: rgba(55, 58, 60, 0.4);
|
|
}
|
|
|
|
.close-icon {
|
|
display: flex;
|
|
}
|
|
</style>
|
|
}
|
|
</head>
|
|
<body>
|
|
<noscript>
|
|
<center style="padding: 2em">
|
|
<h2>Javascript is currently disabled in your browser.</h2>
|
|
<h5>Please enable Javascript and refresh this page for the best experience.</h5>
|
|
|
|
<p>Alternatively, click below to continue to our HTML-only invoice.</p>
|
|
|
|
<a href="/invoice-noscript?id=@Model.InvoiceId" style="text-decoration: underline; color: blue">
|
|
Continue to javascript-disabled invoice >
|
|
</a>
|
|
</center>
|
|
</noscript>
|
|
|
|
<!--[if lte IE 8]>
|
|
<center style="padding: 2em">
|
|
<form action="/invoice-noscript" method="GET">
|
|
<button style="text-decoration: underline; color: blue">Continue to legacy browser compatible invoice page
|
|
</button>
|
|
</form>
|
|
</center>
|
|
<![endif]-->
|
|
|
|
<invoice>
|
|
<div class="no-bounce" id="checkoutCtrl" v-cloak>
|
|
<div class="modal page">
|
|
<div class="modal-dialog open opened" role="document" v-bind:class="{ 'expired': invoiceUnpayable, 'paid': invoicePaid, 'enter-purchaser-email': showEmailForm}">
|
|
<div class="modal-content long">
|
|
<div class="content">
|
|
<div class="invoice">
|
|
<partial name="Checkout-Body" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 10px; text-align: center;">
|
|
@* Not working because of nsSeparator: false, keySeparator: false,
|
|
{{$t("nested.lang")}} >>
|
|
*@
|
|
|
|
<select asp-for="DefaultLang"
|
|
class="cmblang reverse invisible"
|
|
onkeypress="if(event.keyCode==13){ $(this).click();}"
|
|
onchange="changeLanguage($(this).val())"
|
|
asp-items="@langService.GetLanguages().Select((language) => new SelectListItem(language.DisplayName,language.Code, false))"></select>
|
|
|
|
<script>
|
|
var languageSelectorPrettyDropdown;
|
|
$(function () {
|
|
// REVIEW: don't use initDropdown method but rather directly initialize select whenever you are using it
|
|
$("#DefaultLang").val(startingLanguage);
|
|
languageSelectorPrettyDropdown = initDropdown("#DefaultLang");
|
|
});
|
|
|
|
function initDropdown(selector) {
|
|
return $(selector).prettyDropdown({
|
|
classic: false,
|
|
height: 32,
|
|
reverse: true,
|
|
hoverIntent: 5000
|
|
});
|
|
}
|
|
</script>
|
|
</div>
|
|
<div class="powered__by__btcpayserver">
|
|
Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver">BTCPay Server</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</invoice>
|
|
<script type="text/javascript">
|
|
var availableLanguages = @Safe.Json(langService.GetLanguages().Select((language) => language.Code));;
|
|
var storeDefaultLang = @Safe.Json(@Model.DefaultLang);
|
|
var fallbackLanguage = "en";
|
|
startingLanguage = computeStartingLanguage();
|
|
i18next
|
|
.use(window.i18nextXHRBackend)
|
|
.init({
|
|
backend: {
|
|
loadPath: @Safe.Json($"{Model.RootPath}locales/{{{{lng}}}}.json")
|
|
},
|
|
lng: startingLanguage,
|
|
fallbackLng: fallbackLanguage,
|
|
nsSeparator: false,
|
|
keySeparator: false
|
|
});
|
|
|
|
function computeStartingLanguage() {
|
|
if (urlParams.lang && isLanguageAvailable(urlParams.lang)) {
|
|
return urlParams.lang;
|
|
}
|
|
else if (isLanguageAvailable(storeDefaultLang)) {
|
|
return storeDefaultLang;
|
|
} else {
|
|
return fallbackLanguage;
|
|
}
|
|
}
|
|
|
|
function changeLanguage(lang) {
|
|
if (isLanguageAvailable(lang)) {
|
|
i18next.changeLanguage(lang);
|
|
}
|
|
}
|
|
|
|
function isLanguageAvailable(languageCode) {
|
|
return availableLanguages.indexOf(languageCode) >= 0;
|
|
}
|
|
|
|
var i18n = new VueI18next(i18next);
|
|
Vue.config.ignoredElements = [
|
|
'line-items',
|
|
'low-fee-timeline',
|
|
// Ignoring custom HTML5 elements, eg: bp-spinner
|
|
/^bp-/
|
|
];
|
|
var eventBus = new Vue();
|
|
var checkoutCtrl = new Vue({
|
|
i18n: i18n,
|
|
el: '#checkoutCtrl',
|
|
data: {
|
|
srvModel: initialSrvModel,
|
|
end: new Date(),
|
|
expirationPercentage: 0,
|
|
timerText: "@Model.TimeLeft",
|
|
emailAddressInput: "",
|
|
emailAddressInputDirty: false,
|
|
emailAddressInputInvalid: false,
|
|
emailAddressFormSubmitting: false,
|
|
lineItemsExpanded: false,
|
|
changingCurrencies: false,
|
|
loading: true,
|
|
isModal: initialSrvModel.isModal
|
|
},
|
|
computed: {
|
|
expiringSoon: function(){
|
|
return this.expirationPercentage >= 75 && !this.invoiceUnpayable && !this.invoicePaid;
|
|
},
|
|
showPaymentUI: function(){
|
|
return !this.showEmailForm && !this.invoiceUnpayable && !this.invoicePaid;
|
|
},
|
|
showEmailForm: function(){
|
|
return this.srvModel.requiresRefundEmail && (!this.srvModel.customerEmail || !this.validateEmail(this.srvModel.customerEmail)) && !this.invoiceUnpayable;
|
|
},
|
|
showRecommendedFee: function(){
|
|
return this.srvModel.showRecommendedFee && this.srvModel.feeRate != 0;
|
|
},
|
|
invoiceUnpayable: function(){
|
|
return ["expired", "invalid"].indexOf(this.srvModel.status) >= 0;
|
|
},
|
|
invoicePaid: function(){
|
|
return ["complete", "confirmed", "paid"].indexOf(this.srvModel.status) >= 0;
|
|
}
|
|
},
|
|
mounted: function(){
|
|
this.startProgressTimer();
|
|
this.listenIn();
|
|
this.onDataCallback(this.srvModel);
|
|
if (this.srvModel.status === "new" && this.srvModel.txCount > 1) {
|
|
this.onlyExpandLineItems();
|
|
}
|
|
window.parent.postMessage("loaded", "*");
|
|
jQuery("invoice").fadeOut(0).fadeIn(300);
|
|
window.closePaymentMethodDialog = this.closePaymentMethodDialog.bind(this);
|
|
this.loading = false;
|
|
},
|
|
methods: {
|
|
onlyExpandLineItems: function() {
|
|
if (!this.lineItemsExpanded) {
|
|
this.toggleLineItems();
|
|
}},
|
|
toggleLineItems: function() {
|
|
this.lineItemsExpanded ? $("line-items").slideUp() : $("line-items").slideDown();
|
|
this.lineItemsExpanded = !this.lineItemsExpanded;
|
|
},
|
|
openPaymentMethodDialog: function() {
|
|
var content = $("#vexPopupDialog").html();
|
|
vex.open({
|
|
unsafeContent: content
|
|
});
|
|
},
|
|
closePaymentMethodDialog: function(currencyId) {
|
|
vex.closeAll();
|
|
this.changeCurrency(currencyId);
|
|
},
|
|
changeCurrency: function (currency) {
|
|
if (currency !== null && this.srvModel.paymentMethodId !== currency) {
|
|
this.changingCurrencies = true;
|
|
this.srvModel.paymentMethodId = currency;
|
|
this.fetchData();
|
|
this.closePaymentMethodDialog(null);
|
|
}
|
|
},
|
|
close: function(){
|
|
$("invoice").fadeOut(300, function () {
|
|
window.parent.postMessage("close", "*");
|
|
});
|
|
},
|
|
validateEmail: function (email) {
|
|
var re = /^(([^<>()\[\]\\.,;:\s@@"]+(\.[^<>()\[\]\\.,;:\s@@"]+)*)|(".+"))@@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
return re.test(email);
|
|
},
|
|
startProgressTimer: function(){
|
|
var timeLeftS = this.endDate? (this.endDate.getTime() - new Date().getTime())/1000 : this.srvModel.expirationSeconds;
|
|
this.expirationPercentage = 100 - ((timeLeftS / this.srvModel.maxTimeSeconds) * 100);
|
|
this.timerText = this.updateTimerText(timeLeftS);
|
|
if( this.expirationPercentage < 100 && (this.srvModel.status === "paidPartial" || this.srvModel.status === "new")){
|
|
setTimeout(this.startProgressTimer, 500);
|
|
}
|
|
},
|
|
updateTimerText: function (timer) {
|
|
if (timer >= 0) {
|
|
var minutes = parseInt(timer / 60, 10);
|
|
minutes = minutes < 10 ? "0" + minutes : minutes;
|
|
var seconds = parseInt(timer % 60, 10);
|
|
seconds = seconds < 10 ? "0" + seconds : seconds;
|
|
return minutes + ":" + seconds;
|
|
} else {
|
|
return "00:00";
|
|
}
|
|
},
|
|
listenIn: function(){
|
|
var self = this;
|
|
var socket = null;
|
|
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
|
|
if (supportsWebSockets) {
|
|
var loc = window.location, ws_uri;
|
|
if (loc.protocol === "https:") {
|
|
ws_uri = "wss:";
|
|
} else {
|
|
ws_uri = "ws:";
|
|
}
|
|
ws_uri += "//" + loc.host;
|
|
ws_uri += loc.pathname + "/status/ws?invoiceId=" + this.srvModel.invoiceId;
|
|
try {
|
|
socket = new WebSocket(ws_uri);
|
|
socket.onmessage = function (e) {
|
|
self.fetchData();
|
|
};
|
|
socket.onerror = function (e) {
|
|
console.error("Error while connecting to websocket for invoice notifications (callback)");
|
|
};
|
|
}
|
|
catch (e) {
|
|
console.error("Error while connecting to websocket for invoice notifications");
|
|
}
|
|
}
|
|
var self = this;
|
|
function watcher(){
|
|
setTimeout(function(){
|
|
if (socket === null || socket.readyState !== 1) {
|
|
self.fetchData();
|
|
}
|
|
watcher();
|
|
}, 2000);
|
|
}
|
|
watcher();
|
|
},
|
|
fetchData: function(){
|
|
var self = this;
|
|
$.ajax({
|
|
url: window.location.pathname + "/status?invoiceId=" + this.srvModel.invoiceId + "&paymentMethodId=" + this.srvModel.paymentMethodId,
|
|
type: "GET",
|
|
cache: false
|
|
})
|
|
.done(function (data) {
|
|
self.onDataCallback.bind(self)(data);
|
|
})
|
|
},
|
|
onDataCallback : function(jsonData){
|
|
if (this.srvModel.status !== jsonData.status) {
|
|
window.parent.postMessage({ "invoiceId": this.srvModel.invoiceId, "status": jsonData.status }, "*");
|
|
}
|
|
if (jsonData.paymentMethodId === this.srvModel.paymentMethodId) {
|
|
this.changingCurrencies = false;
|
|
}
|
|
// displaying satoshis for lightning payments
|
|
jsonData.cryptoCodeSrv = jsonData.cryptoCode;
|
|
// expand line items to show details on amount due for multi-transaction payment
|
|
if (this.srvModel.txCount === 1 && jsonData.txCount > 1) {
|
|
this.onlyExpandLineItems();
|
|
}
|
|
var newEnd = new Date();
|
|
newEnd.setSeconds(newEnd.getSeconds()+ jsonData.expirationSeconds);
|
|
this.endDate = newEnd;
|
|
// updating ui
|
|
this.srvModel = jsonData;
|
|
if (this.invoicePaid && !jsonData.isModal && jsonData.redirectAutomatically && jsonData.merchantRefLink) {
|
|
this.loading = true;
|
|
setTimeout(function () {
|
|
window.location = jsonData.merchantRefLink;
|
|
}, 2000);
|
|
}
|
|
},
|
|
onEmailChange: function(){
|
|
this.emailAddressInputDirty = true;
|
|
this.emailAddressInputInvalid = false;
|
|
},
|
|
onEmailSubmit : function(){
|
|
var self = this;
|
|
if (this.validateEmail(this.emailAddressInput)) {
|
|
this.emailAddressFormSubmitting = true;
|
|
// Push the email to a server, once the reception is confirmed move on
|
|
$.ajax({
|
|
url: window.location.pathname + "/UpdateCustomer?invoiceId=" +this.srvModel.invoiceId,
|
|
type: "POST",
|
|
data: JSON.stringify({ Email: this.emailAddressInput }),
|
|
contentType: "application/json; charset=utf-8"
|
|
})
|
|
.done(function () {
|
|
self.srvModel.customerEmail = self.emailAddressInput;
|
|
}).always(function () {
|
|
self.emailAddressFormSubmitting = false;
|
|
});
|
|
} else {
|
|
this.emailAddressInputInvalid = true;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
@foreach (var paymentMethodHandler in PaymentMethodHandlerDictionary.Select(handler => handler.GetCheckoutUISettings()).Where(settings => settings != null))
|
|
{
|
|
<partial name="@paymentMethodHandler.ExtensionPartial" model="@Model"/>
|
|
}
|
|
</body>
|
|
</html>
|