Convert public app parts

This commit is contained in:
Dennis Reimann 2022-07-22 15:41:14 +02:00 committed by Andrew Camilleri
parent 8c6705bccb
commit 701ba59bd8
23 changed files with 477 additions and 511 deletions

View file

@ -651,7 +651,7 @@ donation:
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>(); vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
Assert.Equal("hello", vmpos.Title); Assert.Equal("hello", vmpos.Title);
var publicApps = user.GetController<UIAppsPublicController>(); var publicApps = user.GetController<UIPointOfSaleController>();
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>(); var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
Assert.Equal("hello", vmview.Title); Assert.Equal("hello", vmview.Title);
Assert.Equal(3, vmview.Items.Length); Assert.Equal(3, vmview.Items.Length);
@ -720,7 +720,7 @@ donation:
custom: true custom: true
"; ";
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result); Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
publicApps = user.GetController<UIAppsPublicController>(); publicApps = user.GetController<UIPointOfSaleController>();
vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>(); vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
Assert.Equal(test.Code, vmview.CurrencyCode); Assert.Equal(test.Code, vmview.CurrencyCode);
Assert.Equal(test.ExpectedSymbol, Assert.Equal(test.ExpectedSymbol,

View file

@ -97,8 +97,8 @@ namespace BTCPayServer.Tests
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
var anonAppPubsController = tester.PayTester.GetController<UIAppsPublicController>(); var anonAppPubsController = tester.PayTester.GetController<UICrowdfundController>();
var publicApps = user.GetController<UIAppsPublicController>(); var crowdfundController = user.GetController<UICrowdfundController>();
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{ {
@ -108,12 +108,12 @@ namespace BTCPayServer.Tests
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty)); Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
//Scenario 2: Not Enabled But Admin - Allowed //Scenario 2: Not Enabled But Admin - Allowed
Assert.IsType<OkObjectResult>(await publicApps.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund() Assert.IsType<OkObjectResult>(await crowdfundController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
{ {
RedirectToCheckout = false, RedirectToCheckout = false,
Amount = new decimal(0.01) Amount = new decimal(0.01)
}, default)); }, default));
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(app.Id, string.Empty)); Assert.IsType<ViewResult>(await crowdfundController.ViewCrowdfund(app.Id, string.Empty));
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty)); Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(app.Id, string.Empty));
//Scenario 3: Enabled But Start Date > Now - Not Allowed //Scenario 3: Enabled But Start Date > Now - Not Allowed
@ -190,8 +190,7 @@ namespace BTCPayServer.Tests
crowdfundViewModel.EnforceTargetAmount = true; crowdfundViewModel.EnforceTargetAmount = true;
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result); Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
var anonAppPubsController = tester.PayTester.GetController<UIAppsPublicController>(); var publicApps = user.GetController<UICrowdfundController>();
var publicApps = user.GetController<UIAppsPublicController>();
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model); .IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);

View file

@ -55,7 +55,7 @@ donation:
"; ";
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result); Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>(); await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
var publicApps = user.GetController<UIAppsPublicController>(); var publicApps = user.GetController<UIPointOfSaleController>();
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>(); var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
// apple shouldn't be available since we it's set to "disabled: true" above // apple shouldn't be available since we it's set to "disabled: true" above

View file

