mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-24 14:50:50 +01:00
* Checkout v2: Configure countdown timer This addresses feedback by @astupidmoose left [here](https://github.com/btcpayserver/btcpayserver/discussions/4308#discussioncomment-4438926): Make the countdown timer configurable with a minutes setting. This way the merchant has full control over when to display the timer. They could even set it to equal the invoice expiry, so that it is shown right from the beginning. * Rename property and adjust wording * Remove expiration percentage from Checkout v2
283 lines
10 KiB
JavaScript
283 lines
10 KiB
JavaScript
Vue.directive('collapsible', {
|
|
bind: function (el, binding) {
|
|
el.classList.add('collapse');
|
|
el.classList[binding.value ? 'add' : 'remove']('show');
|
|
el.transitionDuration = 350;
|
|
},
|
|
update: function (el, binding) {
|
|
if (binding.oldValue !== binding.value){
|
|
if (binding.value) {
|
|
setTimeout(function () {
|
|
el.classList.remove('collapse');
|
|
const height = window.getComputedStyle(el).height;
|
|
el.classList.add('collapsing');
|
|
el.offsetHeight;
|
|
el.style.height = height;
|
|
setTimeout(function () {
|
|
el.classList.remove('collapsing');
|
|
el.classList.add('collapse');
|
|
el.style.height = null;
|
|
el.classList.add('show');
|
|
}, el.transitionDuration)
|
|
}, 0);
|
|
} else {
|
|
el.style.height = window.getComputedStyle(el).height;
|
|
el.classList.remove('collapse');
|
|
el.classList.remove('show');
|
|
el.offsetHeight;
|
|
el.style.height = null;
|
|
el.classList.add('collapsing');
|
|
setTimeout(function () {
|
|
el.classList.add('collapse');
|
|
el.classList.remove('collapsing');
|
|
}, el.transitionDuration)
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const fallbackLanguage = 'en';
|
|
const startingLanguage = computeStartingLanguage();
|
|
const STATUS_PAID = ['complete', 'confirmed', 'paid'];
|
|
const STATUS_UNPAYABLE = ['expired', 'invalid'];
|
|
|
|
function computeStartingLanguage() {
|
|
const { defaultLang } = initialSrvModel;
|
|
return isLanguageAvailable(defaultLang) ? defaultLang : fallbackLanguage;
|
|
}
|
|
|
|
function isLanguageAvailable(languageCode) {
|
|
return availableLanguages.indexOf(languageCode) >= 0;
|
|
}
|
|
|
|
Vue.use(VueI18next);
|
|
|
|
const i18n = new VueI18next(i18next);
|
|
const eventBus = new Vue();
|
|
|
|
const PaymentDetails = Vue.component('payment-details', {
|
|
el: '#payment-details',
|
|
props: {
|
|
srvModel: Object,
|
|
isActive: Boolean
|
|
},
|
|
computed: {
|
|
orderAmount () {
|
|
return parseFloat(this.srvModel.orderAmount);
|
|
},
|
|
btcDue () {
|
|
return parseFloat(this.srvModel.btcDue);
|
|
},
|
|
btcPaid () {
|
|
return parseFloat(this.srvModel.btcPaid);
|
|
},
|
|
showRecommendedFee () {
|
|
return this.isActive && this.srvModel.showRecommendedFee && this.srvModel.feeRate;
|
|
},
|
|
}
|
|
});
|
|
|
|
function initApp() {
|
|
return new Vue({
|
|
i18n,
|
|
el: '#Checkout-v2',
|
|
components: {
|
|
PaymentDetails
|
|
},
|
|
data () {
|
|
const srvModel = initialSrvModel;
|
|
return {
|
|
srvModel,
|
|
displayPaymentDetails: false,
|
|
remainingSeconds: srvModel.expirationSeconds,
|
|
emailAddressInput: "",
|
|
emailAddressInputDirty: false,
|
|
emailAddressInputInvalid: false,
|
|
paymentMethodId: null,
|
|
endData: null,
|
|
isModal: srvModel.isModal
|
|
}
|
|
},
|
|
computed: {
|
|
isUnpayable () {
|
|
return STATUS_UNPAYABLE.includes(this.srvModel.status);
|
|
},
|
|
isPaid () {
|
|
return STATUS_PAID.includes(this.srvModel.status);
|
|
},
|
|
isActive () {
|
|
return !this.isUnpayable && !this.isPaid;
|
|
},
|
|
showInfo () {
|
|
return this.showTimer || this.showPaymentDueInfo;
|
|
},
|
|
showTimer () {
|
|
return this.isActive && this.remainingSeconds < this.srvModel.displayExpirationTimer;
|
|
},
|
|
showPaymentDueInfo () {
|
|
return this.btcPaid > 0 && this.btcDue > 0;
|
|
},
|
|
showRecommendedFee () {
|
|
return this.isActive() && this.srvModel.showRecommendedFee && this.srvModel.feeRate;
|
|
},
|
|
orderAmount () {
|
|
return parseFloat(this.srvModel.orderAmount);
|
|
},
|
|
btcDue () {
|
|
return parseFloat(this.srvModel.btcDue);
|
|
},
|
|
btcPaid () {
|
|
return parseFloat(this.srvModel.btcPaid);
|
|
},
|
|
pmId () {
|
|
return this.paymentMethodId || this.srvModel.paymentMethodId;
|
|
},
|
|
minutesLeft () {
|
|
return Math.floor(this.remainingSeconds / 60);
|
|
},
|
|
secondsLeft () {
|
|
return Math.floor(this.remainingSeconds % 60);
|
|
},
|
|
timeText () {
|
|
return this.remainingSeconds > 0
|
|
? `${this.padTime(this.minutesLeft)}:${this.padTime(this.secondsLeft)}`
|
|
: '00:00';
|
|
},
|
|
storeLink () {
|
|
return this.srvModel.merchantRefLink && this.srvModel.merchantRefLink !== this.srvModel.receiptLink
|
|
? this.srvModel.merchantRefLink
|
|
: null;
|
|
},
|
|
paymentMethodIds () {
|
|
return this.srvModel.availableCryptos.map(function (c) { return c.paymentMethodId });
|
|
},
|
|
paymentMethodComponent () {
|
|
return this.isPluginPaymentMethod
|
|
? `${this.pmId}Checkout`
|
|
: this.srvModel.activated && this.srvModel.uiSettings.checkoutBodyVueComponentName;
|
|
},
|
|
isPluginPaymentMethod () {
|
|
return !this.paymentMethodIds.includes(this.pmId);
|
|
}
|
|
},
|
|
mounted () {
|
|
this.updateData(this.srvModel);
|
|
this.updateTimer();
|
|
if (this.isActive) {
|
|
this.listenIn();
|
|
}
|
|
window.parent.postMessage('loaded', '*');
|
|
},
|
|
methods: {
|
|
changePaymentMethod (id) { // payment method or plugin id
|
|
if (this.pmId !== id) {
|
|
this.paymentMethodId = id;
|
|
this.fetchData();
|
|
}
|
|
},
|
|
changeLanguage (e) {
|
|
const lang = e.target.value;
|
|
if (isLanguageAvailable(lang)) {
|
|
i18next.changeLanguage(lang);
|
|
}
|
|
},
|
|
padTime (val) {
|
|
return val.toString().padStart(2, '0');
|
|
},
|
|
close () {
|
|
window.parent.postMessage('close', '*');
|
|
},
|
|
updateTimer () {
|
|
this.remainingSeconds = Math.floor((this.endDate.getTime() - new Date().getTime())/1000);
|
|
if (this.isActive) {
|
|
setTimeout(this.updateTimer, 500);
|
|
}
|
|
},
|
|
listenIn () {
|
|
let socket = null;
|
|
const updateFn = this.fetchData;
|
|
const supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
|
|
if (supportsWebSockets) {
|
|
const protocol = window.location.protocol.replace('http', 'ws');
|
|
const wsUri = `${protocol}//${window.location.host}${statusWsUrl}`;
|
|
try {
|
|
socket = new WebSocket(wsUri);
|
|
socket.onmessage = async function (e) {
|
|
if (e.data !== 'ping') await updateFn();
|
|
};
|
|
socket.onerror = function (e) {
|
|
console.error('Error while connecting to websocket for invoice notifications (callback):', e);
|
|
};
|
|
}
|
|
catch (e) {
|
|
console.error('Error while connecting to websocket for invoice notifications', e);
|
|
}
|
|
}
|
|
// fallback in case there is no websocket support
|
|
(function watcher() {
|
|
setTimeout(async function () {
|
|
if (socket === null || socket.readyState !== 1) {
|
|
await updateFn();
|
|
}
|
|
watcher();
|
|
}, 2000);
|
|
})();
|
|
},
|
|
async fetchData () {
|
|
if (this.isPluginPaymentMethod) return;
|
|
|
|
const url = `${statusUrl}&paymentMethodId=${this.pmId}`;
|
|
const response = await fetch(url);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this.updateData(data);
|
|
}
|
|
},
|
|
updateData (data) {
|
|
if (this.srvModel.status !== data.status) {
|
|
const { invoiceId } = this.srvModel;
|
|
const { status } = data;
|
|
window.parent.postMessage({ invoiceId, status }, '*');
|
|
}
|
|
|
|
// displaying satoshis for lightning payments
|
|
data.cryptoCodeSrv = data.cryptoCode;
|
|
|
|
const newEnd = new Date();
|
|
newEnd.setSeconds(newEnd.getSeconds() + data.expirationSeconds);
|
|
this.endDate = newEnd;
|
|
|
|
// updating ui
|
|
this.srvModel = data;
|
|
eventBus.$emit('data-fetched', this.srvModel);
|
|
|
|
const self = this;
|
|
if (this.isPaid && data.redirectAutomatically && data.merchantRefLink) {
|
|
setTimeout(function () {
|
|
if (self.isModal && window.top.location === data.merchantRefLink){
|
|
self.close();
|
|
} else {
|
|
window.top.location = data.merchantRefLink;
|
|
}
|
|
}, 2000);
|
|
}
|
|
},
|
|
replaceNewlines (value) {
|
|
return value ? value.replace(/\n/ig, '<br>') : '';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
i18next
|
|
.use(window.i18nextHttpBackend)
|
|
.init({
|
|
backend: {
|
|
loadPath: i18nUrl
|
|
},
|
|
lng: startingLanguage,
|
|
fallbackLng: fallbackLanguage,
|
|
nsSeparator: false,
|
|
keySeparator: false,
|
|
load: 'currentOnly'
|
|
}, initApp);
|