Allow overriding UI of checkout in payment handler (#992)

This commit is contained in:
Andrew Camilleri
2019-08-25 15:50:11 +02:00
committed by Nicolas Dorier
parent 989a7b863e
commit 43ee22f965
7 changed files with 296 additions and 246 deletions

View File

@@ -167,7 +167,6 @@ namespace BTCPayServer.Controllers
return View(model); return View(model);
} }
//TODO: abstract
private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId) private async Task<PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId)
{ {
var invoice = await _InvoiceRepository.GetInvoice(invoiceId); var invoice = await _InvoiceRepository.GetInvoice(invoiceId);
@@ -297,6 +296,7 @@ namespace BTCPayServer.Controllers
}; };
paymentMethodHandler.PreparePaymentModel(model, dto); paymentMethodHandler.PreparePaymentModel(model, dto);
model.UISettings = paymentMethodHandler.GetCheckoutUISettings();
model.PaymentMethodId = paymentMethodId.ToString(); model.PaymentMethodId = paymentMethodId.ToString();
var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds); var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds);
model.TimeLeft = expiration.PrettyPrint(); model.TimeLeft = expiration.PrettyPrint();

View File

@@ -5,8 +5,15 @@ using System.Threading.Tasks;
namespace BTCPayServer.Models.InvoicingModels namespace BTCPayServer.Models.InvoicingModels
{ {
public class CheckoutUIPaymentMethodSettings
{
public string ExtensionPartial { get; set; }
public string CheckoutBodyVueComponentName { get; set; }
public string NoScriptPartialName { get; set; }
}
public class PaymentModel public class PaymentModel
{ {
public CheckoutUIPaymentMethodSettings UISettings;
public class AvailableCrypto public class AvailableCrypto
{ {
public string PaymentMethodId { get; set; } public string PaymentMethodId { get; set; }

View File

@@ -46,6 +46,7 @@ namespace BTCPayServer.Payments
Money amount, PaymentMethodId paymentMethodId); Money amount, PaymentMethodId paymentMethodId);
IEnumerable<PaymentMethodId> GetSupportedPaymentMethods(); IEnumerable<PaymentMethodId> GetSupportedPaymentMethods();
CheckoutUIPaymentMethodSettings GetCheckoutUISettings();
} }
public interface IPaymentMethodHandler<TSupportedPaymentMethod, TBTCPayNetwork> : IPaymentMethodHandler public interface IPaymentMethodHandler<TSupportedPaymentMethod, TBTCPayNetwork> : IPaymentMethodHandler
@@ -93,6 +94,11 @@ namespace BTCPayServer.Payments
throw new NotSupportedException("Invalid supportedPaymentMethod"); throw new NotSupportedException("Invalid supportedPaymentMethod");
} }
public virtual CheckoutUIPaymentMethodSettings GetCheckoutUISettings()
{
return null;
}
object IPaymentMethodHandler.PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store, object IPaymentMethodHandler.PreparePayment(ISupportedPaymentMethod supportedPaymentMethod, StoreData store,
BTCPayNetworkBase network) BTCPayNetworkBase network)
{ {

View File

@@ -99,12 +99,10 @@
<div class="single-item-order__right__btc-price" v-else> <div class="single-item-order__right__btc-price" v-else>
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span> <span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
</div> </div>
<div class="single-item-order__right__ex-rate" v-if="srvModel.orderAmountFiat && srvModel.cryptoCode">
<div class="single-item-order__right__ex-rate" v-if="srvModel.orderAmountFiat">
1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }} 1 {{ srvModel.cryptoCodeSrv }} = {{ srvModel.rate }}
</div> </div>
</div> </div>
<span class="fa fa-angle-double-down"></span> <span class="fa fa-angle-double-down"></span>
<span class="fa fa-angle-double-up"></span> <span class="fa fa-angle-double-up"></span>
</div> </div>
@@ -128,7 +126,7 @@
<span>{{$t("Network Cost")}}</span> <span>{{$t("Network Cost")}}</span>
</div> </div>
<div class="line-items__item__value"> <div class="line-items__item__value">
<span v-if="srvModel.IsMultiCurrency"> <span v-if="srvModel.isMultiCurrency">
{{ srvModel.networkFee }} {{ srvModel.cryptoCode }} {{ srvModel.networkFee }} {{ srvModel.cryptoCode }}
</span> </span>
<span v-else> <span v-else>
@@ -148,7 +146,7 @@
</div> </div>
</div> </div>
</line-items> </line-items>
<div class="payment-tabs"> <div class="payment-tabs" v-if="!srvModel.uiSettings || !srvModel.uiSettings.checkoutBodyVueComponentName">
<div class="payment-tabs__tab active" id="scan-tab"> <div class="payment-tabs__tab active" id="scan-tab">
<span>{{$t("Scan")}}</span> <span>{{$t("Scan")}}</span>
</div> </div>
@@ -194,6 +192,14 @@
</div> </div>
</form> </form>
</div> </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="bp-view payment scan" id="scan">
<div class="wrapBtnGroup" v-bind:class="{ invisible: lndModel === null || !scanDisplayQr }"> <div class="wrapBtnGroup" v-bind:class="{ invisible: lndModel === null || !scanDisplayQr }">
<div class="btnGroupLnd"> <div class="btnGroupLnd">
@@ -326,15 +332,14 @@
v-on:load="onLoadIframe" v-on:load="onLoadIframe"
style="height: 100%; position: fixed; top: 0; width: 100%; left: 0;" style="height: 100%; position: fixed; top: 0; width: 100%; left: 0;"
sandbox="allow-scripts allow-forms allow-popups allow-same-origin" sandbox="allow-scripts allow-forms allow-popups allow-same-origin"
:src="url"></iframe> :src="url">
</iframe>
</div> </div>
</coinswitch> </coinswitch>
} }
@if(Model.ChangellyEnabled){ @if (Model.ChangellyEnabled)
{
<changelly inline-template <changelly inline-template
v-if="!srvModel.coinSwitchEnabled || selectedThirdPartyProcessor === 'changelly'" v-if="!srvModel.coinSwitchEnabled || selectedThirdPartyProcessor === 'changelly'"
:merchant-id="srvModel.changellyMerchantId" :merchant-id="srvModel.changellyMerchantId"
@@ -382,6 +387,8 @@
</nav> </nav>
</div> </div>
} }
</template>
</div>
<div class="bp-view" id="paid"> <div class="bp-view" id="paid">
<div class="status-block"> <div class="status-block">