@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken] [AutoValidateAntiforgeryToken]
[Route("apps")] [Route("apps")]
public partial class UIAppsController : Controller public partial class UIAppsController : Controller
@ -37,7 +36,6 @@ namespace BTCPayServer.Controllers
public string CreatedAppId { get; set; } public string CreatedAppId { get; set; }
public class AppUpdated public class AppUpdated
{ {
public string AppId { get; set; } public string AppId { get; set; }
@ -49,6 +47,22 @@ namespace BTCPayServer.Controllers
} }
} }
[HttpGet("/apps/{appId}")]
public async Task<IActionResult> RedirectToApp(string appId)
{
var app = await _appService.GetApp(appId, null);
if (app is null)
return NotFound();
return app.AppType switch
{
nameof(AppType.Crowdfund) => RedirectToAction(nameof(UICrowdfundController.ViewCrowdfund), "UICrowdfund", new { appId }),
nameof(AppType.PointOfSale) => RedirectToAction(nameof(UIPointOfSaleController.ViewPointOfSale), "UIPointOfSale", new { appId }),
_ => NotFound()
};
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("/stores/{storeId}/apps")] [HttpGet("/stores/{storeId}/apps")]
public async Task<IActionResult> ListApps( public async Task<IActionResult> ListApps(
string storeId, string storeId,
@ -94,6 +108,7 @@ namespace BTCPayServer.Controllers
}); });
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("/stores/{storeId}/apps/create")] [HttpGet("/stores/{storeId}/apps/create")]
public IActionResult CreateApp(string storeId) public IActionResult CreateApp(string storeId)
{ {
@ -103,6 +118,7 @@ namespace BTCPayServer.Controllers
}); });
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("/stores/{storeId}/apps/create")] [HttpPost("/stores/{storeId}/apps/create")]
public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm) public async Task<IActionResult> CreateApp(string storeId, CreateAppViewModel vm)
{ {
@ -151,6 +167,7 @@ namespace BTCPayServer.Controllers
}; };
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/delete")] [HttpGet("{appId}/delete")]
public IActionResult DeleteApp(string appId) public IActionResult DeleteApp(string appId)
{ {
@ -161,6 +178,7 @@ namespace BTCPayServer.Controllers
return View("Confirm", new ConfirmModel("Delete app", $"The app <strong>{app.Name}</strong> and its settings will be permanently deleted. Are you sure?", "Delete")); return View("Confirm", new ConfirmModel("Delete app", $"The app <strong>{app.Name}</strong> and its settings will be permanently deleted. Are you sure?", "Delete"));
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/delete")] [HttpPost("{appId}/delete")]
public async Task<IActionResult> DeleteAppPost(string appId) public async Task<IActionResult> DeleteAppPost(string appId)
{ {

View file

@ -1,430 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Controllers
{
public class UIAppsPublicController : Controller
{
public UIAppsPublicController(AppService appService,
BTCPayServerOptions btcPayServerOptions,
UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager)
{
_AppService = appService;
_BtcPayServerOptions = btcPayServerOptions;
_InvoiceController = invoiceController;
_UserManager = userManager;
}
private readonly AppService _AppService;
private readonly BTCPayServerOptions _BtcPayServerOptions;
private readonly UIInvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
[HttpGet("/apps/{appId}")]
public async Task<IActionResult> RedirectToApp(string appId)
{
var app = await _AppService.GetApp(appId, null);
if (app is null)
return NotFound();
switch (app.AppType)
{
case nameof(AppType.Crowdfund):
return RedirectToAction("ViewCrowdfund", new { appId });
case nameof(AppType.PointOfSale):
return RedirectToAction("ViewPointOfSale", new { appId });
}
return NotFound();
}
[HttpGet]
[Route("/")]
[Route("/apps/{appId}/pos/{viewType?}")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[DomainMappingConstraint(AppType.PointOfSale)]
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var numberFormatInfo = _AppService.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppService.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits));
viewType ??= settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
var store = await _AppService.GetStore(app);
var storeBlob = store.GetStoreBlob();
return View("PointOfSale/" + viewType, new ViewPointOfSaleViewModel()
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ViewType = (PosViewType)viewType,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
CurrencyCode = settings.Currency,
CurrencySymbol = numberFormatInfo.CurrencySymbol,
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
{
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
},
Items = _AppService.GetPOSItems(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
CustomTipPercentages = settings.CustomTipPercentages,
CustomCSSLink = settings.CustomCSSLink,
CustomLogoLink = storeBlob.CustomLogo,
AppId = appId,
StoreId = store.Id,
Description = settings.Description,
EmbeddedCSS = settings.EmbeddedCSS,
RequiresRefundEmail = settings.RequiresRefundEmail
});
}
[HttpPost]
[Route("/")]
[Route("/apps/{appId}/pos/{viewType?}")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraint(AppType.PointOfSale)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ViewPointOfSale(string appId,
PosViewType viewType,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey,
string posData = null,
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
CancellationToken cancellationToken = default)
{
var app = await _AppService.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId, viewType = viewType });
}
string title = null;
decimal? price = null;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _AppService.GetPOSItems(settings.Template, settings.Currency);
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}
else
{
price = choice.Price.Value;
if (amount > price)
price = amount;
}
if (choice.Inventory.HasValue)
{
if (choice.Inventory <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
}
}
else
{
if (!settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
return NotFound();
price = amount;
title = settings.Title;
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
if (!string.IsNullOrEmpty(posData) &&
settings.DefaultView == PosViewType.Cart &&
AppService.TryParsePosCartItems(posData, out var cartItems))
{
var choices = _AppService.GetPOSItems(settings.Template, settings.Currency);
foreach (var cartItem in cartItems)
{
var itemChoice = choices.FirstOrDefault(c => c.Id == cartItem.Key);
if (itemChoice == null)
return NotFound();
if (itemChoice.Inventory.HasValue)
{
switch (itemChoice.Inventory)
{
case int i when i <= 0:
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
case int inventory when inventory < cartItem.Value:
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
}
}
}
}
}
var store = await _AppService.GetStore(app);
try
{
var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
{
ItemCode = choice?.Id,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId ?? AppService.GetAppOrderId(app),
NotificationURL =
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl
: !string.IsNullOrEmpty(settings.RedirectUrl) ? settings.RedirectUrl
: Request.GetDisplayUrl(),
FullNotifications = true,
ExtendedNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData,
RedirectAutomatically = settings.RedirectAutomatically,
SupportedTransactionCurrencies = paymentMethods,
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
? store.GetStoreBlob().RequiresRefundEmail
: requiresRefundEmail == RequiresRefundEmail.On,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() { AppService.GetAppInternalTag(appId) },
cancellationToken, (entity) =>
{
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
} );
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Data.Id });
}
catch (BitpayHttpException e)
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Html = e.Message.Replace("\n", "<br />", StringComparison.OrdinalIgnoreCase),
Severity = StatusMessageModel.StatusSeverity.Error,
AllowDismiss = true
});
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
}
[HttpGet]
[Route("/")]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
if (!hasEnoughSettingsToLoad)
{
if (!isAdmin)
return NotFound();
return NotFound("A Target Currency must be set for this app in order to be loadable.");
}
var appInfo = await GetAppInfo(appId);
if (settings.Enabled)
return View(appInfo);
if (!isAdmin)
return NotFound();
return View(appInfo);
}
[HttpPost]
[Route("/")]
[Route("/apps/{appId}/crowdfund")]
[XFrameOptionsAttribute(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraintAttribute(AppType.Crowdfund)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
var app = await _AppService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _AppService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled && !isAdmin)
{
return NotFound("Crowdfund is not currently active");
}
var info = await GetAppInfo(appId);
if (!isAdmin &&
((settings.StartDate.HasValue && DateTime.UtcNow < settings.StartDate) ||
(settings.EndDate.HasValue && DateTime.UtcNow > settings.EndDate) ||
(settings.EnforceTargetAmount &&
(info.Info.PendingProgressPercentage.GetValueOrDefault(0) +
info.Info.ProgressPercentage.GetValueOrDefault(0)) >= 100)))
{
return NotFound("Crowdfund is not currently active");
}
var store = await _AppService.GetStore(app);
var title = settings.Title;
decimal? price = request.Amount;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
var choices = _AppService.GetPOSItems(settings.PerksTemplate, settings.TargetCurrency);
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
if (choice == null)
return NotFound("Incorrect option provided");
title = choice.Title;
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}
else
{
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
}
if (choice.Inventory.HasValue)
{
if (choice.Inventory <= 0)
{
return NotFound("Option was out of stock");
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
}
}
else
{
if (request.Amount < 0)
{
return NotFound("Please provide an amount greater than 0");
}
price = request.Amount;
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
{
return NotFound("Contribution Amount is more than is currently allowed.");
}
try
{
var invoice = await _InvoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
{
OrderId = AppService.GetAppOrderId(app),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
SupportedTransactionCurrencies = paymentMethods,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl(),
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() {AppService.GetAppInternalTag(appId)},
cancellationToken, (entity) =>
{
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
});
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice",
new {invoiceId = invoice.Data.Id});
}
return Ok(invoice.Data.Id);
}
catch (BitpayHttpException e)
{
return BadRequest(e.Message);
}
}
private async Task<ViewCrowdfundViewModel> GetAppInfo(string appId)
{
var info = (ViewCrowdfundViewModel)await _AppService.GetAppInfo(appId);
info.HubPath = AppHub.GetHubPath(Request);
info.SimpleDisplay = Request.Query.ContainsKey("simple");
return info;
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View file

@ -40,8 +40,8 @@ namespace Microsoft.AspNetCore.Mvc
public static string AppLink(this LinkGenerator urlHelper, string appId, string scheme, HostString host, string pathbase) public static string AppLink(this LinkGenerator urlHelper, string appId, string scheme, HostString host, string pathbase)
{ {
return urlHelper.GetUriByAction( return urlHelper.GetUriByAction(
action: nameof(UIAppsPublicController.RedirectToApp), action: nameof(UIAppsController.RedirectToApp),
controller: "UIAppsPublic", controller: "UIApps",
values: new { appId }, values: new { appId },
scheme, host, pathbase); scheme, host, pathbase);
} }

View file

@ -415,7 +415,6 @@ namespace BTCPayServer.Hosting
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>(); services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<BitpayAccessTokenController>(); services.AddTransient<BitpayAccessTokenController>();
services.AddTransient<UIInvoiceController>(); services.AddTransient<UIInvoiceController>();
services.AddTransient<UIAppsPublicController>();
services.AddTransient<UIPaymentRequestController>(); services.AddTransient<UIPaymentRequestController>();
// Add application services. // Add application services.
services.AddSingleton<EmailSenderFactory>(); services.AddSingleton<EmailSenderFactory>();

View file

@ -1,40 +1,210 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Plugins.Crowdfund.Models; using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Plugins.Crowdfund.Controllers namespace BTCPayServer.Plugins.Crowdfund.Controllers
{ {
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken] [AutoValidateAntiforgeryToken]
[Route("apps")] [Route("apps")]
public class UICrowdfundController : Controller public class UICrowdfundController : Controller
{ {
public UICrowdfundController( public UICrowdfundController(
EventAggregator eventAggregator, AppService appService,
CurrencyNameTable currencies, CurrencyNameTable currencies,
EventAggregator eventAggregator,
StoreRepository storeRepository, StoreRepository storeRepository,
AppService appService) UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager)
{ {
_eventAggregator = eventAggregator;
_currencies = currencies; _currencies = currencies;
_storeRepository = storeRepository;
_appService = appService; _appService = appService;
_userManager = userManager;
_storeRepository = storeRepository;
_eventAggregator = eventAggregator;
_invoiceController = invoiceController;
} }
private readonly EventAggregator _eventAggregator; private readonly EventAggregator _eventAggregator;
private readonly CurrencyNameTable _currencies; private readonly CurrencyNameTable _currencies;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly AppService _appService; private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
private readonly UserManager<ApplicationUser> _userManager;
[HttpGet("/")]
[HttpGet("/apps/{appId}/crowdfund")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[DomainMappingConstraint(AppType.Crowdfund)]
public async Task<IActionResult> ViewCrowdfund(string appId, string statusMessage)
{
var app = await _appService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
var hasEnoughSettingsToLoad = !string.IsNullOrEmpty(settings.TargetCurrency);
if (!hasEnoughSettingsToLoad)
{
if (!isAdmin)
return NotFound();
return NotFound("A Target Currency must be set for this app in order to be loadable.");
}
var appInfo = await GetAppInfo(appId);
if (settings.Enabled)
return View("Crowdfund/Public/ViewCrowdfund", appInfo);
if (!isAdmin)
return NotFound();
return View("Crowdfund/Public/ViewCrowdfund", appInfo);
}
[HttpPost("/")]
[HttpPost("/apps/{appId}/crowdfund")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraint(AppType.Crowdfund)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ContributeToCrowdfund(string appId, ContributeToCrowdfund request, CancellationToken cancellationToken)
{
var app = await _appService.GetApp(appId, AppType.Crowdfund, true);
if (app == null)
return NotFound();
var settings = app.GetSettings<CrowdfundSettings>();
var isAdmin = await _appService.GetAppDataIfOwner(GetUserId(), appId, AppType.Crowdfund) != null;
if (!settings.Enabled && !isAdmin)
{
return NotFound("Crowdfund is not currently active");
}
var info = await GetAppInfo(appId);
if (!isAdmin &&
((settings.StartDate.HasValue && DateTime.UtcNow < settings.StartDate) ||
(settings.EndDate.HasValue && DateTime.UtcNow > settings.EndDate) ||
(settings.EnforceTargetAmount &&
(info.Info.PendingProgressPercentage.GetValueOrDefault(0) +
info.Info.ProgressPercentage.GetValueOrDefault(0)) >= 100)))
{
return NotFound("Crowdfund is not currently active");
}
var store = await _appService.GetStore(app);
var title = settings.Title;
decimal? price = request.Amount;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(request.ChoiceKey))
{
var choices = _appService.GetPOSItems(settings.PerksTemplate, settings.TargetCurrency);
choice = choices.FirstOrDefault(c => c.Id == request.ChoiceKey);
if (choice == null)
return NotFound("Incorrect option provided");
title = choice.Title;
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}
else
{
price = choice.Price.Value;
if (request.Amount > price)
price = request.Amount;
}
if (choice.Inventory.HasValue)
{
if (choice.Inventory <= 0)
{
return NotFound("Option was out of stock");
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
}
}
else
{
if (request.Amount < 0)
{
return NotFound("Please provide an amount greater than 0");
}
price = request.Amount;
}
if (!isAdmin && (settings.EnforceTargetAmount && info.TargetAmount.HasValue && price >
(info.TargetAmount - (info.Info.CurrentAmount + info.Info.CurrentPendingAmount))))
{
return NotFound("Contribution Amount is more than is currently allowed.");
}
try
{
var invoice = await _invoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
{
OrderId = AppService.GetAppOrderId(app),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,
BuyerEmail = request.Email,
Price = price,
NotificationURL = settings.NotificationUrl,
FullNotifications = true,
ExtendedNotifications = true,
SupportedTransactionCurrencies = paymentMethods,
RedirectURL = request.RedirectUrl ?? Request.GetDisplayUrl(),
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() {AppService.GetAppInternalTag(appId)},
cancellationToken, (entity) =>
{
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
});
if (request.RedirectToCheckout)
{
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice",
new {invoiceId = invoice.Data.Id});
}
return Ok(invoice.Data.Id);
}
catch (BitpayHttpException e)
{
return BadRequest(e.Message);
}
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)] [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/settings/crowdfund")] [HttpGet("{appId}/settings/crowdfund")]
@ -85,6 +255,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
return View("Crowdfund/UpdateCrowdfund", vm); return View("Crowdfund/UpdateCrowdfund", vm);
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/settings/crowdfund")] [HttpPost("{appId}/settings/crowdfund")]
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command) public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
{ {
@ -199,7 +370,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
return RedirectToAction(nameof(UpdateCrowdfund), new { appId }); return RedirectToAction(nameof(UpdateCrowdfund), new { appId });
} }
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency) private async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
{ {
if (string.IsNullOrWhiteSpace(currency)) if (string.IsNullOrWhiteSpace(currency))
{ {
@ -209,5 +380,15 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
} }
private AppData GetCurrentApp() => HttpContext.GetAppData(); private AppData GetCurrentApp() => HttpContext.GetAppData();
private string GetUserId() => _userManager.GetUserId(User);
private async Task<ViewCrowdfundViewModel> GetAppInfo(string appId)
{
var info = (ViewCrowdfundViewModel)await _appService.GetAppInfo(appId);
info.HubPath = AppHub.GetHubPath(Request);
info.SimpleDisplay = Request.Query.ContainsKey("simple");
return info;
}
} }
} }

