Split Bitcoin/LN to partial defined in payment handler and convert checkout to Vue (#996)

This commit is contained in:
Andrew Camilleri 2019-08-27 04:31:15 +02:00 committed by Nicolas Dorier
parent 55c0c0ea6f
commit 9a9e31c759
10 changed files with 596 additions and 766 deletions

View file

@ -79,7 +79,6 @@
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\checkout\js\core.js" />
<None Include="wwwroot\vendor\bootstrap4-creativestart\creative.js" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.svg" />
<None Include="wwwroot\vendor\font-awesome\fonts\fontawesome-webfont.woff2" />

View file

@ -9,6 +9,7 @@ namespace BTCPayServer.Models.InvoicingModels
{
public string ExtensionPartial { get; set; }
public string CheckoutBodyVueComponentName { get; set; }
public string CheckoutHeaderVueComponentName { get; set; }
public string NoScriptPartialName { get; set; }
}
public class PaymentModel

View file

@ -76,6 +76,21 @@ namespace BTCPayServer.Payments
Dictionary<CurrencyPair, Task<RateResult>> rate, Money amount, PaymentMethodId paymentMethodId);
public abstract IEnumerable<PaymentMethodId> GetSupportedPaymentMethods();
public virtual CheckoutUIPaymentMethodSettings GetCheckoutUISettings()
{
return new CheckoutUIPaymentMethodSettings()
{
ExtensionPartial = "Bitcoin_Lightning_LikeMethodCheckout",
CheckoutBodyVueComponentName = "BitcoinLightningLikeMethodCheckout",
CheckoutHeaderVueComponentName = "BitcoinLightningLikeMethodCheckoutHeader",
NoScriptPartialName = "Bitcoin_Lightning_LikeMethodCheckoutNoScript"
};
}
public PaymentMethod GetPaymentMethodInInvoice(InvoiceEntity invoice, PaymentMethodId paymentMethodId)
{
return invoice.GetPaymentMethod(paymentMethodId);
}
public virtual object PreparePayment(TSupportedPaymentMethod supportedPaymentMethod, StoreData store,
BTCPayNetworkBase network)
@ -93,11 +108,6 @@ namespace BTCPayServer.Payments
throw new NotSupportedException("Invalid supportedPaymentMethod");
}
public virtual CheckoutUIPaymentMethodSettings GetCheckoutUISettings()
{
return null;
}
object IPaymentMethodHandler.PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store,
BTCPayNetworkBase network)

View file

