mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-20 10:40:29 +01:00
768d97ac6c
* fix modfiy webhook inline JS issue * fix uncofirmed warning issue in Safari * fix inline JS Safari issues in checkout
390 lines
16 KiB
Plaintext
390 lines
16 KiB
Plaintext
@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>
|
|
var initialSrvModel = @Safe.Json(Model);
|
|
</script>
|
|
|
|
<bundle name="wwwroot/bundles/bootstrap-bundle.min.js" asp-append-version="true" />
|
|
<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(25, 25, 25, 0.9);
|
|
}
|
|
.close-icon {
|
|
display: flex;
|
|
}
|
|
</style>
|
|
}
|
|
</head>
|
|
<body>
|
|
<noscript>
|
|
<div style="padding:2em;text-align:center;">
|
|
<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 asp-action="CheckoutNoScript" asp-route-invoiceId="@Model.InvoiceId" style="text-decoration:underline;color:blue">
|
|
Continue to javascript-disabled invoice >
|
|
</a>
|
|
</div>
|
|
</noscript>
|
|
|
|
<!--[if lte IE 8]>
|
|
<div style="padding:2em;text-align:center;">
|
|
<form asp-action="CheckoutNoScript" method="GET">
|
|
<button style="text-decoration: underline; color: blue">Continue to legacy browser compatible invoice page
|
|
</button>
|
|
</form>
|
|
</div>
|
|
<![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"
|
|
asp-items="@langService.GetLanguages().Select((language) => new SelectListItem(language.DisplayName,language.Code, false))"></select>
|
|
|
|
<script>
|
|
$(function () {
|
|
// REVIEW: don't use initDropdown method but rather directly initialize select whenever you are using it
|
|
$("#DefaultLang").val(startingLanguage);
|
|
var languageSelectorPrettyDropdown = initDropdown("#DefaultLang");
|
|
|
|
languageSelectorPrettyDropdown.change(function() {
|
|
changeLanguage(languageSelectorPrettyDropdown.val());
|
|
});
|
|
|
|
languageSelectorPrettyDropdown.keypress(function(event) {
|
|
if (event.keyCode == 13) {
|
|
languageSelectorPrettyDropdown.click();
|
|
}
|
|
});
|
|
});
|
|
|
|
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" rel="noreferrer noopener">BTCPay Server</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</invoice>
|
|
<script>
|
|
var availableLanguages = @Safe.Json(langService.GetLanguages().Select((language) => language.Code));;
|
|
var defaultLang = @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(defaultLang)) {
|
|
return defaultLang;
|
|
} 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) {
|
|
if (e.data === "ping")
|
|
return;
|
|
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;
|
|
|
|
eventBus.$emit("data-fetched", this.srvModel);
|
|
if (this.invoicePaid && jsonData.redirectAutomatically && jsonData.merchantRefLink) {
|
|
this.loading = true;
|
|
setTimeout(function () {
|
|
if (this.isModal && window.top.location == jsonData.merchantRefLink){
|
|
this.close();
|
|
} else {
|
|
window.top.location = jsonData.merchantRefLink;
|
|
}
|
|
}.bind(this), 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"/>
|
|
}
|
|
@await Component.InvokeAsync("UiExtensionPoint" , new { location="checkout-end", model = Model})
|
|
</body>
|
|
</html>
|