Add ability to set default payment method for pay button (#3606)

* Add ability to set default payment method for pay button

close #3604

* Add "#nullable enable" to UIStoresController

* Add PaymentMethodOptionViewModel

* Add explicit "Use the store’s default" option
This commit is contained in:
Umar Bolatov
2022-04-11 01:48:12 -07:00
committed by GitHub
parent 6bd7fb64ab
commit 8feb60c30d
8 changed files with 77 additions and 31 deletions

View File

@@ -67,7 +67,8 @@ namespace BTCPayServer.Controllers
NotificationEmail = model.NotifyEmail, NotificationEmail = model.NotifyEmail,
NotificationURL = model.ServerIpn, NotificationURL = model.ServerIpn,
RedirectURL = model.BrowserRedirect, RedirectURL = model.BrowserRedirect,
FullNotifications = true FullNotifications = true,
DefaultPaymentMethod = model.DefaultPaymentMethod
}, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken); }, store, HttpContext.Request.GetAbsoluteRoot(), cancellationToken: cancellationToken);
} }
catch (BitpayHttpException e) catch (BitpayHttpException e)

View File

@@ -1,3 +1,4 @@
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
@@ -229,7 +230,7 @@ namespace BTCPayServer.Controllers
} }
[HttpPost("{storeId}/rates")] [HttpPost("{storeId}/rates")]
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default) public async Task<IActionResult> Rates(RatesViewModel model, string? command = null, string? storeId = null, CancellationToken cancellationToken = default)
{ {
if (command == "scripting-on") if (command == "scripting-on")
{ {
@@ -243,7 +244,7 @@ namespace BTCPayServer.Controllers
var exchanges = GetSupportedExchanges(); var exchanges = GetSupportedExchanges();
model.SetExchangeRates(exchanges, model.PreferredExchange); model.SetExchangeRates(exchanges, model.PreferredExchange);
model.StoreId = storeId ?? model.StoreId; model.StoreId = storeId ?? model.StoreId;
CurrencyPair[] currencyPairs = null; CurrencyPair[]? currencyPairs = null;
try try
{ {
currencyPairs = model.DefaultCurrencyPairs? currencyPairs = model.DefaultCurrencyPairs?
@@ -277,7 +278,7 @@ namespace BTCPayServer.Controllers
return View(model); return View(model);
} }
} }
RateRules rules = null; RateRules? rules = null;
if (model.ShowScripting) if (model.ShowScripting)
{ {
if (!RateRules.TryParse(model.Script, out rules, out var errors)) if (!RateRules.TryParse(model.Script, out rules, out var errors))
@@ -421,6 +422,29 @@ namespace BTCPayServer.Controllers
} }
void SetCryptoCurrencies(CheckoutAppearanceViewModel vm, Data.StoreData storeData) void SetCryptoCurrencies(CheckoutAppearanceViewModel vm, Data.StoreData storeData)
{
var choices = GetEnabledPaymentMethodChoices(storeData);
var chosen = GetDefaultPaymentMethodChoice(storeData);
vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
vm.DefaultPaymentMethod = chosen?.Value;
}
PaymentMethodOptionViewModel.Format[] GetEnabledPaymentMethodChoices(Data.StoreData storeData)
{
var enabled = storeData.GetEnabledPaymentIds(_NetworkProvider);
return enabled
.Select(o =>
new PaymentMethodOptionViewModel.Format()
{
Name = o.ToPrettyString(),
Value = o.ToString(),
PaymentId = o
}).ToArray();
}
PaymentMethodOptionViewModel.Format? GetDefaultPaymentMethodChoice(Data.StoreData storeData)
{ {
var enabled = storeData.GetEnabledPaymentIds(_NetworkProvider); var enabled = storeData.GetEnabledPaymentIds(_NetworkProvider);
var defaultPaymentId = storeData.GetDefaultPaymentId(); var defaultPaymentId = storeData.GetDefaultPaymentId();
@@ -431,17 +455,9 @@ namespace BTCPayServer.Controllers
enabled.FirstOrDefault(e => e.CryptoCode == "BTC" && e.PaymentType == PaymentTypes.LightningLike) ?? enabled.FirstOrDefault(e => e.CryptoCode == "BTC" && e.PaymentType == PaymentTypes.LightningLike) ??
enabled.FirstOrDefault(); enabled.FirstOrDefault();
} }
var choices = enabled var choices = GetEnabledPaymentMethodChoices(storeData);
.Select(o =>
new CheckoutAppearanceViewModel.Format() return defaultChoice is null ? null : choices.FirstOrDefault(c => defaultChoice.ToString().Equals(c.Value, StringComparison.OrdinalIgnoreCase));
{
Name = o.ToPrettyString(),
Value = o.ToString(),
PaymentId = o
}).ToArray();
var chosen = defaultChoice is null ? null : choices.FirstOrDefault(c => defaultChoice.ToString().Equals(c.Value, StringComparison.OrdinalIgnoreCase));
vm.PaymentMethods = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen?.Value);
vm.DefaultPaymentMethod = chosen?.Value;
} }
[HttpPost] [HttpPost]
@@ -616,7 +632,7 @@ namespace BTCPayServer.Controllers
} }
[HttpPost("{storeId}/settings")] [HttpPost("{storeId}/settings")]
public async Task<IActionResult> GeneralSettings(GeneralSettingsViewModel model, string command = null) public async Task<IActionResult> GeneralSettings(GeneralSettingsViewModel model, string? command = null)
{ {
bool needUpdate = false; bool needUpdate = false;
if (CurrentStore.StoreName != model.StoreName) if (CurrentStore.StoreName != model.StoreName)
@@ -747,7 +763,7 @@ namespace BTCPayServer.Controllers
TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token."; TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token.";
else else
TempData[WellKnownTempData.SuccessMessage] = "Token revoked"; TempData[WellKnownTempData.SuccessMessage] = "Token revoked";
return RedirectToAction(nameof(ListTokens), new { storeId = token.StoreId }); return RedirectToAction(nameof(ListTokens), new { storeId = token?.StoreId });
} }
[HttpGet] [HttpGet]
@@ -782,7 +798,7 @@ namespace BTCPayServer.Controllers
Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey).Compress()) Id = model.PublicKey == null ? null : NBitpayClient.Extensions.BitIdExtensions.GetBitIDSIN(new PubKey(model.PublicKey).Compress())
}; };
string pairingCode = null; string? pairingCode = null;
if (model.PublicKey == null) if (model.PublicKey == null)
{ {
tokenRequest.PairingCode = await _TokenRepository.CreatePairingCodeAsync(); tokenRequest.PairingCode = await _TokenRepository.CreatePairingCodeAsync();
@@ -807,7 +823,7 @@ namespace BTCPayServer.Controllers
}); });
} }
public string GeneratedPairingCode { get; set; } public string? GeneratedPairingCode { get; set; }
public WebhookSender WebhookNotificationManager { get; } public WebhookSender WebhookNotificationManager { get; }
public IDataProtector DataProtector { get; } public IDataProtector DataProtector { get; }
@@ -880,7 +896,7 @@ namespace BTCPayServer.Controllers
[HttpGet] [HttpGet]
[Route("/api-access-request")] [Route("/api-access-request")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null) public async Task<IActionResult> RequestPairing(string pairingCode, string? selectedStore = null)
{ {
var userId = GetUserId(); var userId = GetUserId();
if (userId == null) if (userId == null)
@@ -956,9 +972,9 @@ namespace BTCPayServer.Controllers
} }
} }
private string GetUserId() private string? GetUserId()
{ {
if (User.Identity.AuthenticationType != AuthenticationSchemes.Cookie) if (User.Identity?.AuthenticationType != AuthenticationSchemes.Cookie)
return null; return null;
return _UserManager.GetUserId(User); return _UserManager.GetUserId(User);
} }
@@ -990,6 +1006,8 @@ namespace BTCPayServer.Controllers
{ {
Price = null, Price = null,
Currency = storeBlob.DefaultCurrency, Currency = storeBlob.DefaultCurrency,
DefaultPaymentMethod = String.Empty,
PaymentMethods = GetEnabledPaymentMethodChoices(store),
ButtonSize = 2, ButtonSize = 2,
UrlRoot = appUrl, UrlRoot = appUrl,
PayButtonImageUrl = appUrl + "img/paybutton/pay.svg", PayButtonImageUrl = appUrl + "img/paybutton/pay.svg",

View File

@@ -38,7 +38,7 @@ namespace BTCPayServer.Data
return paymentMethodIds; return paymentMethodIds;
} }
public static void SetDefaultPaymentId(this StoreData storeData, PaymentMethodId defaultPaymentId) public static void SetDefaultPaymentId(this StoreData storeData, PaymentMethodId? defaultPaymentId)
{ {
storeData.DefaultCrypto = defaultPaymentId?.ToString(); storeData.DefaultCrypto = defaultPaymentId?.ToString();
} }