@ -12,17 +12,17 @@
<img class="header__icon__img" src="~/img/logo-white.png" height="40">
}
</div>
<div class="close-icon close-action">
<div class="close-icon close-action" v-on:click="close">
&#10006;
</div>
</div>
<div class="timer-row">
<div class="timer-row__progress-bar" style="width: 0%;"></div>
<div class="timer-row__spinner">
<partial name="Checkout-Spinner" />
<div class="timer-row" v-bind:class="{ 'expiring-soon': expiringSoon }">
<div class="timer-row__progress-bar" v-bind:style="{ 'width': expirationPercentage+ '%' }"></div>
<div class="timer-row__spinner" v-if="!invoiceUnpayable && !invoicePaid">
<partial name="Checkout-Spinner"/>
</div>
<div class="timer-row__message">
<span v-if="srvModel.status === 'expired' || srvModel.status === 'invalid'">
<span v-if="invoiceUnpayable">
{{$t("Invoice expired")}}
</span>
<span v-else-if="expiringSoon">
@ -32,7 +32,7 @@
{{$t("Awaiting Payment...")}}
</span>
</div>
<div class="timer-row__time-left">@Model.TimeLeft</div>
<div class="timer-row__time-left">{{timerText}}</div>
</div>
</div>
<div class="order-details">
@ -45,9 +45,9 @@
<div class="single-item-order__right">
@if (Model.AvailableCryptos.Count > 1)
{
<div class="paywithRowRight cursorPointer" onclick="openPaymentMethodDialog()">
<span class="payment__currencies ">
<img v-bind:src="srvModel.cryptoImage" />
<div class="paywithRowRight cursorPointer" v-on:click="openPaymentMethodDialog">
<span class="payment__currencies " v-show="!changingCurrencies">
<img v-bind:src="srvModel.cryptoImage"/>
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCodeSrv}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
<span class="clickable_indicator fa fa-angle-right"></span>
@ -58,8 +58,8 @@
@foreach (var crypto in Model.AvailableCryptos)
{
<li class="vexmenuitem">
<a href="@crypto.Link" onclick="return closePaymentMethodDialog('@crypto.PaymentMethodId');">
<img alt="@crypto.PaymentMethodName" src="@crypto.CryptoImage" />
<a href="@crypto.Link" onclick="closePaymentMethodDialog('@crypto.PaymentMethodId');return false;">
<img alt="@crypto.PaymentMethodName" src="@crypto.CryptoImage"/>
@crypto.PaymentMethodName
@(crypto.IsLightning ? Html.Raw("&#9889;") : null)
<span>@crypto.CryptoCode</span>
@ -72,18 +72,18 @@
else
{
<div class="payment__currencies_noborder">
<img v-bind:src="srvModel.cryptoImage" />
<img v-bind:src="srvModel.cryptoImage"/>
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCodeSrv}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
</div>
}
<div class="payment__spinner">
<partial name="Checkout-Spinner" />
<div class="payment__spinner" v-show="changingCurrencies || loading">
<partial name="Checkout-Spinner"/>
</div>
</div>
</div>
<div class="single-item-order buyerTotalLine">
<div class="single-item-order buyerTotalLine" v-on:click="toggleLineItems" v-bind:class="{ 'expanded': lineItemsExpanded}">
<div class="single-item-order__left">
<div class="single-item-order__left__name">
{{ srvModel.storeName }}
@ -110,7 +110,7 @@
<div class="extraPayment" v-if="srvModel.status === 'new' && srvModel.txCount > 1">
{{$t("NotPaid_ExtraTransaction")}}
</div>
<div class="line-items">
<div class="line-items" v-bind:class="{ 'expanded': lineItemsExpanded}">
<div class="line-items__item">
<div class="line-items__item__label">{{$t("Order Amount")}}</div>
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
@ -146,251 +146,50 @@
</div>
</div>
</line-items>
<div class="payment-tabs" v-if="!srvModel.uiSettings || !srvModel.uiSettings.checkoutBodyVueComponentName">
<div class="payment-tabs__tab active" id="scan-tab">
<span>{{$t("Scan")}}</span>
</div>
<div class="payment-tabs__tab" id="copy-tab">
<span>{{$t("Copy")}}</span>
</div>
@if (Model.ChangellyEnabled || Model.CoinSwitchEnabled)
{
<div class="payment-tabs__tab" id="altcoins-tab">
<span>{{$t("Conversion")}}</span>
</div>
<div id="tabsSlider" class="payment-tabs__slider three-tabs"></div>
}
else
{
<div id="tabsSlider" class="payment-tabs__slider"></div>
}
</div>
<component
v-if="srvModel.uiSettings && srvModel.uiSettings.checkoutHeaderVueComponentName"
v-bind:srv-model="srvModel"
v-bind:is="srvModel.uiSettings.checkoutHeaderVueComponentName">
</component>
</div>
<div class="payment-box">
<div class="bp-view payment manual-flow enter-contact-email active" id="emailAddressView">
<form class="manual__step-one refund-address-form contact-email-form" id="emailAddressForm" name="emailAddressForm" novalidate="">
<div class="manual__step-one__header">
<span>{{$t("Contact and Refund Email")}}</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<span>{{$t("Contact_Body")}}</span>
</span>
<span class="submission-error-label">{{$t("Please enter a valid email address")}}</span>
</div>
<div class="input-wrapper">
<input class="bp-input email-input ng-pristine ng-invalid ng-touched" id="emailAddressFormInput" v-bind:placeholder="$t('Your email')" type="email">
<bp-loading-button>
<button type="submit" class="action-button" style="margin-top: 15px;">
<span class="button-text">{{$t("Continue")}}</span>
<div class="loader-wrapper">
<partial name="Checkout-Spinner"/>
</div>
</button>
</bp-loading-button>
</div>
</form>
</div>
<div v-if="showPaymentUI">
<component
v-if="srvModel.uiSettings && srvModel.uiSettings.checkoutBodyVueComponentName"
v-bind:srv-model="srvModel"
v-bind:is="srvModel.uiSettings.checkoutBodyVueComponentName">
</component>
<template v-else>
<div class="bp-view payment scan" id="scan">
<div class="wrapBtnGroup" v-bind:class="{ invisible: lndModel === null || !scanDisplayQr }">
<div class="btnGroupLnd">
<button onclick="lndToggleBolt11()" v-bind:class="{ active: lndModel != null && lndModel.toggle === 0 }"
v-bind:title="$t('BOLT 11 Invoice')">
{{$t("BOLT 11 Invoice")}}
<div class="bp-view payment manual-flow enter-contact-email" id="emailAddressView" v-bind:class="{ 'active': showEmailForm}">
<form class="manual__step-one refund-address-form contact-email-form" id="emailAddressForm" name="emailAddressForm" novalidate="" v-on:submit.prevent="onEmailSubmit">
<div class="manual__step-one__header">
<span>{{$t("Contact and Refund Email")}}</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<span>{{$t("Contact_Body")}}</span>
</span>
<span class="submission-error-label">{{$t("Please enter a valid email address")}}</span>
</div>
<div class="input-wrapper">
<input class="bp-input email-input "
v-bind:class="{ 'ng-pristine ng-submitted ng-touched': !emailAddressInputDirty, 'ng-invalid': emailAddressInputInvalid }" id="emailAddressFormInput"
v-bind:placeholder="$t('Your email')" type="email" v-model="emailAddressInput"
v-on:change="onEmailChange">
<bp-loading-button>
<button type="submit" class="action-button" style="margin-top: 15px;" v-bind:disabled="emailAddressFormSubmitting" v-bind:class="{ 'loading': emailAddressFormSubmitting }">
<span class="button-text">{{$t("Continue")}}</span>
<div class="loader-wrapper">
<partial name="Checkout-Spinner"/>
</div>
</button>
<button onclick="lndToggleNode()" v-bind:class="{ active: lndModel != null && lndModel.toggle === 1 }"
v-bind:title="$t('Node Info')">
{{$t("Node Info")}}
</button>
</div>
</bp-loading-button>
</div>
<div class="payment__scan">
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon"
v-if="scanDisplayQr"/>
<qrcode v-bind:value="scanDisplayQr" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"
v-if="scanDisplayQr">
</qrcode>
</form>
</div>
<div v-if="showPaymentUI">
<component
v-if="srvModel.uiSettings && srvModel.uiSettings.checkoutBodyVueComponentName"
v-bind:srv-model="srvModel"
v-bind:is="srvModel.uiSettings.checkoutBodyVueComponentName">
</component>
</div>
<div class="payment__spinner qr_currency_icon" style="padding-right: 20px;">
<partial name="Checkout-Spinner"/>
</div>
</div>
<div class="payment__details__instruction__open-wallet" v-if="scanDisplayQr">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span>{{$t("Open in wallet")}}</span>
</a>
</div>
</div>
<div class="bp-view payment manual-flow" id="copy">
<div class="manual__step-two__instructions">
<span i18n="">{{$t("CompletePay_Body", srvModel)}}</span>
</div>
<div class="copyLabelPopup">
<span>{{$t("Copied")}}</span>
</div>
<nav v-if="srvModel.isLightning" class="copyBox">
<div class="copySectionBox bottomBorder">
<label>{{$t("BOLT 11 Invoice")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.btcAddress" readonly="readonly"/>
<img v-bind:src="srvModel.cryptoImage"/>
</div>
</div>
<div class="separatorGem"></div>
<div class="copySectionBox">
<label>{{$t("Node Info")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.peerInfo" readonly="readonly"/>
<img v-bind:src="srvModel.cryptoImage"/>
</div>
</div>
</nav>
<nav v-else class="copyBox">
<div class="copySectionBox bottomBorder">
<label>{{$t("Amount")}}</label>
<div class="copyAmountText copy-cursor _copySpan">
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
</div>
</div>
<div class="separatorGem"></div>
<div class="copySectionBox">
<label>{{$t("Address")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.btcAddress" readonly="readonly"/>
<img v-bind:src="srvModel.cryptoImage"/>
</div>
</div>
</nav>
</div>
@if (Model.ChangellyEnabled || Model.CoinSwitchEnabled)
{
<div id="altcoins" class="bp-view payment manual-flow">
<nav v-if="srvModel.isLightning">
<div class="manual__step-two__instructions">
<span>
{{$t("ConversionTab_Lightning")}}
</span>
</div>
</nav>
<nav v-else>
<div class="manual__step-two__instructions">
<span>
{{$t("ConversionTab_BodyTop", srvModel)}}
<br/><br/>
{{$t("ConversionTab_BodyDesc", srvModel)}}
</span>
</div>
<center>
@if (Model.CoinSwitchEnabled && Model.ChangellyEnabled)
{
<template v-if="!selectedThirdPartyProcessor">
<button v-on:click="selectedThirdPartyProcessor = 'coinswitch'" class="action-button">
{{$t("Pay with CoinSwitch")}}
</button>
<button v-on:click="selectedThirdPartyProcessor = 'changelly'" class="action-button">
{{$t("Pay with Changelly")}}
</button>
</template>
}
@if (Model.CoinSwitchEnabled)
{
<coinswitch inline-template
v-if="!srvModel.changellyEnabled || selectedThirdPartyProcessor === 'coinswitch'"
:mode="srvModel.coinSwitchMode"
:merchant-id="srvModel.coinSwitchMerchantId"
:to-currency="srvModel.paymentMethodId"
:to-currency-due="coinswitchAmountDue"
:autoload="selectedThirdPartyProcessor === 'coinswitch'"
:to-currency-address="srvModel.btcAddress">
<div>
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url && !opened">
{{$t("Pay with CoinSwitch")}}
</a>
@if (Model.ChangellyEnabled)
{
<button v-show="!opened" v-on:click="$parent.selectedThirdPartyProcessor = 'changelly'" class="btn-link mt-2">
{{$t("Pay with Changelly")}}
</button>
}
<iframe
v-if="showInlineIFrame"
v-on:load="onLoadIframe"
style="height: 100%; position: fixed; top: 0; width: 100%; left: 0;"
sandbox="allow-scripts allow-forms allow-popups allow-same-origin"
:src="url">
</iframe>
</div>
</coinswitch>
}
@if (Model.ChangellyEnabled)
{
<changelly inline-template
v-if="!srvModel.coinSwitchEnabled || selectedThirdPartyProcessor === 'changelly'"
:merchant-id="srvModel.changellyMerchantId"
:store-id="srvModel.storeId"
:to-currency="srvModel.paymentMethodId"
:to-currency-due="srvModel.changellyAmountDue"
:to-currency-address="srvModel.btcAddress">
<div class="changelly-component">
<div class="changelly-component-dropdown-holder" v-show="prettyDropdownInstance">
<select
v-model="selectedFromCurrency"
:disabled="isLoading"
v-on:change="onCurrencyChange($event)"
ref="changellyCurrenciesDropdown">
<option value="">{{$t("ConversionTab_CurrencyList_Select_Option")}}</option>
<option v-for="currency of currencies"
:data-prefix="'<img src=\''+currency.image+'\'/>'"
:value="currency.name">
{{currency.fullName}}
</option>
</select>
</div>
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url">
{{$t("Pay with Changelly")}}
</a>
@if (Model.CoinSwitchEnabled)
{
<button v-on:click="$parent.selectedThirdPartyProcessor = 'coinswitch'" class="btn-link mt-2">
{{$t("Pay with CoinSwitch")}}
</button>
}
<button class="retry-button" v-if="calculateError" v-on:click="retry('calculateAmount')">
{{$t("ConversionTab_CalculateAmount_Error")}}
</button>
<button class="retry-button" v-if="currenciesError" v-on:click="retry('loadCurrencies')">
{{$t("ConversionTab_LoadCurrencies_Error")}}
</button>
<div v-show="isLoading" class="general__spinner">
<partial name="Checkout-Spinner"/>
</div>
</div>
</changelly>
}
</center>
</nav>
</div>
}
</template>
</div>
<div class="bp-view" id="paid">
<div class="bp-view" id="paid" v-bind:class="{ 'active': invoicePaid}">
<div class="status-block">
<div class="success-block">
<div class="status-icon">
@ -404,10 +203,10 @@
</div>
</div>
<div class="success-message">{{$t("This invoice has been paid")}}</div>
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!isModal">
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!srvModel.isModal">
<span v-html="$t('Return to StoreName', srvModel)"></span>
</a>
<button class="action-button close-action" v-show="isModal">
<button class="action-button close-action" v-show="srvModel.isModal" v-on:click="close">
<span v-html="$t('Close')">{{$t("Return to StoreName", srvModel)}}</span>
</button>
</div>
@ -416,21 +215,7 @@
</div>
</div>
<div class="bp-view expired" id="archived">
<div class="expired-icon">
<img src="~/imlegacy/archived.svg">
</div>
<div class="archived__message">
<div class="archived__message__header">
<span>{{$t("This invoice has been archived")}}</span>
</div>
<div>
<span>{{$t("Archived_Body")}}</span>
</div>
</div>
</div>
<div class="bp-view expired" id="expired">
<div class="bp-view expired" id="expired" v-bind:class="{ 'active': invoiceUnpayable}">
<div>
<div class="expired__body" style="margin-bottom: 20px;">
<div class="expired__header">{{$t("What happened?")}}</div>
@ -446,15 +231,15 @@
<div class="expired__text expired__text__smaller">
<span class="expired__text__bullet">{{$t("Invoice ID")}}</span>:
{{srvModel.invoiceId}}
<br />
<br/>
<span class="expired__text__bullet">{{$t("Order ID")}}</span>:
{{srvModel.orderId}}
</div>
</div>
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!isModal">
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!srvModel.isModal">
<span>{{$t("Return to StoreName", srvModel)}}</span>
</a>
<button class="action-button close-action" v-show="isModal">
<button class="action-button close-action" v-show="srvModel.isModal" v-on:click="close">
<span>{{$t("Return to StoreName", srvModel)}}</span>
</button>
</div>

View file

@ -68,7 +68,7 @@
<invoice>
<div class="no-bounce" id="checkoutCtrl" v-cloak>
<div class="modal page">
<div class="modal-dialog open opened enter-purchaser-email" role="document">
<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">
@ -117,7 +117,6 @@
var storeDefaultLang = @Safe.Json(@Model.DefaultLang);
var fallbackLanguage = "en";
startingLanguage = computeStartingLanguage();
// initialization
i18next
.use(window.i18nextXHRBackend)
.init({
@ -152,40 +151,222 @@
}
var i18n = new VueI18next(i18next);
// TODO: Move all logic from core.js to Vue controller
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',
components: {
qrcode: VueQrcode,
changelly: ChangellyComponent,
coinswitch: CoinSwitchComponent
},
data: {
srvModel: srvModel,
lndModel: null,
scanDisplayQr: "",
expiringSoon: false,
isModal: srvModel.isModal,
lightningAmountInSatoshi: srvModel.lightningAmountInSatoshi,
selectedThirdPartyProcessor: ""
end: new Date(),
expirationPercentage: 0,
timerText: "@Model.TimeLeft",
emailAddressInput: "",
emailAddressInputDirty: false,
emailAddressInputInvalid: false,
emailAddressFormSubmitting: false,
lineItemsExpanded: false,
changingCurrencies: false,
loading: true
},
computed: {
coinswitchAmountDue: function() {
return this.srvModel.coinSwitchAmountMarkupPercentage
? this.srvModel.btcDue * (1 + (this.srvModel.coinSwitchAmountMarkupPercentage / 100))
: this.srvModel.btcDue;
expiringSoon: function(){
return this.expirationPercentage >= 75 && !this.invoiceUnpayable && !this.invoicePaid;
},
showPaymentUI: function(){
var disallowedStatuses = ["complete","confirmed" ,"paid", "expired", "invalid"];
return (!this.srvModel.requiresRefundEmail || validateEmail(srvModel.customerEmail)) && disallowedStatuses.indexOf(this.srvModel.status) < 0;
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;
},
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;
},
numberFormatted: function(x) {
var rounded = Math.round(x);
var parts = rounded.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " ");
return parts.join(".");
},
openPaymentMethodDialog: function() {
var content = $("#vexPopupDialog").html();
vex.open({
unsafeContent: content
});
},
closePaymentMethodDialog: function(currencyId) {
vex.closeAll();
this.changeCurrency(currencyId);
},
changeCurrency: function (currency) {
if (currency !== null && srvModel.paymentMethodId !== currency) {
this.changingCurrencies = true;
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 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) {
this.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");
}
}
function watcher(){
setTimeout(function(){
if (socket === null || socket.readyState !== 1) {
this.fetchData();
}
watcher();
}, 2000);
}
watcher();
},
fetchData: function(){
var self = this;
$.ajax({
url: window.location.pathname + "/status?invoiceId=" + srvModel.invoiceId + "&paymentMethodId=" + 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": srvModel.invoiceId, "status": jsonData.status }, "*");
}
if (jsonData.paymentMethodId === this.srvModel.paymentMethodId) {
this.changingCurrencies = false;
}
// displaying satoshis for lightning payments
jsonData.cryptoCodeSrv = jsonData.cryptoCode;
if (jsonData.isLightning && jsonData.lightningAmountInSatoshi && jsonData.cryptoCode === "BTC") {
var SATOSHIME = 100000000;
jsonData.cryptoCode = "Sats";
jsonData.btcDue = numberFormatted(jsonData.btcDue * SATOSHIME);
jsonData.btcPaid = numberFormatted(jsonData.btcPaid * SATOSHIME);
jsonData.networkFee = numberFormatted(jsonData.networkFee * SATOSHIME);
jsonData.orderAmount = numberFormatted(jsonData.orderAmount * SATOSHIME);
}
// 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;
}
}
}
});

View file

@ -22,15 +22,7 @@
}
else
{
<div>
<p>To complete payment, please send <b>@Model.BtcDue @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
<p>Time remaining: @Model.TimeLeft</p>
<p><a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;">@Model.InvoiceBitcoinUrl</a></p>
@if (Model.IsLightning)
{
<p>Peer Info: <b>@Model.PeerInfo</b></p>
}
</div>
<h1 class="text-danger">This payment method requires javascript.</h1>
}
@if (Model.AvailableCryptos.Count > 1)
{

View file

@ -0,0 +1,304 @@
@model BTCPayServer.Models.InvoicingModels.PaymentModel
<script type="text/x-template" id="bitcoin-lightning-method-checkout-template">
<div>
<div class="bp-view payment scan" id="scan" v-bind:class="{ 'active': currentTab == 'scan'}">
<div class="wrapBtnGroup" v-bind:class="{ invisible: !srvModel.isLightning || !scanDisplayQr }">
<div class="btnGroupLnd">
<button
v-on:click="toggleLightningData('bolt11')"
v-bind:class="{ active: currentLightningDisplay === 'bolt11' }"
v-bind:title="$t('BOLT 11 Invoice')">
{{$t("BOLT 11 Invoice")}}
</button>
<button
v-on:click="toggleLightningData('node')"
v-bind:class="{ active: currentLightningDisplay === 'node' }"
v-bind:title="$t('Node Info')">
{{$t("Node Info")}}
</button>
</div>
</div>
<div class="payment__scan">
<img v-bind:src="srvModel.cryptoImage" class="qr_currency_icon" v-if="scanDisplayQr"/>
<qrcode v-bind:value="scanDisplayQr" :options="{ width: 256, margin: 0, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg" v-if="scanDisplayQr"></qrcode>
<div class="payment__spinner qr_currency_icon" style="padding-right: 20px;">
<partial name="Checkout-Spinner"/>
</div>
</div>
<div class="payment__details__instruction__open-wallet" v-if="srvModel.invoiceBitcoinUrl">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span>{{$t("Open in wallet")}}</span>
</a>
</div>
</div>
<div class="bp-view payment manual-flow" id="copy" v-bind:class="{ 'active': currentTab == 'copy'}">
<div class="manual__step-two__instructions">
<span i18n="">{{$t("CompletePay_Body", srvModel)}}</span>
</div>
<div class="copyLabelPopup">
<span>{{$t("Copied")}}</span>
</div>
<nav v-if="srvModel.isLightning" class="copyBox">
<div class="copySectionBox bottomBorder">
<label>{{$t("BOLT 11 Invoice")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.btcAddress" readonly="readonly"/>
<img v-bind:src="srvModel.cryptoImage"/>
</div>
</div>
<div class="separatorGem"></div>
<div class="copySectionBox">
<label>{{$t("Node Info")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.peerInfo" readonly="readonly"/>
<img v-bind:src="srvModel.cryptoImage"/>
</div>
</div>
</nav>
<nav v-else class="copyBox">
<div class="copySectionBox bottomBorder">
<label>{{$t("Amount")}}</label>
<div class="copyAmountText copy-cursor _copySpan">
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
</div>
</div>
<div class="separatorGem"></div>
<div class="copySectionBox">
<label>{{$t("Address")}}</label>
<div class="inputWithIcon _copyInput">
<input type="text" class="checkoutTextbox" v-bind:value="srvModel.btcAddress" readonly="readonly"/>
<img v-bind:src="srvModel.cryptoImage"/>
</div>
</div>
</nav>
</div>
@if (Model.ChangellyEnabled || Model.CoinSwitchEnabled)
{
<div id="altcoins" class="bp-view payment manual-flow" v-bind:class="{ 'active': currentTab == 'altcoins'}">
<nav v-if="srvModel.isLightning">
<div class="manual__step-two__instructions">
<span>
{{$t("ConversionTab_Lightning")}}
</span>
</div>
</nav>
<nav v-else>
<div class="manual__step-two__instructions">
<span>
{{$t("ConversionTab_BodyTop", srvModel)}}
<br/><br/>
{{$t("ConversionTab_BodyDesc", srvModel)}}
</span>
</div>
<center>
@if (Model.CoinSwitchEnabled && Model.ChangellyEnabled)
{
<template v-if="!selectedThirdPartyProcessor">
<button v-on:click="selectedThirdPartyProcessor = 'coinswitch'" class="action-button">{{$t("Pay with CoinSwitch")}}</button>
<button v-on:click="selectedThirdPartyProcessor = 'changelly'" class="action-button">{{$t("Pay with Changelly")}}</button>
</template>
}
@if (Model.CoinSwitchEnabled)
{
<coinswitch inline-template
v-if="!srvModel.changellyEnabled || selectedThirdPartyProcessor === 'coinswitch'"
:mode="srvModel.coinSwitchMode"
:merchant-id="srvModel.coinSwitchMerchantId"
:to-currency="srvModel.paymentMethodId"
:to-currency-due="coinswitchAmountDue"
:autoload="selectedThirdPartyProcessor === 'coinswitch'"
:to-currency-address="srvModel.btcAddress">
<div>
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url && !opened">{{$t("Pay with CoinSwitch")}}</a>
@if (Model.ChangellyEnabled)
{
<button v-show="!opened" v-on:click="$parent.selectedThirdPartyProcessor = 'changelly'" class="btn-link mt-2">{{$t("Pay with Changelly")}}</button>
}
<iframe
v-if="showInlineIFrame"
v-on:load="onLoadIframe"
style="height: 100%; position: fixed; top: 0; width: 100%; left: 0;"
sandbox="allow-scripts allow-forms allow-popups allow-same-origin"
:src="url">
</iframe>
</div>
</coinswitch>
}
@if (Model.ChangellyEnabled)
{
<changelly inline-template
v-if="!srvModel.coinSwitchEnabled || selectedThirdPartyProcessor === 'changelly'"
:merchant-id="srvModel.changellyMerchantId"
:store-id="srvModel.storeId"
:to-currency="srvModel.paymentMethodId"
:to-currency-due="srvModel.changellyAmountDue"
:to-currency-address="srvModel.btcAddress">
<div class="changelly-component">
<div class="changelly-component-dropdown-holder" v-show="prettyDropdownInstance">
<select
v-model="selectedFromCurrency"
:disabled="isLoading"
v-on:change="onCurrencyChange($event)"
ref="changellyCurrenciesDropdown">
<option value="">{{$t("ConversionTab_CurrencyList_Select_Option")}}</option>
<option v-for="currency of currencies"
:data-prefix="'<img src=\''+currency.image+'\'/>'"
:value="currency.name">{{currency.fullName}}</option>
</select>
</div>
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url">{{$t("Pay with Changelly")}}</a>
@if (Model.CoinSwitchEnabled)
{
<button v-on:click="$parent.selectedThirdPartyProcessor = 'coinswitch'" class="btn-link mt-2">{{$t("Pay with CoinSwitch")}}</button>
}
<button class="retry-button" v-if="calculateError" v-on:click="retry('calculateAmount')">
{{$t("ConversionTab_CalculateAmount_Error")}}
</button>
<button class="retry-button" v-if="currenciesError" v-on:click="retry('loadCurrencies')">
{{$t("ConversionTab_LoadCurrencies_Error")}}
</button>
<div v-show="isLoading" class="general__spinner">
<partial name="Checkout-Spinner"/>
</div>
</div>
</changelly>
}
</center>
</nav>
</div>
}
</div>
</script>
<script type="text/x-template" id="bitcoin-lightning-method-checkout-header-template">
<div class="payment-tabs">
<div class="payment-tabs__tab " id="scan-tab" v-on:click="switchTab('scan')" v-bind:class="{ 'active': currentTab == 'scan'}" >
<span>{{$t("Scan")}}</span>
</div>
<div class="payment-tabs__tab" id="copy-tab" v-on:click="switchTab('copy')" v-bind:class="{ 'active': currentTab == 'copy'}" >
<span>{{$t("Copy")}}</span>
</div>
@if (Model.ChangellyEnabled || Model.CoinSwitchEnabled)
{
<div class="payment-tabs__tab" id="altcoins-tab" v-on:click="switchTab('altcoins')" v-bind:class="{ 'active': currentTab == 'altcoins'}" >
<span>{{$t("Conversion")}}</span>
</div>
<div id="tabsSlider" class="payment-tabs__slider three-tabs" v-bind:class="['slide-'+currentTab]"></div>
}
else
{
<div id="tabsSlider" class="payment-tabs__slider" v-bind:class="['slide-'+currentTab]"></div>
}
</div>
</script>
<script type="text/javascript">
Vue.component('BitcoinLightningLikeMethodCheckout',
{
props: ["srvModel"],
template: "#bitcoin-lightning-method-checkout-template",
components: {
qrcode: VueQrcode,
changelly: ChangellyComponent,
coinswitch: CoinSwitchComponent
},
data: function() {
return {
currentLightningDisplay: "bolt11",
selectedThirdPartyProcessor: "",
currentTab: "scan"
}
},
computed: {
coinswitchAmountDue: function() {
return this.srvModel.coinSwitchAmountMarkupPercentage
? this.srvModel.btcDue * (1 + (this.srvModel.coinSwitchAmountMarkupPercentage / 100))
: this.srvModel.btcDue;
},
scanDisplayQr: function() {
if (this.srvModel.isLightning && this.currentLightningDisplay === "node") {
return this.srvModel.peerInfo;
}
return this.srvModel.invoiceBitcoinUrlQR;
}
},
methods: {
toggleLightningData: function(display) {
if (!this.srvModel.isLightning) {
return;
}
this.currentLightningDisplay = display;
}
},
mounted: function() {
var self = this;
eventBus.$on("tab-switched",
function(tab) {
self.currentTab = tab;
});
}
});
Vue.component('BitcoinLightningLikeMethodCheckoutHeader', {
props: ["srvModel"],
template: "#bitcoin-lightning-method-checkout-header-template",
data: function() {
return {
currentTab: "scan"
};
},
methods: {
switchTab: function(tab) {
this.currentTab = tab;
eventBus.$emit("tab-switched", tab);
}
}
});
$(document).ready(function() {
// Clipboard Copy
var copySpan = new Clipboard('._copySpan', {
target: function(trigger) {
return copyElement(trigger, 0, 65).firstChild;
}
});
var copyInput = new Clipboard('._copyInput', {
target: function(trigger) {
return copyElement(trigger, 4, 65).firstChild;
}
});
function copyElement(trigger, popupLeftModifier, popupTopModifier) {
var elm = $(trigger);
var position = elm.offset();
position.top -= popupLeftModifier;
position.left += (elm.width() / 2) - popupTopModifier;
$(".copyLabelPopup").css(position).addClass("copied");
elm.removeClass("copy-cursor").addClass("clipboardCopied");
setTimeout(clearSelection, 100);
setTimeout(function() {
elm.removeClass("clipboardCopied").addClass("copy-cursor");
$(".copyLabelPopup").removeClass("copied");
},
1000);
return trigger;
}
function clearSelection() {
if (window.getSelection) {
window.getSelection().removeAllRanges();
} else if (document.selection) {
document.selection.empty();
}
}
// Disable enter key
$(document).keypress(
function(event) {
if (event.which === '13') {
event.preventDefault();
}
}
);
});
</script>

View file

@ -0,0 +1,12 @@
@model PaymentModel
<div>
<p>To complete payment, please send <b>@Model.BtcDue @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
<p>Time remaining: @Model.TimeLeft</p>
<p>
<a href="@Model.InvoiceBitcoinUrl" style="word-break: break-word;">@Model.InvoiceBitcoinUrl</a>
</p>
@if (Model.IsLightning)
{
<p>Peer Info: <b>@Model.PeerInfo</b></p>
}
</div>

View file

@ -1,443 +0,0 @@
// TODO: Refactor... switch from jQuery to Vue.js
// public methods
function resetTabsSlider() {
// Scan/Copy Transitions
// Scan Tab
$("#scan-tab").off().on("click", function () {
resetTabsSlider();
activateTab("#scan");
});
// Copy tab
$("#copy-tab").off().on("click", function () {
resetTabsSlider();
activateTab("#copy");
$("#tabsSlider").addClass("slide-copy");
});
// Altcoins tab
$("#altcoins-tab").off().on("click", function () {
resetTabsSlider();
activateTab("#altcoins");
$("#tabsSlider").addClass("slide-altcoins");
});
$("#tabsSlider").removeClass("slide-copy");
$("#tabsSlider").removeClass("slide-altcoins");
$("#scan-tab").removeClass("active");
$("#copy-tab").removeClass("active");
$("#altcoins-tab").removeClass("active");
$("#copy").hide();
$("#copy").removeClass("active");
$("#scan").hide();
$("#scan").removeClass("active");
$("#altcoins").hide();
$("#altcoins").removeClass("active");
closePaymentMethodDialog(null);
}
function changeCurrency(currency) {
if (currency !== null && srvModel.paymentMethodId !== currency) {
$(".payment__currencies").hide();
$(".payment__spinner").show();
checkoutCtrl.scanDisplayQr = "";
srvModel.paymentMethodId = currency;
fetchStatus();
setTimeout(function(){
resetTabsSlider();
if ($("#tabsSlider").hasClass("slide-copy")) {
activateTab("#copy");
} else if ($("#tabsSlider").hasClass("slide-altcoins")) {
activateTab("#altcoins");
} else {
activateTab("#scan");
}
},50);
}
return false;
}
function onDataCallback(jsonData) {
var newStatus = jsonData.status;
if (newStatus === "complete" ||
newStatus === "confirmed" ||
newStatus === "paid") {
if ($(".modal-dialog").hasClass("expired")) {
$(".modal-dialog").removeClass("expired");
$("#expired").removeClass("active");
}
$(".modal-dialog").addClass("paid");
resetTabsSlider();
$("#paid").addClass("active");
if (!jsonData.isModal && jsonData.redirectAutomatically && jsonData.merchantRefLink) {
$(".payment__spinner").show();
setTimeout(function () {
window.location = jsonData.merchantRefLink;
}, 2000);
}
}
if (newStatus === "expired" || newStatus === "invalid") { //TODO: different state if the invoice is invalid (failed to confirm after timeout)
if ($(".modal-dialog").hasClass("paid")) {
$(".modal-dialog").removeClass("paid");
$("#paid").removeClass("active");
}
$(".timer-row").removeClass("expiring-soon");
$(".timer-row__spinner").html("");
$("#emailAddressView").removeClass("active");
$(".modal-dialog").addClass("expired");
$("#expired").addClass("active");
resetTabsSlider();
}
if (checkoutCtrl.srvModel.status !== newStatus) {
window.parent.postMessage({ "invoiceId": srvModel.invoiceId, "status": newStatus }, "*");
}
// restoring qr code view only when currency is switched
if (jsonData.paymentMethodId === srvModel.paymentMethodId &&
checkoutCtrl.scanDisplayQr === "") {
checkoutCtrl.scanDisplayQr = jsonData.invoiceBitcoinUrlQR;
}
if (jsonData.paymentMethodId === srvModel.paymentMethodId) {
$(".payment__currencies").show();
$(".payment__spinner").hide();
}
if (jsonData.isLightning && checkoutCtrl.lndModel === null) {
var lndModel = {
toggle: 0
};
checkoutCtrl.lndModel = lndModel;
}
if (!jsonData.isLightning) {
checkoutCtrl.lndModel = null;
}
// displaying satoshis for lightning payments
jsonData.cryptoCodeSrv = jsonData.cryptoCode;
if (jsonData.isLightning && checkoutCtrl.lightningAmountInSatoshi && jsonData.cryptoCode === "BTC") {
var SATOSHIME = 100000000;
jsonData.cryptoCode = "Sats";
jsonData.btcDue = numberFormatted(jsonData.btcDue * SATOSHIME);
jsonData.btcPaid = numberFormatted(jsonData.btcPaid * SATOSHIME);
jsonData.networkFee = numberFormatted(jsonData.networkFee * SATOSHIME);
jsonData.orderAmount = numberFormatted(jsonData.orderAmount * SATOSHIME);
}
// expand line items to show details on amount due for multi-transaction payment
if (checkoutCtrl.srvModel.txCount === 1 && jsonData.txCount > 1) {
onlyExpandLineItems();
}
// updating ui
checkoutCtrl.srvModel = jsonData;
}
function numberFormatted(x) {
var rounded = Math.round(x);
var parts = rounded.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " ");
return parts.join(".");
}
function fetchStatus() {
$.ajax({
url: window.location.pathname + "/status?invoiceId=" + srvModel.invoiceId + "&paymentMethodId=" + srvModel.paymentMethodId,
type: "GET",
cache: false
}).done(function (data) {
onDataCallback(data);
}).fail(function (jqXHR, textStatus, errorThrown) {
});
}
function lndToggleBolt11() {
checkoutCtrl.lndModel.toggle = 0;
checkoutCtrl.scanDisplayQr = checkoutCtrl.srvModel.invoiceBitcoinUrlQR;
}
function lndToggleNode() {
checkoutCtrl.lndModel.toggle = 1;
checkoutCtrl.scanDisplayQr = checkoutCtrl.srvModel.peerInfo;
}
var lineItemsExpanded = false;
function toggleLineItems() {
$("line-items").toggleClass("expanded");
lineItemsExpanded ? $("line-items").slideUp() : $("line-items").slideDown();
lineItemsExpanded = !lineItemsExpanded;
$(".buyerTotalLine").toggleClass("expanded");
$(".single-item-order__right__btc-price__chevron").toggleClass("expanded");
}
function onlyExpandLineItems() {
if (!lineItemsExpanded) {
toggleLineItems();
}
}
function activateTab(senderName) {
$(senderName + "-tab").addClass("active");
$(senderName).show();
$(senderName).addClass("active");
}
// private methods
$(document).ready(function () {
// initialize
onDataCallback(srvModel);
// initial expand of line items to show error message if multiple payments needed
if (srvModel.status === "new" && srvModel.txCount > 1) {
onlyExpandLineItems();
}
// check if the Document expired
if (srvModel.expirationSeconds > 0) {
progressStart(srvModel.maxTimeSeconds); // Progress bar
if (srvModel.requiresRefundEmail && !validateEmail(srvModel.customerEmail))
showEmailForm();
else
hideEmailForm();
}
$(".close-action").on("click", function () {
$("invoice").fadeOut(300, function () {
window.parent.postMessage("close", "*");
});
});
window.parent.postMessage("loaded", "*");
jQuery("invoice").fadeOut(0);
jQuery("invoice").fadeIn(300);
// eof initialize
// Expand Line-Items
$(".buyerTotalLine").click(function () {
toggleLineItems();
});
// FUNCTIONS
function hideEmailForm() {
$("#emailAddressView").removeClass("active");
$("placeholder-refundEmail").html(srvModel.customerEmail);
// Remove Email mode
$(".modal-dialog").removeClass("enter-purchaser-email");
$("#scan").addClass("active");
}
// Email Form
// Setup Email mode
function showEmailForm() {
$(".modal-dialog").addClass("enter-purchaser-email");
$("#emailAddressForm .action-button").click(function () {
var emailAddress = $("#emailAddressFormInput").val();
if (validateEmail(emailAddress)) {
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").addClass("loading");
// Push the email to a server, once the reception is confirmed move on
srvModel.customerEmail = emailAddress;
$.ajax({
url: window.location.pathname + "/UpdateCustomer?invoiceId=" + srvModel.invoiceId,
type: "POST",
data: JSON.stringify({ Email: srvModel.customerEmail }),
contentType: "application/json; charset=utf-8"
}).done(function () {
hideEmailForm();
}).fail(function (jqXHR, textStatus, errorThrown) {
})
.always(function () {
$("#emailAddressForm .input-wrapper bp-loading-button .action-button").removeClass("loading");
});
} else {
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
}
return false;
});
}
/* =============== Even listeners =============== */
// Email
$("#emailAddressFormInput").change(function () {
if ($("#emailAddressForm").hasClass("ng-submitted")) {
$("#emailAddressForm").removeClass("ng-submitted");
}
});
// Payment received
// Should connect using webhook ?
// If notification received
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=" + srvModel.invoiceId;
try {
socket = new WebSocket(ws_uri);
socket.onmessage = function (e) {
fetchStatus();
};
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 watcher = setInterval(function () {
if (socket === null || socket.readyState !== 1) {
fetchStatus();
}
}, 2000);
$(".menu__item").click(function () {
$(".menu__scroll .menu__item").removeClass("selected");
$(this).addClass("selected");
language();
$(".selector span").text($(".selected").text());
// function to load contents in different language should go there
});
// Timer Countdown && Progress bar
function progressStart(timerMax) {
var end = new Date(); // Setup Time Variable, should come from server
end.setSeconds(end.getSeconds() + srvModel.expirationSeconds);
timerMax *= 1000; // Usually 15 minutes = 9000 second= 900000 ms
animateUpdate(); //Launch it
function animateUpdate() {
var now = new Date();
var timeDiff = end.getTime() - now.getTime();
var perc = 100 - Math.round(timeDiff / timerMax * 100);
var status = checkoutCtrl.srvModel.status;
updateTimer(timeDiff / 1000);
if (perc === 75 && (status === "paidPartial" || status === "new")) {
$(".timer-row").addClass("expiring-soon");
checkoutCtrl.expiringSoon = true;
updateProgress(perc);
}
if (perc <= 100) {
updateProgress(perc);
var timeoutVal = 300; // Timeout calc
setTimeout(animateUpdate, timeoutVal);
}
//if (perc >= 100 && status === "expired") {
// onDataCallback(status);
//}
}
function updateProgress(percentage) {
$('.timer-row__progress-bar').css("width", percentage + "%");
}
function updateTimer(timer) {
var display = $(".timer-row__time-left");
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;
display.text(minutes + ":" + seconds);
} else {
display.text("00:00");
}
}
}
// Clipboard Copy
var copySpan = new Clipboard('._copySpan', {
target: function (trigger) {
return copyElement(trigger, 0, 65).firstChild;
}
});
var copyInput = new Clipboard('._copyInput', {
target: function (trigger) {
return copyElement(trigger, 4, 65).firstChild;
}
});
function copyElement(trigger, popupLeftModifier, popupTopModifier) {
var elm = $(trigger);
var position = elm.offset();
position.top -= popupLeftModifier;
position.left += (elm.width() / 2) - popupTopModifier;
$(".copyLabelPopup").css(position).addClass("copied");
elm.removeClass("copy-cursor").addClass("clipboardCopied");
setTimeout(clearSelection, 100);
setTimeout(function () {
elm.removeClass("clipboardCopied").addClass("copy-cursor");
$(".copyLabelPopup").removeClass("copied");
}, 1000);
return trigger;
}
function clearSelection() {
if (window.getSelection) { window.getSelection().removeAllRanges(); }
else if (document.selection) { document.selection.empty(); }
}
// EOF Copy
// Disable enter key
$(document).keypress(
function (event) {
if (event.which === '13') {
event.preventDefault();
}
}
);
resetTabsSlider();
});
// Validate Email address
function validateEmail(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);
}

View file

@ -1,11 +0,0 @@
function openPaymentMethodDialog() {
var content = $("#vexPopupDialog").html();
vex.open({
unsafeContent: content
});
}
function closePaymentMethodDialog(currencyId) {
vex.closeAll();
return changeCurrency(currencyId);
}