View file

@ -1,42 +1,252 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants; using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client; using BTCPayServer.Client;
using BTCPayServer.Controllers;
using BTCPayServer.Data; using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.ModelBinders;
using BTCPayServer.Models;
using BTCPayServer.Plugins.PointOfSale.Models; using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services.Apps; using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores; using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using NBitpayClient;
using NicolasDorier.RateLimits;
namespace BTCPayServer.Plugins.PointOfSale.Controllers namespace BTCPayServer.Plugins.PointOfSale.Controllers
{ {
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[AutoValidateAntiforgeryToken] [AutoValidateAntiforgeryToken]
[Route("apps")] [Route("apps")]
public class UIPointOfSaleController : Controller public class UIPointOfSaleController : Controller
{ {
public UIPointOfSaleController( public UIPointOfSaleController(
AppService appService,
CurrencyNameTable currencies, CurrencyNameTable currencies,
StoreRepository storeRepository, StoreRepository storeRepository,
AppService appService) UIInvoiceController invoiceController)
{ {
_currencies = currencies; _currencies = currencies;
_storeRepository = storeRepository;
_appService = appService; _appService = appService;
_storeRepository = storeRepository;
_invoiceController = invoiceController;
} }
private readonly CurrencyNameTable _currencies; private readonly CurrencyNameTable _currencies;
private readonly StoreRepository _storeRepository; private readonly StoreRepository _storeRepository;
private readonly AppService _appService; private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
[HttpGet("/")]
[HttpGet("/apps/{appId}/pos/{viewType?}")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[DomainMappingConstraint(AppType.PointOfSale)]
public async Task<IActionResult> ViewPointOfSale(string appId, PosViewType? viewType = null)
{
var app = await _appService.GetApp(appId, AppType.PointOfSale);
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
var numberFormatInfo = _appService.Currencies.GetNumberFormatInfo(settings.Currency) ??
_appService.Currencies.GetNumberFormatInfo("USD");
double step = Math.Pow(10, -numberFormatInfo.CurrencyDecimalDigits);
viewType ??= settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
var store = await _appService.GetStore(app);
var storeBlob = store.GetStoreBlob();
return View($"PointOfSale/Public/{viewType}", new ViewPointOfSaleViewModel
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
ViewType = (PosViewType)viewType,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
CurrencyCode = settings.Currency,
CurrencySymbol = numberFormatInfo.CurrencySymbol,
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData
{
CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol,
Divisibility = numberFormatInfo.CurrencyDecimalDigits,
DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator,
ThousandSeparator = numberFormatInfo.NumberGroupSeparator,
Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern),
SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern)
},
Items = _appService.GetPOSItems(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
CustomTipPercentages = settings.CustomTipPercentages,
CustomCSSLink = settings.CustomCSSLink,
CustomLogoLink = storeBlob.CustomLogo,
AppId = appId,
StoreId = store.Id,
Description = settings.Description,
EmbeddedCSS = settings.EmbeddedCSS,
RequiresRefundEmail = settings.RequiresRefundEmail
});
}
[HttpPost("/")]
[HttpPost("/apps/{appId}/pos/{viewType?}")]
[XFrameOptions(XFrameOptionsAttribute.XFrameOptions.AllowAll)]
[IgnoreAntiforgeryToken]
[EnableCors(CorsPolicies.All)]
[DomainMappingConstraint(AppType.PointOfSale)]
[RateLimitsFilter(ZoneLimits.PublicInvoices, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> ViewPointOfSale(string appId,
PosViewType viewType,
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount,
string email,
string orderId,
string notificationUrl,
string redirectUrl,
string choiceKey,
string posData = null,
RequiresRefundEmail requiresRefundEmail = RequiresRefundEmail.InheritFromStore,
CancellationToken cancellationToken = default)
{
var app = await _appService.GetApp(appId, AppType.PointOfSale);
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
}
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId, viewType });
}
string title = null;
decimal? price = null;
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
ViewPointOfSaleViewModel.Item choice = null;
if (!string.IsNullOrEmpty(choiceKey))
{
var choices = _appService.GetPOSItems(settings.Template, settings.Currency);
choice = choices.FirstOrDefault(c => c.Id == choiceKey);
if (choice == null)
return NotFound();
title = choice.Title;
if (choice.Price.Type == ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{
price = null;
}
else
{
price = choice.Price.Value;
if (amount > price)
price = amount;
}
if (choice.Inventory.HasValue)
{
if (choice.Inventory <= 0)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
}
if (choice?.PaymentMethods?.Any() is true)
{
paymentMethods = choice?.PaymentMethods.ToDictionary(s => s,
s => new InvoiceSupportedTransactionCurrency() { Enabled = true });
}
}
else
{
if (!settings.ShowCustomAmount && settings.DefaultView != PosViewType.Cart)
return NotFound();
price = amount;
title = settings.Title;
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
if (!string.IsNullOrEmpty(posData) &&
settings.DefaultView == PosViewType.Cart &&
AppService.TryParsePosCartItems(posData, out var cartItems))
{
var choices = _appService.GetPOSItems(settings.Template, settings.Currency);
foreach (var cartItem in cartItems)
{
var itemChoice = choices.FirstOrDefault(c => c.Id == cartItem.Key);
if (itemChoice == null)
return NotFound();
if (itemChoice.Inventory.HasValue)
{
switch (itemChoice.Inventory)
{
case int i when i <= 0:
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
case int inventory when inventory < cartItem.Value:
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
}
}
}
}
}
var store = await _appService.GetStore(app);
try
{
var invoice = await _invoiceController.CreateInvoiceCore(new BitpayCreateInvoiceRequest()
{
ItemCode = choice?.Id,
ItemDesc = title,
Currency = settings.Currency,
Price = price,
BuyerEmail = email,
OrderId = orderId ?? AppService.GetAppOrderId(app),
NotificationURL =
string.IsNullOrEmpty(notificationUrl) ? settings.NotificationUrl : notificationUrl,
RedirectURL = !string.IsNullOrEmpty(redirectUrl) ? redirectUrl
: !string.IsNullOrEmpty(settings.RedirectUrl) ? settings.RedirectUrl
: Request.GetDisplayUrl(),
FullNotifications = true,
ExtendedNotifications = true,
PosData = string.IsNullOrEmpty(posData) ? null : posData,
RedirectAutomatically = settings.RedirectAutomatically,
SupportedTransactionCurrencies = paymentMethods,
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
? store.GetStoreBlob().RequiresRefundEmail
: requiresRefundEmail == RequiresRefundEmail.On,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string>() { AppService.GetAppInternalTag(appId) },
cancellationToken, (entity) =>
{
entity.Metadata.OrderUrl = Request.GetDisplayUrl();
} );
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Data.Id });
}
catch (BitpayHttpException e)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Html = e.Message.Replace("\n", "<br />", StringComparison.OrdinalIgnoreCase),
Severity = StatusMessageModel.StatusSeverity.Error,
AllowDismiss = true
});
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
}
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpGet("{appId}/settings/pos")] [HttpGet("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId) public async Task<IActionResult> UpdatePointOfSale(string appId)
{ {
@ -113,6 +323,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
return View("PointOfSale/UpdatePointOfSale", vm); return View("PointOfSale/UpdatePointOfSale", vm);
} }
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[HttpPost("{appId}/settings/pos")] [HttpPost("{appId}/settings/pos")]
public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm) public async Task<IActionResult> UpdatePointOfSale(string appId, UpdatePointOfSaleViewModel vm)
{ {
@ -180,7 +391,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
return list.Split(separator, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray(); return list.Split(separator, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
} }
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency) private async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
{ {
if (string.IsNullOrWhiteSpace(currency)) if (string.IsNullOrWhiteSpace(currency))
{ {

View file

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions; using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Controllers; using BTCPayServer.Controllers;
using BTCPayServer.Models.AppViewModels; using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Plugins.Crowdfund.Controllers;
using BTCPayServer.Plugins.Crowdfund.Models; using BTCPayServer.Plugins.Crowdfund.Models;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -18,12 +19,14 @@ namespace BTCPayServer.Services.Apps
public const string PaymentReceived = "PaymentReceived"; public const string PaymentReceived = "PaymentReceived";
public const string InfoUpdated = "InfoUpdated"; public const string InfoUpdated = "InfoUpdated";
public const string InvoiceError = "InvoiceError"; public const string InvoiceError = "InvoiceError";
private readonly UIAppsPublicController _AppsPublicController;
public AppHub(UIAppsPublicController appsPublicController) private readonly UICrowdfundController _crowdfundController;
public AppHub(UICrowdfundController crowdfundController)
{ {
_AppsPublicController = appsPublicController; _crowdfundController = crowdfundController;
} }
public async Task ListenToCrowdfundApp(string appId) public async Task ListenToCrowdfundApp(string appId)
{ {
if (Context.Items.ContainsKey("app")) if (Context.Items.ContainsKey("app"))
@ -35,16 +38,15 @@ namespace BTCPayServer.Services.Apps
await Groups.AddToGroupAsync(Context.ConnectionId, appId); await Groups.AddToGroupAsync(Context.ConnectionId, appId);
} }
public async Task CreateInvoice(ContributeToCrowdfund model) public async Task CreateInvoice(ContributeToCrowdfund model)
{ {
model.RedirectToCheckout = false; model.RedirectToCheckout = false;
_AppsPublicController.ControllerContext.HttpContext = Context.GetHttpContext(); _crowdfundController.ControllerContext.HttpContext = Context.GetHttpContext();
try try
{ {
var result = var result =
await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model, Context.ConnectionAborted); await _crowdfundController.ContributeToCrowdfund(Context.Items["app"].ToString(), model, Context.ConnectionAborted);
switch (result) switch (result)
{ {
case OkObjectResult okObjectResult: case OkObjectResult okObjectResult:
@ -54,16 +56,14 @@ namespace BTCPayServer.Services.Apps
await Clients.Caller.SendCoreAsync(InvoiceError, new[] { objectResult.Value }); await Clients.Caller.SendCoreAsync(InvoiceError, new[] { objectResult.Value });
break; break;
default: default:
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>()); await Clients.Caller.SendCoreAsync(InvoiceError, Array.Empty<object>());
break; break;
} }
} }
catch (Exception) catch (Exception)
{ {
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>()); await Clients.Caller.SendCoreAsync(InvoiceError, Array.Empty<object>());
} }
} }
public static string GetHubPath(HttpRequest request) public static string GetHubPath(HttpRequest request)
@ -75,6 +75,5 @@ namespace BTCPayServer.Services.Apps
{ {
route.MapHub<AppHub>("/apps/hub"); route.MapHub<AppHub>("/apps/hub");
} }
} }
} }

