mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
Allow Pay Button to work on Apps
This PR allows you to use the pay button generator to create buttons that target apps. This means that you can generate an invoice that is linked to an item on the POS/Crowdfund (targeting the item is optional). The POS/Crowdfund item amount -> invoice creation amount validation works too so that the user cannot modify the amount of a perk using just html ( fixes #1392 )
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ using BTCPayServer.Rating;
|
|||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
using BTCPayServer.Security.Bitpay;
|
using BTCPayServer.Security.Bitpay;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Rates;
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
@@ -60,7 +61,8 @@ namespace BTCPayServer.Controllers
|
|||||||
SettingsRepository settingsRepository,
|
SettingsRepository settingsRepository,
|
||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
CssThemeManager cssThemeManager)
|
CssThemeManager cssThemeManager,
|
||||||
|
AppService appService)
|
||||||
{
|
{
|
||||||
_RateFactory = rateFactory;
|
_RateFactory = rateFactory;
|
||||||
_Repo = repo;
|
_Repo = repo;
|
||||||
@@ -76,6 +78,7 @@ namespace BTCPayServer.Controllers
|
|||||||
_settingsRepository = settingsRepository;
|
_settingsRepository = settingsRepository;
|
||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
_CssThemeManager = cssThemeManager;
|
_CssThemeManager = cssThemeManager;
|
||||||
|
_appService = appService;
|
||||||
_EventAggregator = eventAggregator;
|
_EventAggregator = eventAggregator;
|
||||||
_NetworkProvider = networkProvider;
|
_NetworkProvider = networkProvider;
|
||||||
_ExplorerProvider = explorerProvider;
|
_ExplorerProvider = explorerProvider;
|
||||||
@@ -103,6 +106,7 @@ namespace BTCPayServer.Controllers
|
|||||||
private readonly SettingsRepository _settingsRepository;
|
private readonly SettingsRepository _settingsRepository;
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
private readonly CssThemeManager _CssThemeManager;
|
private readonly CssThemeManager _CssThemeManager;
|
||||||
|
private readonly AppService _appService;
|
||||||
private readonly EventAggregator _EventAggregator;
|
private readonly EventAggregator _EventAggregator;
|
||||||
|
|
||||||
[TempData]
|
[TempData]
|
||||||
@@ -889,7 +893,7 @@ namespace BTCPayServer.Controllers
|
|||||||
const string DEFAULT_CURRENCY = "USD";
|
const string DEFAULT_CURRENCY = "USD";
|
||||||
|
|
||||||
[Route("{storeId}/paybutton")]
|
[Route("{storeId}/paybutton")]
|
||||||
public IActionResult PayButton()
|
public async Task<IActionResult> PayButton()
|
||||||
{
|
{
|
||||||
var store = CurrentStore;
|
var store = CurrentStore;
|
||||||
|
|
||||||
@@ -899,6 +903,7 @@ namespace BTCPayServer.Controllers
|
|||||||
return View("PayButtonEnable", null);
|
return View("PayButtonEnable", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var apps = await _appService.GetAllApps(_UserManager.GetUserId(User), false, store.Id);
|
||||||
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
|
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
|
||||||
var model = new PayButtonViewModel
|
var model = new PayButtonViewModel
|
||||||
{
|
{
|
||||||
@@ -911,7 +916,8 @@ namespace BTCPayServer.Controllers
|
|||||||
ButtonType = 0,
|
ButtonType = 0,
|
||||||
Min = 1,
|
Min = 1,
|
||||||
Max = 20,
|
Max = 20,
|
||||||
Step = 1
|
Step = 1,
|
||||||
|
Apps = apps
|
||||||
};
|
};
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using BTCPayServer.ModelBinders;
|
using BTCPayServer.ModelBinders;
|
||||||
|
using BTCPayServer.Models.AppViewModels;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.StoreViewModels
|
namespace BTCPayServer.Models.StoreViewModels
|
||||||
@@ -43,5 +44,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
public string PayButtonText { get; set; }
|
public string PayButtonText { get; set; }
|
||||||
public bool UseModal { get; set; }
|
public bool UseModal { get; set; }
|
||||||
public bool JsonResponse { get; set; }
|
public bool JsonResponse { get; set; }
|
||||||
|
public ListAppsViewModel.ListAppViewModel[] Apps { get; set; }
|
||||||
|
public string AppIdEndpoint { get; set; } = "";
|
||||||
|
public string AppChoiceKey { get; set; } = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,12 +222,14 @@ namespace BTCPayServer.Services.Apps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string userId, bool allowNoUser = false)
|
public async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps(string userId, bool allowNoUser = false, string storeId = null)
|
||||||
{
|
{
|
||||||
using (var ctx = _ContextFactory.CreateContext())
|
using (var ctx = _ContextFactory.CreateContext())
|
||||||
{
|
{
|
||||||
return await ctx.UserStore
|
return await ctx.UserStore
|
||||||
.Where(us => (allowNoUser && string.IsNullOrEmpty(userId)) || us.ApplicationUserId == userId)
|
.Where(us =>
|
||||||
|
((allowNoUser && string.IsNullOrEmpty(userId)) || us.ApplicationUserId == userId) &&
|
||||||
|
(storeId == null || us.StoreDataId == storeId))
|
||||||
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
||||||
(us, app) =>
|
(us, app) =>
|
||||||
new ListAppsViewModel.ListAppViewModel()
|
new ListAppsViewModel.ListAppViewModel()
|
||||||
|
|||||||
@@ -16,14 +16,14 @@
|
|||||||
v-validate="'required|decimal|min_value:0'" :class="{'is-invalid': errors.has('price') }">
|
v-validate="'required|decimal|min_value:0'" :class="{'is-invalid': errors.has('price') }">
|
||||||
<small class="text-danger">{{ errors.first('price') }}</small>
|
<small class="text-danger">{{ errors.first('price') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group col-md-4">
|
<div class="form-group col-md-4" v-if="!srvModel.appIdEndpoint">
|
||||||
<label> </label>
|
<label> </label>
|
||||||
<input name="currency" type="text" class="form-control"
|
<input name="currency" type="text" class="form-control"
|
||||||
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>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" v-if="!srvModel.appIdEndpoint">
|
||||||
<label>Checkout Description</label>
|
<label>Checkout Description</label>
|
||||||
<input name="checkoutDesc" type="text" class="form-control" placeholder="(optional)"
|
<input name="checkoutDesc" type="text" class="form-control" placeholder="(optional)"
|
||||||
v-model="srvModel.checkoutDesc" v-on:change="inputChanges">
|
v-model="srvModel.checkoutDesc" v-on:change="inputChanges">
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-7">
|
<div class="col-lg-7">
|
||||||
<div class="form-group">
|
<div class="form-group" v-if="!srvModel.appIdEndpoint">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" v-model="srvModel.useModal" v-on:change="inputChanges" class="form-check-inline"/>Use Modal
|
<input type="checkbox" v-model="srvModel.useModal" v-on:change="inputChanges" class="form-check-inline"/>Use Modal
|
||||||
</label>
|
</label>
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
v-validate="'url'" :class="{'is-invalid': errors.has('serverIpn') }">
|
v-validate="'url'" :class="{'is-invalid': errors.has('serverIpn') }">
|
||||||
<small class="text-danger">{{ errors.first('serverIpn') }}</small>
|
<small class="text-danger">{{ errors.first('serverIpn') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" v-if="!srvModel.appIdEndpoint">
|
||||||
<label>Send Email Notifications to</label>
|
<label>Send Email Notifications to</label>
|
||||||
<input name="notifyEmail" type="text" class="form-control" placeholder="(optional)"
|
<input name="notifyEmail" type="text" class="form-control" placeholder="(optional)"
|
||||||
v-model="srvModel.notifyEmail" v-on:change="inputChanges"
|
v-model="srvModel.notifyEmail" v-on:change="inputChanges"
|
||||||
@@ -193,7 +193,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<h3>Advanced</h3>
|
<h3>Advanced</h3>
|
||||||
<br />
|
<br />
|
||||||
<div class="row">
|
<div class="row" v-if="!srvModel.appIdEndpoint">
|
||||||
<div class="col-lg-7">
|
<div class="col-lg-7">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Checkout Additional Query String</label>
|
<label>Checkout Additional Query String</label>
|
||||||
@@ -209,6 +209,30 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Use App as endpoint</label>
|
||||||
|
<select v-model="srvModel.appIdEndpoint" v-on:change="inputChanges" class="form-control">
|
||||||
|
<option value="">Use default pay button endpoint</option>
|
||||||
|
<option v-for="app in srvModel.apps" v-bind:value="app.id" >{{app.appName}} ({{app.appType}})</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-danger">{{ errors.first('appIdEndpoint') }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" v-if="srvModel.appIdEndpoint">
|
||||||
|
<label>App Item/Perk</label>
|
||||||
|
<input name="appChoiceKey" type="text" class="form-control" placeholder="(optional)"
|
||||||
|
v-model="srvModel.appChoiceKey" v-on:change="inputChanges"
|
||||||
|
:class="{'is-invalid': errors.has('appChoiceKey') }">
|
||||||
|
<small class="text-danger">{{ errors.first('appChoiceKey') }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<br />
|
||||||
|
This allows you to link this pay button to an app instead. Some features are disabled due to the different endpoint capabilities. You can set which perk/item this button should be targeting.
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
@@ -298,7 +322,10 @@
|
|||||||
.btcpay-form--block select {
|
.btcpay-form--block select {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.btcpay-form .btcpay-custom {
|
.btcpay-form .btcpay-custom-container{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.btcpay-custom {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -92,19 +92,49 @@ function inputChanges(event, buttonSize) {
|
|||||||
width = "209px";
|
width = "209px";
|
||||||
height = "57px";
|
height = "57px";
|
||||||
}
|
}
|
||||||
|
var actionUrl = "api/v1/invoices";
|
||||||
|
var priceInputName = "price";
|
||||||
|
var app = srvModel.appIdEndpoint? srvModel.apps.find(value => value.id === srvModel.appIdEndpoint ): null;
|
||||||
|
var allowCurrencySelection = true;
|
||||||
|
if (app) {
|
||||||
|
|
||||||
|
if (app.appType.toLowerCase() == "pointofsale") {
|
||||||
|
actionUrl = "apps/" + app.id + "/pos";
|
||||||
|
} else if (app.appType.toLowerCase() == "crowdfund") {
|
||||||
|
actionUrl = "apps/" + app.id + "/crowdfund";
|
||||||
|
} else {
|
||||||
|
actionUrl = "api/v1/invoices";
|
||||||
|
app = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionUrl != "api/v1/invoices") {
|
||||||
|
priceInputName = "amount";
|
||||||
|
allowCurrencySelection = false;
|
||||||
|
srvModel.useModal = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var html =
|
var html =
|
||||||
//Scripts
|
//Scripts
|
||||||
(srvModel.useModal? getScripts(srvModel) :"") +
|
(srvModel.useModal? getScripts(srvModel) :"") +
|
||||||
// Styles
|
// Styles
|
||||||
getStyles('template-paybutton-styles') + (isSlider ? getStyles('template-slider-styles') : '') +
|
getStyles('template-paybutton-styles') + (isSlider ? getStyles('template-slider-styles') : '') +
|
||||||
// Form
|
// Form
|
||||||
'<form method="POST" '+ ( srvModel.useModal? ' onsubmit="onBTCPayFormSubmit(event);return false" ' : '' )+' action="' + esc(srvModel.urlRoot) + 'api/v1/invoices" class="btcpay-form btcpay-form--' + (srvModel.fitButtonInline ? 'inline' : 'block') +'">\n' +
|
'<form method="POST" '+ ( srvModel.useModal? ' onsubmit="onBTCPayFormSubmit(event);return false" ' : '' )+' action="' + esc(srvModel.urlRoot) + actionUrl + '" class="btcpay-form btcpay-form--' + (srvModel.fitButtonInline ? 'inline' : 'block') +'">\n' +
|
||||||
addInput("storeId", srvModel.storeId);
|
addInput("storeId", srvModel.storeId);
|
||||||
|
|
||||||
|
if(app){
|
||||||
|
if (srvModel.orderId) html += addInput("orderId", srvModel.orderId);
|
||||||
|
if (srvModel.serverIpn) html += addInput("notificationUrl", srvModel.serverIpn);
|
||||||
|
if (srvModel.browserRedirect) html += addInput("redirectUrl", srvModel.browserRedirect);
|
||||||
|
if (srvModel.appChoiceKey) html += addInput("choiceKey", srvModel.appChoiceKey);
|
||||||
|
|
||||||
|
}else{
|
||||||
if (srvModel.useModal) html += addInput("jsonResponse", true);
|
if (srvModel.useModal) html += addInput("jsonResponse", true);
|
||||||
if (srvModel.checkoutDesc) html += addInput("checkoutDesc", srvModel.checkoutDesc);
|
|
||||||
|
|
||||||
if (srvModel.orderId) html += addInput("orderId", srvModel.orderId);
|
if (srvModel.orderId) html += addInput("orderId", srvModel.orderId);
|
||||||
|
if (srvModel.checkoutDesc) html += addInput("checkoutDesc", srvModel.checkoutDesc);
|
||||||
|
|
||||||
|
|
||||||
if (srvModel.serverIpn) html += addInput("serverIpn", srvModel.serverIpn);
|
if (srvModel.serverIpn) html += addInput("serverIpn", srvModel.serverIpn);
|
||||||
|
|
||||||
@@ -113,27 +143,35 @@ function inputChanges(event, buttonSize) {
|
|||||||
if (srvModel.notifyEmail) html += addInput("notifyEmail", srvModel.notifyEmail);
|
if (srvModel.notifyEmail) html += addInput("notifyEmail", srvModel.notifyEmail);
|
||||||
|
|
||||||
if (srvModel.checkoutQueryString) html += addInput("checkoutQueryString", srvModel.checkoutQueryString);
|
if (srvModel.checkoutQueryString) html += addInput("checkoutQueryString", srvModel.checkoutQueryString);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Fixed amount: Add price and currency as hidden inputs
|
// Fixed amount: Add price and currency as hidden inputs
|
||||||
if (isFixedAmount) {
|
if (isFixedAmount) {
|
||||||
html += addInput("price", srvModel.price);
|
html += addInput(priceInputName, srvModel.price);
|
||||||
|
if(allowCurrencySelection){
|
||||||
html += addInput("currency", srvModel.currency);
|
html += addInput("currency", srvModel.currency);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Custom amount
|
// Custom amount
|
||||||
else if (isCustomAmount) {
|
else if (isCustomAmount) {
|
||||||
html += ' <div>\n <div class="btcpay-custom">\n';
|
html += ' <div class="btcpay-custom-container">\n <div class="btcpay-custom">\n';
|
||||||
html += srvModel.simpleInput ? '' : addPlusMinusButton("-");
|
html += srvModel.simpleInput ? '' : addPlusMinusButton("-");
|
||||||
html += ' ' + addInputPrice(srvModel.price, widthInput, "", srvModel.simpleInput ? "number": null, srvModel.min, srvModel.max, srvModel.step);
|
html += ' ' + addInputPrice(priceInputName, srvModel.price, widthInput, "", srvModel.simpleInput ? "number": null, srvModel.min, srvModel.max, srvModel.step);
|
||||||
html += srvModel.simpleInput ? '' : addPlusMinusButton("+");
|
html += srvModel.simpleInput ? '' : addPlusMinusButton("+");
|
||||||
html += ' </div>\n';
|
html += ' </div>\n';
|
||||||
|
if(allowCurrencySelection) {
|
||||||
html += addSelectCurrency(srvModel.currency);
|
html += addSelectCurrency(srvModel.currency);
|
||||||
|
}
|
||||||
html += ' </div>\n';
|
html += ' </div>\n';
|
||||||
}
|
}
|
||||||
// Slider
|
// Slider
|
||||||
else if (isSlider) {
|
else if (isSlider) {
|
||||||
html += ' <div>\n';
|
html += ' <div class="btcpay-custom-container">\n';
|
||||||
html += addInputPrice(srvModel.price, width, 'onchange="document.querySelector(\'#btcpay-input-range\').value = document.querySelector(\'#btcpay-input-price\').value"');
|
html += addInputPrice(priceInputName, srvModel.price, width, 'onchange="document.querySelector(\'#btcpay-input-range\').value = document.querySelector(\'#btcpay-input-price\').value"');
|
||||||
|
if(allowCurrencySelection) {
|
||||||
html += addSelectCurrency(srvModel.currency);
|
html += addSelectCurrency(srvModel.currency);
|
||||||
|
}
|
||||||
html += addSlider(srvModel.price, srvModel.min, srvModel.max, srvModel.step, width);
|
html += addSlider(srvModel.price, srvModel.min, srvModel.max, srvModel.step, width);
|
||||||
html += ' </div>\n';
|
html += ' </div>\n';
|
||||||
}
|
}
|
||||||
@@ -166,8 +204,8 @@ function addPlusMinusButton(type) {
|
|||||||
return ' <button class="plus-minus" onclick="event.preventDefault(); var price = parseInt(document.querySelector(\'#btcpay-input-price\').value); if (\'' + type + '\' == \'-\' && (price - 1) < 1) { return; } document.querySelector(\'#btcpay-input-price\').value = parseInt(document.querySelector(\'#btcpay-input-price\').value) ' + type + ' 1;">' + type + '</button>\n';
|
return ' <button class="plus-minus" onclick="event.preventDefault(); var price = parseInt(document.querySelector(\'#btcpay-input-price\').value); if (\'' + type + '\' == \'-\' && (price - 1) < 1) { return; } document.querySelector(\'#btcpay-input-price\').value = parseInt(document.querySelector(\'#btcpay-input-price\').value) ' + type + ' 1;">' + type + '</button>\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
function addInputPrice(price, widthInput, customFn, type, min, max, step) {
|
function addInputPrice(name, price, widthInput, customFn, type, min, max, step) {
|
||||||
return ' <input id="btcpay-input-price" name="price" type="' + (type || "text") + '" min="' + (min || 0) + '" max="' + (max || "none") + '" step="' + (step || "any") + '" value="' + price + '" style="width: ' + widthInput + ';" oninput="event.preventDefault();isNaN(event.target.value) || event.target.value <= 0 ? document.querySelector(\'#btcpay-input-price\').value = ' + price + ' : event.target.value" ' + (customFn || '') + ' />\n';
|
return ' <input id="btcpay-input-price" name="'+name+'" type="' + (type || "text") + '" min="' + (min || 0) + '" max="' + (max || "none") + '" step="' + (step || "any") + '" value="' + price + '" style="width: ' + widthInput + ';" oninput="event.preventDefault();isNaN(event.target.value) || event.target.value <= 0 ? document.querySelector(\'#btcpay-input-price\').value = ' + price + ' : event.target.value" ' + (customFn || '') + ' />\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSelectCurrency(currency) {
|
function addSelectCurrency(currency) {
|
||||||
|
|||||||
Reference in New Issue
Block a user