mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-11 01:35:22 +01:00
Convert public app parts
This commit is contained in:
parent
8c6705bccb
commit
701ba59bd8
23 changed files with 477 additions and 511 deletions
|
@ -651,7 +651,7 @@ donation:
|
|||
vmpos = await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
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>();
|
||||
Assert.Equal("hello", vmview.Title);
|
||||
Assert.Equal(3, vmview.Items.Length);
|
||||
|
@ -720,7 +720,7 @@ donation:
|
|||
custom: true
|
||||
";
|
||||
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>();
|
||||
Assert.Equal(test.Code, vmview.CurrencyCode);
|
||||
Assert.Equal(test.ExpectedSymbol,
|
||||
|
|
|
@ -97,8 +97,8 @@ namespace BTCPayServer.Tests
|
|||
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<UIAppsPublicController>();
|
||||
var publicApps = user.GetController<UIAppsPublicController>();
|
||||
var anonAppPubsController = tester.PayTester.GetController<UICrowdfundController>();
|
||||
var crowdfundController = user.GetController<UICrowdfundController>();
|
||||
|
||||
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));
|
||||
|
||||
//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,
|
||||
Amount = new decimal(0.01)
|
||||
}, 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));
|
||||
|
||||
//Scenario 3: Enabled But Start Date > Now - Not Allowed
|
||||
|
@ -190,8 +190,7 @@ namespace BTCPayServer.Tests
|
|||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<UIAppsPublicController>();
|
||||
var publicApps = user.GetController<UIAppsPublicController>();
|
||||
var publicApps = user.GetController<UICrowdfundController>();
|
||||
|
||||
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(app.Id, String.Empty).Result).Model);
|
||||
|
|
|
@ -55,7 +55,7 @@ donation:
|
|||
";
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
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>();
|
||||
|
||||
// apple shouldn't be available since we it's set to "disabled: true" above
|
||||
|
|
|
@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public partial class UIAppsController : Controller
|
||||
|
@ -37,7 +36,6 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
public string CreatedAppId { get; set; }
|
||||
|
||||
|
||||
public class AppUpdated
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
|
@ -48,7 +46,23 @@ namespace BTCPayServer.Controllers
|
|||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
[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")]
|
||||
public async Task<IActionResult> ListApps(
|
||||
string storeId,
|
||||
|
@ -94,6 +108,7 @@ namespace BTCPayServer.Controllers
|
|||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpGet("/stores/{storeId}/apps/create")]
|
||||
public IActionResult CreateApp(string storeId)
|
||||
{
|
||||
|
@ -103,6 +118,7 @@ namespace BTCPayServer.Controllers
|
|||
});
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("/stores/{storeId}/apps/create")]
|
||||
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")]
|
||||
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"));
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/delete")]
|
||||
public async Task<IActionResult> DeleteAppPost(string appId)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,8 +40,8 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
public static string AppLink(this LinkGenerator urlHelper, string appId, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(
|
||||
action: nameof(UIAppsPublicController.RedirectToApp),
|
||||
controller: "UIAppsPublic",
|
||||
action: nameof(UIAppsController.RedirectToApp),
|
||||
controller: "UIApps",
|
||||
values: new { appId },
|
||||
scheme, host, pathbase);
|
||||
}
|
||||
|
|
|
@ -415,7 +415,6 @@ namespace BTCPayServer.Hosting
|
|||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddTransient<BitpayAccessTokenController>();
|
||||
services.AddTransient<UIInvoiceController>();
|
||||
services.AddTransient<UIAppsPublicController>();
|
||||
services.AddTransient<UIPaymentRequestController>();
|
||||
// Add application services.
|
||||
services.AddSingleton<EmailSenderFactory>();
|
||||
|
|
|
@ -1,41 +1,211 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
{
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public class UICrowdfundController : Controller
|
||||
{
|
||||
public UICrowdfundController(
|
||||
EventAggregator eventAggregator,
|
||||
AppService appService,
|
||||
CurrencyNameTable currencies,
|
||||
EventAggregator eventAggregator,
|
||||
StoreRepository storeRepository,
|
||||
AppService appService)
|
||||
UIInvoiceController invoiceController,
|
||||
UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_currencies = currencies;
|
||||
_storeRepository = storeRepository;
|
||||
_appService = appService;
|
||||
_userManager = userManager;
|
||||
_storeRepository = storeRepository;
|
||||
_eventAggregator = eventAggregator;
|
||||
_invoiceController = invoiceController;
|
||||
}
|
||||
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
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)]
|
||||
[HttpGet("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId)
|
||||
|
@ -85,6 +255,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
|||
return View("Crowdfund/UpdateCrowdfund", vm);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/settings/crowdfund")]
|
||||
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 });
|
||||
}
|
||||
|
||||
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
|
||||
private async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currency))
|
||||
{
|
||||
|
@ -209,5 +380,15 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,42 +1,252 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.ModelBinders;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitpayClient;
|
||||
using NicolasDorier.RateLimits;
|
||||
|
||||
namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
[Route("apps")]
|
||||
public class UIPointOfSaleController : Controller
|
||||
{
|
||||
public UIPointOfSaleController(
|
||||
AppService appService,
|
||||
CurrencyNameTable currencies,
|
||||
StoreRepository storeRepository,
|
||||
AppService appService)
|
||||
UIInvoiceController invoiceController)
|
||||
{
|
||||
_currencies = currencies;
|
||||
_storeRepository = storeRepository;
|
||||
_appService = appService;
|
||||
_storeRepository = storeRepository;
|
||||
_invoiceController = invoiceController;
|
||||
}
|
||||
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
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")]
|
||||
public async Task<IActionResult> UpdatePointOfSale(string appId)
|
||||
{
|
||||
|
@ -113,6 +323,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
|||
return View("PointOfSale/UpdatePointOfSale", vm);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/settings/pos")]
|
||||
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();
|
||||
}
|
||||
|
||||
async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
|
||||
private async Task<string> GetStoreDefaultCurrentIfEmpty(string storeId, string currency)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(currency))
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Threading.Tasks;
|
|||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund.Controllers;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
@ -18,12 +19,14 @@ namespace BTCPayServer.Services.Apps
|
|||
public const string PaymentReceived = "PaymentReceived";
|
||||
public const string InfoUpdated = "InfoUpdated";
|
||||
public const string InvoiceError = "InvoiceError";
|
||||
private readonly UIAppsPublicController _AppsPublicController;
|
||||
|
||||
private readonly UICrowdfundController _crowdfundController;
|
||||
|
||||
public AppHub(UIAppsPublicController appsPublicController)
|
||||
public AppHub(UICrowdfundController crowdfundController)
|
||||
{
|
||||
_AppsPublicController = appsPublicController;
|
||||
_crowdfundController = crowdfundController;
|
||||
}
|
||||
|
||||
public async Task ListenToCrowdfundApp(string appId)
|
||||
{
|
||||
if (Context.Items.ContainsKey("app"))
|
||||
|
@ -35,16 +38,15 @@ namespace BTCPayServer.Services.Apps
|
|||
await Groups.AddToGroupAsync(Context.ConnectionId, appId);
|
||||
}
|
||||
|
||||
|
||||
public async Task CreateInvoice(ContributeToCrowdfund model)
|
||||
{
|
||||
model.RedirectToCheckout = false;
|
||||
_AppsPublicController.ControllerContext.HttpContext = Context.GetHttpContext();
|
||||
_crowdfundController.ControllerContext.HttpContext = Context.GetHttpContext();
|
||||
try
|
||||
{
|
||||
|
||||
var result =
|
||||
await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model, Context.ConnectionAborted);
|
||||
await _crowdfundController.ContributeToCrowdfund(Context.Items["app"].ToString(), model, Context.ConnectionAborted);
|
||||
switch (result)
|
||||
{
|
||||
case OkObjectResult okObjectResult:
|
||||
|
@ -54,16 +56,14 @@ namespace BTCPayServer.Services.Apps
|
|||
await Clients.Caller.SendCoreAsync(InvoiceError, new[] { objectResult.Value });
|
||||
break;
|
||||
default:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, Array.Empty<object>());
|
||||
break;
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
@ -75,6 +75,5 @@ namespace BTCPayServer.Services.Apps
|
|||
{
|
||||
route.MapHub<AppHub>("/apps/hub");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -252,7 +252,7 @@
|
|||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<partial
|
||||
name="/Views/UIAppsPublic/Crowdfund/ContributeForm.cshtml"
|
||||
name="Crowdfund/Public/ContributeForm"
|
||||
model="@(new ContributeToCrowdfund { ViewCrowdfundViewModel = Model, RedirectToCheckout = true })">
|
||||
</partial>
|
||||
</div>
|
|
@ -31,7 +31,7 @@
|
|||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
|
||||
@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>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
@using BTCPayServer.Models.AppViewModels
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@{
|
||||
Layout = "_LayoutPos";
|
||||
int[] customTipPercentages = Model.CustomTipPercentages;
|
||||
Layout = "PointOfSale/Public/_LayoutPos";
|
||||
var customTipPercentages = Model.CustomTipPercentages;
|
||||
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
|
||||
}
|
||||
|
||||
|
@ -162,7 +161,6 @@
|
|||
<form
|
||||
id="js-cart-pay-form"
|
||||
method="post"
|
||||
asp-controller="UIAppsPublic"
|
||||
asp-action="ViewPointOfSale"
|
||||
asp-route-appId="@Model.AppId"
|
||||
asp-antiforgery="false"
|
18
BTCPayServer/Views/Shared/PointOfSale/Public/Light.cshtml
Normal file
18
BTCPayServer/Views/Shared/PointOfSale/Public/Light.cshtml
Normal 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" />
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
}
|
||||
</div>
|
||||
<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">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
<input class="form-control" type="number" step="@Model.Step" name="amount" placeholder="Amount">
|
|
@ -1,4 +1,3 @@
|
|||
@using BTCPayServer.Models.AppViewModels
|
||||
@using BTCPayServer.Payments.Lightning
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@using BTCPayServer.Services.Stores
|
||||
|
@ -20,7 +19,7 @@
|
|||
|
||||
@{
|
||||
var store = await StoreRepository.FindStore(Model.StoreId);
|
||||
Layout = "_LayoutPos";
|
||||
Layout = "PointOfSale/Public/_LayoutPos";
|
||||
Context.Request.Query.TryGetValue("cryptocode", out var cryptoCodeValues);
|
||||
var cryptoCode = cryptoCodeValues.FirstOrDefault() ?? "BTC";
|
||||
var supported = store.GetSupportedPaymentMethods(BTCPayNetworkProvider).OfType<LNURLPaySupportedPaymentMethod>().OrderBy(method => method.CryptoCode == cryptoCode).FirstOrDefault();
|
|
@ -1,8 +1,7 @@
|
|||
@using BTCPayServer.Models.AppViewModels
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@{
|
||||
Layout = "_LayoutPos";
|
||||
Layout = "PointOfSale/Public/_LayoutPos";
|
||||
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
|
||||
}
|
||||
|
||||
|
@ -18,15 +17,14 @@
|
|||
</div>
|
||||
}
|
||||
<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 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)
|
||||
?.Replace("{Price}",item.Price.Formatted);
|
||||
buttonText = buttonText.Replace("{0}",item.Price.Formatted).Replace("{Price}",item.Price.Formatted);
|
||||
|
||||
<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">
|
||||
}
|
||||
|
@ -36,7 +34,7 @@
|
|||
{
|
||||
@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="requiresRefundEmail" value="@Model.RequiresRefundEmail.ToString()" />
|
||||
@{PayFormInputContent(item.BuyButtonText ?? Model.CustomButtonText, item.Price.Type, item.Price.Value, item.Price.Value);}
|
||||
|
@ -44,7 +42,7 @@
|
|||
}
|
||||
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()" />
|
||||
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
|
||||
@Safe.Raw(buttonText)
|
||||
|
@ -54,7 +52,6 @@
|
|||
}
|
||||
@if (item.Inventory.HasValue)
|
||||
{
|
||||
|
||||
<div class="w-100 pt-2 text-center text-muted">
|
||||
@if (item.Inventory > 0)
|
||||
{
|
||||
|
@ -78,7 +75,7 @@
|
|||
<div class="card px-0">
|
||||
@{CardBody("Custom Amount", "Create invoice to pay custom amount");}
|
||||
<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);}
|
||||
</form>
|
||||
@if (anyInventoryItems)
|
||||
|
@ -111,14 +108,13 @@
|
|||
<button class="btn btn-primary text-nowrap" type="submit">@buttonText</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void CardBody(string title, string description)
|
||||
{
|
||||
<div class="card-body my-auto pb-0">
|
||||
<h5 class="card-title">@title</h5>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
@if (!string.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@Safe.Raw(description)</p>
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
<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">
|
||||
@if (!string.IsNullOrEmpty(Model.CustomLogoLink))
|
||||
{
|
|
@ -13,7 +13,7 @@
|
|||
<h2 class="mb-0">@ViewData["Title"]</h2>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
@ -173,7 +173,7 @@
|
|||
<div class="accordion-body">
|
||||
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>
|
||||
</div>
|
||||
|
|
|
@ -103,10 +103,6 @@
|
|||
<a asp-action="@app.UpdateAction" asp-controller="UIApps" asp-route-appId="@app.Id" asp-route-storeId="@app.StoreId">Settings</a>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -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" />
|
||||
}
|
Loading…
Add table
Reference in a new issue