mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Split Bitcoin/LN to partial defined in payment handler and convert checkout to Vue (#996)
This commit is contained in:
parent
55c0c0ea6f
commit
9a9e31c759
10 changed files with 596 additions and 766 deletions
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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">
|
||||
✖
|
||||
</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">⚡</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("⚡") : 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">⚡</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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
function openPaymentMethodDialog() {
|
||||
var content = $("#vexPopupDialog").html();
|
||||
vex.open({
|
||||
unsafeContent: content
|
||||
});
|
||||
}
|
||||
|
||||
function closePaymentMethodDialog(currencyId) {
|
||||
vex.closeAll();
|
||||
return changeCurrency(currencyId);
|
||||
}
|
Loading…
Add table
Reference in a new issue