View File

@@ -1,7 +1,6 @@
@addTagHelper *, BundlerMinifier.TagHelpers @addTagHelper *, BundlerMinifier.TagHelpers
@inject BTCPayServer.Services.LanguageService langService @inject BTCPayServer.Services.LanguageService langService
@using Newtonsoft.Json @inject PaymentMethodHandlerDictionary PaymentMethodHandlerDictionary
@using Newtonsoft.Json.Linq
@model PaymentModel @model PaymentModel
@{ @{
Layout = null; Layout = null;
@@ -152,7 +151,7 @@
return availableLanguages.indexOf(languageCode) >= 0; return availableLanguages.indexOf(languageCode) >= 0;
} }
const i18n = new VueI18next(i18next); var i18n = new VueI18next(i18next);
// TODO: Move all logic from core.js to Vue controller // TODO: Move all logic from core.js to Vue controller
Vue.config.ignoredElements = [ Vue.config.ignoredElements = [
@@ -161,7 +160,6 @@
// Ignoring custom HTML5 elements, eg: bp-spinner // Ignoring custom HTML5 elements, eg: bp-spinner
/^bp-/ /^bp-/
]; ];
var checkoutCtrl = new Vue({ var checkoutCtrl = new Vue({
i18n: i18n, i18n: i18n,
el: '#checkoutCtrl', el: '#checkoutCtrl',
@@ -184,9 +182,18 @@
return this.srvModel.coinSwitchAmountMarkupPercentage return this.srvModel.coinSwitchAmountMarkupPercentage
? this.srvModel.btcDue * (1 + (this.srvModel.coinSwitchAmountMarkupPercentage / 100)) ? this.srvModel.btcDue * (1 + (this.srvModel.coinSwitchAmountMarkupPercentage / 100))
: this.srvModel.btcDue; : this.srvModel.btcDue;
},
showPaymentUI: function(){
var disallowedStatuses = ["complete","confirmed" ,"paid", "expired", "invalid"];
return (!this.srvModel.requiresRefundEmail || validateEmail(srvModel.customerEmail)) && disallowedStatuses.indexOf(this.srvModel.status) < 0;
} }
} }
}); });
</script> </script>
@foreach (var paymentMethodHandler in PaymentMethodHandlerDictionary.Select(handler => handler.GetCheckoutUISettings()).Where(settings => settings != null))
{
<partial name="@paymentMethodHandler.ExtensionPartial" model="@Model"/>
}
</body> </body>
</html> </html>