View file

@ -252,7 +252,7 @@
</div> </div>
<div class="col-md-4 col-sm-12"> <div class="col-md-4 col-sm-12">
<partial <partial
name="/Views/UIAppsPublic/Crowdfund/ContributeForm.cshtml" name="Crowdfund/Public/ContributeForm"
model="@(new ContributeToCrowdfund { ViewCrowdfundViewModel = Model, RedirectToCheckout = true })"> model="@(new ContributeToCrowdfund { ViewCrowdfundViewModel = Model, RedirectToCheckout = true })">
</partial> </partial>
</div> </div>

View file

@ -31,7 +31,7 @@
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button> <button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
@if (Model.ModelWithMinimumData) @if (Model.ModelWithMinimumData)
{ {
<a class="btn btn-secondary" asp-action="ViewCrowdfund" asp-controller="UIAppsPublic" asp-route-appId="@Model.AppId" id="ViewApp" target="_blank">View</a> <a class="btn btn-secondary" asp-action="ViewCrowdfund" asp-route-appId="@Model.AppId" id="ViewApp" target="_blank">View</a>
} }
</div> </div>
</div> </div>

View file

@ -1,9 +1,8 @@
@using BTCPayServer.Models.AppViewModels
@using BTCPayServer.Plugins.PointOfSale.Models @using BTCPayServer.Plugins.PointOfSale.Models
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel @model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
@{ @{
Layout = "_LayoutPos"; Layout = "PointOfSale/Public/_LayoutPos";
int[] customTipPercentages = Model.CustomTipPercentages; var customTipPercentages = Model.CustomTipPercentages;
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue); var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
} }
@ -162,7 +161,6 @@
<form <form
id="js-cart-pay-form" id="js-cart-pay-form"
method="post" method="post"
asp-controller="UIAppsPublic"
asp-action="ViewPointOfSale" asp-action="ViewPointOfSale"
asp-route-appId="@Model.AppId" asp-route-appId="@Model.AppId"
asp-antiforgery="false" asp-antiforgery="false"

