btcpayserver/BTCPayServer/wwwroot/checkout-v2/checkout.js
d11n 068b717a75
Checkout v2: Configure countdown timer (#4471)
* 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
2023-01-16 20:45:19 +09:00

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);