btcpayserver/BTCPayServer/Views/Invoice/Checkout.cshtml
2019-10-27 08:13:30 +01:00

373 lines
15 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" />
<script type="text/javascript">
var initialSrvModel = @Safe.Json(Model);
</script>
<bundle name="wwwroot/bundles/checkout-bundle.min.js" />
<script>vex.defaultOptions.className = 'vex-theme-btcpay'</script>
@if (Model.CustomCSSLink != null)
{
<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 &gt;
</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 style="margin-top: 10px; text-align: center;" class="form-text small text-muted">
<span>Powered by <a target="_blank" href="https://github.com/btcpayserver/btcpayserver">BTCPay Server</a></span>
</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 && !this.invoicePaid;
},
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>