View File

@@ -15,6 +15,12 @@
<body> <body>
<h1>Pay with @Model.StoreName</h1> <h1>Pay with @Model.StoreName</h1>
@if (Model.Status == "new") @if (Model.Status == "new")
{
if (!string.IsNullOrEmpty(Model.UISettings?.NoScriptPartialName))
{
<partial model="@Model" name="@Model.UISettings.NoScriptPartialName"/>
}
else
{ {
<div> <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>To complete payment, please send <b>@Model.BtcDue @Model.CryptoCode</b> to <b style="word-break: break-word;">@Model.BtcAddress</b></p>
@@ -25,7 +31,7 @@
<p>Peer Info: <b>@Model.PeerInfo</b></p> <p>Peer Info: <b>@Model.PeerInfo</b></p>
} }
</div> </div>
}
@if (Model.AvailableCryptos.Count > 1) @if (Model.AvailableCryptos.Count > 1)
{ {
<div> <div>

View File

@@ -1,6 +1,29 @@
// TODO: Refactor... switch from jQuery to Vue.js // TODO: Refactor... switch from jQuery to Vue.js
// public methods // public methods
function resetTabsSlider() { 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-copy");
$("#tabsSlider").removeClass("slide-altcoins"); $("#tabsSlider").removeClass("slide-altcoins");
@@ -27,6 +50,17 @@ function changeCurrency(currency) {
checkoutCtrl.scanDisplayQr = ""; checkoutCtrl.scanDisplayQr = "";
srvModel.paymentMethodId = currency; srvModel.paymentMethodId = currency;
fetchStatus(); 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; return false;
} }
@@ -162,6 +196,14 @@ function onlyExpandLineItems() {
} }
function activateTab(senderName) {
$(senderName + "-tab").addClass("active");
$(senderName).show();
$(senderName).addClass("active");
}
// private methods // private methods
$(document).ready(function () { $(document).ready(function () {
// initialize // initialize
@@ -239,11 +281,7 @@ $(document).ready(function () {
}); });
} }
// 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);
}
/* =============== Even listeners =============== */ /* =============== Even listeners =============== */
@@ -255,35 +293,8 @@ $(document).ready(function () {
}); });
// Scan/Copy Transitions
// Scan Tab
$("#scan-tab").click(function () {
resetTabsSlider();
activateTab("#scan");
});
// Copy tab
$("#copy-tab").click(function () {
resetTabsSlider();
activateTab("#copy");
$("#tabsSlider").addClass("slide-copy");
});
// Altcoins tab
$("#altcoins-tab").click(function () {
resetTabsSlider();
activateTab("#altcoins");
$("#tabsSlider").addClass("slide-altcoins");
});
function activateTab(senderName) {
$(senderName + "-tab").addClass("active");
$(senderName).show();
$(senderName).addClass("active");
}
// Payment received // Payment received
// Should connect using webhook ? // Should connect using webhook ?
@@ -423,3 +434,9 @@ $(document).ready(function () {
); );
}); });
// 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);
}