View file

@ -0,0 +1,18 @@
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
@{
Layout = "PointOfSale/Public/_LayoutPos";
}
<partial name="_StatusMessage" />
@if (Context.Request.Query.ContainsKey("simple"))
{
<partial name="PointOfSale/Public/MinimalLight" model="Model" />
}
else
{
<noscript>
<partial name="PointOfSale/Public/MinimalLight" model="Model" />
</noscript>
<partial name="PointOfSale/Public/VueLight" model="Model" />
}

View file

@ -7,7 +7,7 @@
} }
</div> </div>
<div class="py-5 px-3"> <div class="py-5 px-3">
<form method="post" asp-controller="UIAppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy> <form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
<div class="input-group"> <div class="input-group">
<span class="input-group-text">@Model.CurrencySymbol</span> <span class="input-group-text">@Model.CurrencySymbol</span>
<input class="form-control" type="number" step="@Model.Step" name="amount" placeholder="Amount"> <input class="form-control" type="number" step="@Model.Step" name="amount" placeholder="Amount">

View file

@ -1,4 +1,3 @@
@using BTCPayServer.Models.AppViewModels
@using BTCPayServer.Payments.Lightning @using BTCPayServer.Payments.Lightning
@using BTCPayServer.Plugins.PointOfSale.Models @using BTCPayServer.Plugins.PointOfSale.Models
@using BTCPayServer.Services.Stores @using BTCPayServer.Services.Stores
@ -20,7 +19,7 @@
@{ @{
var store = await StoreRepository.FindStore(Model.StoreId); var store = await StoreRepository.FindStore(Model.StoreId);
Layout = "_LayoutPos"; Layout = "PointOfSale/Public/_LayoutPos";
Context.Request.Query.TryGetValue("cryptocode", out var cryptoCodeValues); Context.Request.Query.TryGetValue("cryptocode", out var cryptoCodeValues);
var cryptoCode = cryptoCodeValues.FirstOrDefault() ?? "BTC"; var cryptoCode = cryptoCodeValues.FirstOrDefault() ?? "BTC";
var supported = store.GetSupportedPaymentMethods(BTCPayNetworkProvider).OfType<LNURLPaySupportedPaymentMethod>().OrderBy(method => method.CryptoCode == cryptoCode).FirstOrDefault(); var supported = store.GetSupportedPaymentMethods(BTCPayNetworkProvider).OfType<LNURLPaySupportedPaymentMethod>().OrderBy(method => method.CryptoCode == cryptoCode).FirstOrDefault();

View file

@ -1,8 +1,7 @@
@using BTCPayServer.Models.AppViewModels
@using BTCPayServer.Plugins.PointOfSale.Models @using BTCPayServer.Plugins.PointOfSale.Models
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel @model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
@{ @{
Layout = "_LayoutPos"; Layout = "PointOfSale/Public/_LayoutPos";
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue); var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
} }
@ -18,15 +17,14 @@
</div> </div>
} }
<div class="card-deck my-3 mx-auto"> <div class="card-deck my-3 mx-auto">
@for (int x = 0; x < Model.Items.Length; x++) @for (var x = 0; x < Model.Items.Length; x++)
{ {
var item = Model.Items[x]; var item = Model.Items[x];
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText; var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
buttonText = buttonText.Replace("{0}",item.Price.Formatted) buttonText = buttonText.Replace("{0}",item.Price.Formatted).Replace("{Price}",item.Price.Formatted);
?.Replace("{Price}",item.Price.Formatted);
<div class="card px-0" data-id="@x"> <div class="card px-0" data-id="@x">
@if (!String.IsNullOrWhiteSpace(item.Image)) @if (!string.IsNullOrWhiteSpace(item.Image))
{ {
<img class="card-img-top" src="@item.Image" alt="Card image cap" asp-append-version="true"> <img class="card-img-top" src="@item.Image" alt="Card image cap" asp-append-version="true">
} }
@ -36,7 +34,7 @@
{ {
@if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup) @if (item.Price.Type != ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Topup)
{ {
<form method="post" asp-controller="UIAppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy> <form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id"/> <input type="hidden" name="choicekey" value="@item.Id"/>
<input type="hidden" name="requiresRefundEmail" value="@Model.RequiresRefundEmail.ToString()" /> <input type="hidden" name="requiresRefundEmail" value="@Model.RequiresRefundEmail.ToString()" />
@{PayFormInputContent(item.BuyButtonText ?? Model.CustomButtonText, item.Price.Type, item.Price.Value, item.Price.Value);} @{PayFormInputContent(item.BuyButtonText ?? Model.CustomButtonText, item.Price.Type, item.Price.Value, item.Price.Value);}
@ -44,7 +42,7 @@
} }
else else
{ {
<form method="post" asp-controller="UIAppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false"> <form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false">
<input type="hidden" name="requiresRefundEmail" value="@Model.RequiresRefundEmail.ToString()" /> <input type="hidden" name="requiresRefundEmail" value="@Model.RequiresRefundEmail.ToString()" />
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id"> <button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@Safe.Raw(buttonText) @Safe.Raw(buttonText)
@ -54,7 +52,6 @@
} }
@if (item.Inventory.HasValue) @if (item.Inventory.HasValue)
{ {
<div class="w-100 pt-2 text-center text-muted"> <div class="w-100 pt-2 text-center text-muted">
@if (item.Inventory > 0) @if (item.Inventory > 0)
{ {
@ -78,7 +75,7 @@
<div class="card px-0"> <div class="card px-0">
@{CardBody("Custom Amount", "Create invoice to pay custom amount");} @{CardBody("Custom Amount", "Create invoice to pay custom amount");}
<div class="card-footer bg-transparent border-0 pb-3"> <div class="card-footer bg-transparent border-0 pb-3">
<form method="post" asp-controller="UIAppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy> <form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy>
@{PayFormInputContent(Model.CustomButtonText, ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);} @{PayFormInputContent(Model.CustomButtonText, ViewPointOfSaleViewModel.Item.ItemPrice.ItemPriceType.Minimum);}
</form> </form>
@if (anyInventoryItems) @if (anyInventoryItems)
@ -111,14 +108,13 @@
<button class="btn btn-primary text-nowrap" type="submit">@buttonText</button> <button class="btn btn-primary text-nowrap" type="submit">@buttonText</button>
</div> </div>
} }
} }
private void CardBody(string title, string description) private void CardBody(string title, string description)
{ {
<div class="card-body my-auto pb-0"> <div class="card-body my-auto pb-0">
<h5 class="card-title">@title</h5> <h5 class="card-title">@title</h5>
@if (!String.IsNullOrWhiteSpace(description)) @if (!string.IsNullOrWhiteSpace(description))
{ {
<p class="card-text">@Safe.Raw(description)</p> <p class="card-text">@Safe.Raw(description)</p>
} }

View file

@ -1,6 +1,6 @@
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel @model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
<div id="app" class="l-pos-wrapper" v-cloak> <div id="app" class="l-pos-wrapper" v-cloak>
<form method="post" asp-controller="UIAppsPublic" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy v-on:submit="handleFormSubmit"> <form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy v-on:submit="handleFormSubmit">
<div class="l-pos-header bg-primary py-3 px-3"> <div class="l-pos-header bg-primary py-3 px-3">
@if (!string.IsNullOrEmpty(Model.CustomLogoLink)) @if (!string.IsNullOrEmpty(Model.CustomLogoLink))
{ {

View file

@ -13,7 +13,7 @@
<h2 class="mb-0">@ViewData["Title"]</h2> <h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-sm-0"> <div class="d-flex gap-3 mt-3 mt-sm-0">
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button> <button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
<a class="btn btn-secondary" asp-action="ViewPointOfSale" asp-controller="UIAppsPublic" asp-route-appId="@Model.Id" id="ViewApp" target="_blank">View</a> <a class="btn btn-secondary" asp-action="ViewPointOfSale" asp-route-appId="@Model.Id" id="ViewApp" target="_blank">View</a>
</div> </div>
</div> </div>
@ -173,7 +173,7 @@
<div class="accordion-body"> <div class="accordion-body">
You can embed this POS via an iframe. You can embed this POS via an iframe.
@{ @{
var iframe = $"<iframe src='{(Url.Action("ViewPointOfSale", "UIAppsPublic", new { appId = Model.Id }, Context.Request.Scheme))}' style='max-width: 100%; border: 0;'></iframe>"; var iframe = $"<iframe src='{Url.Action("ViewPointOfSale", "UIPointOfSale", new { appId = Model.Id }, Context.Request.Scheme)}' style='max-width: 100%; border: 0;'></iframe>";
} }
<pre class="p-3">@iframe</pre> <pre class="p-3">@iframe</pre>
</div> </div>

View file

@ -103,10 +103,6 @@
<a asp-action="@app.UpdateAction" asp-controller="UIApps" asp-route-appId="@app.Id" asp-route-storeId="@app.StoreId">Settings</a> <a asp-action="@app.UpdateAction" asp-controller="UIApps" asp-route-appId="@app.Id" asp-route-storeId="@app.StoreId">Settings</a>
<span> - </span> <span> - </span>
} }
<a asp-action="@app.ViewAction" asp-controller="UIAppsPublic" asp-route-appId="@app.Id">View</a>
<a asp-action="@app.ViewAction" asp-controller="UIAppsPublic" asp-route-appId="@app.Id" target="_blank"
title="View in New Window"><span class="fa fa-external-link"></span></a>
<span> - </span>
<a asp-action="DeleteApp" asp-route-appId="@app.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@app.AppName</strong> and its settings will be permanently deleted from your store <strong>@app.StoreName</strong>." data-confirm-input="DELETE">Delete</a> <a asp-action="DeleteApp" asp-route-appId="@app.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@app.AppName</strong> and its settings will be permanently deleted from your store <strong>@app.StoreName</strong>." data-confirm-input="DELETE">Delete</a>
</td> </td>
</tr> </tr>

View file

@ -1,18 +0,0 @@
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
@{
Layout = "_LayoutPos";
}
<partial name="_StatusMessage" />
@if (Context.Request.Query.ContainsKey("simple"))
{
<partial name="/Views/UIAppsPublic/PointOfSale/MinimalLight.cshtml" model="Model" />
}
else
{
<noscript>
<partial name="/Views/UIAppsPublic/PointOfSale/MinimalLight.cshtml" model="Model" />
</noscript>
<partial name="/Views/UIAppsPublic/PointOfSale/VueLight.cshtml" model="Model" />
}