View File

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using BTCPayServer.Payments;
using BTCPayServer.Services; using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
@@ -10,18 +9,12 @@ namespace BTCPayServer.Models.StoreViewModels
{ {
public class CheckoutAppearanceViewModel public class CheckoutAppearanceViewModel
{ {
public class Format
{
public string Name { get; set; }
public string Value { get; set; }
public PaymentMethodId PaymentId { get; set; }
}
public SelectList PaymentMethods { get; set; } public SelectList PaymentMethods { get; set; }
public void SetLanguages(LanguageService langService, string defaultLang) public void SetLanguages(LanguageService langService, string defaultLang)
{ {
defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang) ? defaultLang : "en"; defaultLang = langService.GetLanguages().Any(language => language.Code == defaultLang) ? defaultLang : "en";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray().OrderBy(o => o.Name); var choices = langService.GetLanguages().Select(o => new PaymentMethodOptionViewModel.Format() { Name = o.DisplayName, Value = o.Code }).ToArray().OrderBy(o => o.Name);
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault(); var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen); Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultLang = chosen.Value; DefaultLang = chosen.Value;

View File

@@ -13,6 +13,8 @@ namespace BTCPayServer.Models.StoreViewModels
public string InvoiceId { get; set; } public string InvoiceId { get; set; }
[Required] [Required]
public string Currency { get; set; } public string Currency { get; set; }
public string DefaultPaymentMethod { get; set; }
public PaymentMethodOptionViewModel.Format[] PaymentMethods { get; set; }
public string CheckoutDesc { get; set; } public string CheckoutDesc { get; set; }
public string OrderId { get; set; } public string OrderId { get; set; }
public int ButtonSize { get; set; } public int ButtonSize { get; set; }

View File

@@ -0,0 +1,14 @@
using BTCPayServer.Payments;
namespace BTCPayServer.Models.StoreViewModels
{
public class PaymentMethodOptionViewModel
{
public class Format
{
public string Name { get; set; }
public string Value { get; set; }
public PaymentMethodId PaymentId { get; set; }
}
}
}

View File

@@ -159,6 +159,13 @@
v-model="srvModel.currency" v-on:change="inputChanges" v-model="srvModel.currency" v-on:change="inputChanges"
:class="{'is-invalid': errors.has('currency') }"> :class="{'is-invalid': errors.has('currency') }">
</div> </div>
<div class="form-group col-md-4" v-if="!srvModel.appIdEndpoint">
<label class="form-label" for="defaultPaymentMethod">Default Payment Method</label>
<select v-model="srvModel.defaultPaymentMethod" v-on:change="inputChanges" class="form-select" id="default-payment-method">
<option value="" selected>Use the stores default</option>
<option v-for="pm in srvModel.paymentMethods" v-bind:value="pm.value">{{pm.name}}</option>
</select>
</div>
<div class="form-group" v-if="!srvModel.appIdEndpoint"> <div class="form-group" v-if="!srvModel.appIdEndpoint">
<label class="form-label" for="description">Checkout Description</label> <label class="form-label" for="description">Checkout Description</label>
<input name="checkoutDesc" type="text" class="form-control" id="description" <input name="checkoutDesc" type="text" class="form-control" id="description"

View File

@@ -84,6 +84,7 @@ function inputChanges(event, buttonSize) {
let priceInputName = 'price'; let priceInputName = 'price';
let app = srvModel.appIdEndpoint? srvModel.apps.find(value => value.id === srvModel.appIdEndpoint ): null; let app = srvModel.appIdEndpoint? srvModel.apps.find(value => value.id === srvModel.appIdEndpoint ): null;
let allowCurrencySelection = true; let allowCurrencySelection = true;
let allowDefaultPaymentMethodSelection = true;
if (app) { if (app) {
if (app.appType.toLowerCase() === 'pointofsale') { if (app.appType.toLowerCase() === 'pointofsale') {
actionUrl = `apps/${app.id}/pos`; actionUrl = `apps/${app.id}/pos`;
@@ -97,6 +98,7 @@ function inputChanges(event, buttonSize) {
if (actionUrl !== 'api/v1/invoices') { if (actionUrl !== 'api/v1/invoices') {
priceInputName = 'amount'; priceInputName = 'amount';
allowCurrencySelection = false; allowCurrencySelection = false;
allowDefaultPaymentMethodSelection = false;
srvModel.useModal = false; srvModel.useModal = false;
} }
} }
@@ -151,6 +153,15 @@ function inputChanges(event, buttonSize) {
html += ' </div>\n'; html += ' </div>\n';
} }
if (
allowDefaultPaymentMethodSelection &&
// Only add default payment method to HTML if user explicitly selected it
event && event.target.id === 'default-payment-method' && event.target.value !== ""
)
{
html += addInput("defaultPaymentMethod", srvModel.defaultPaymentMethod)
}
html += srvModel.payButtonText html += srvModel.payButtonText
? `<button type="submit" class="submit" name="submit" style="min-width:${width};min-height:${height};border-radius:4px;border-style:none;background-color:#0f3b21;" title="Pay with BTCPay Server, a Self-Hosted Bitcoin Payment Processor"><span style="color:#fff">${esc(srvModel.payButtonText)}</span>\n` + ? `<button type="submit" class="submit" name="submit" style="min-width:${width};min-height:${height};border-radius:4px;border-style:none;background-color:#0f3b21;" title="Pay with BTCPay Server, a Self-Hosted Bitcoin Payment Processor"><span style="color:#fff">${esc(srvModel.payButtonText)}</span>\n` +
(srvModel.payButtonImageUrl? `<img src="${esc(srvModel.payButtonImageUrl)}" style="height:${parseInt(height.replace('px', ''))}px;display:inline-block;padding:5% 0 5% 5px;vertical-align:middle;">\n` : '') + (srvModel.payButtonImageUrl? `<img src="${esc(srvModel.payButtonImageUrl)}" style="height:${parseInt(height.replace('px', ''))}px;display:inline-block;padding:5% 0 5% 5px;vertical-align:middle;">\n` : '') +