Compare commits
37 Commits
v1.11.0-rc
...
v1.11.2
Author | SHA1 | Date | |
---|---|---|---|
1c5fcfe094 | |||
45c1fb42ee | |||
64bd493996 | |||
ec6029409e | |||
c0fc31c69a | |||
b5d0188f21 | |||
0ccbaf4bd6 | |||
ed43fb2071 | |||
d67ebd957e | |||
19d360a543 | |||
7dc41ebcea | |||
1eb7c727f3 | |||
ede8171408 | |||
2538f3d8f6 | |||
ac64f5e395 | |||
1a7a731b54 | |||
86f4d48bcb | |||
83536bee88 | |||
abfd6ea1dc | |||
688e873f7a | |||
c88df08350 | |||
82586590a7 | |||
88c66f30f2 | |||
9132592717 | |||
c0ffab768a | |||
69190081c8 | |||
093206cf1e | |||
a0110b7570 | |||
6d65feca4c | |||
95be0242b6 | |||
79e121c3af | |||
676ac2fe46 | |||
8eabdab53a | |||
957fb09ffc | |||
4bffe117a9 | |||
05b01a13c8 | |||
08e21c1a5d |
@ -1,3 +1,4 @@
|
||||
using System.Web;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@ -21,6 +22,11 @@ namespace BTCPayServer.Abstractions.Services
|
||||
{
|
||||
return _htmlHelper.Raw(_htmlSanitizer.Sanitize(value));
|
||||
}
|
||||
|
||||
public IHtmlContent RawEncode(string value)
|
||||
{
|
||||
return _htmlHelper.Raw(HttpUtility.HtmlEncode(_htmlSanitizer.Sanitize(value)));
|
||||
}
|
||||
|
||||
public IHtmlContent Json(object model)
|
||||
{
|
||||
|
@ -16,7 +16,7 @@ namespace BTCPayServer
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTG_X = BTG_BTC * BTC_X",
|
||||
"BTG_BTC = exmo(BTG_BTC)",
|
||||
"BTG_BTC = gate(BTG_BTC)",
|
||||
},
|
||||
CryptoImagePath = "imlegacy/btg.svg",
|
||||
LightningImagePath = "imlegacy/btg-lightning.svg",
|
||||
|
@ -663,7 +663,7 @@ donation:
|
||||
Assert.Equal(3, vmview.Items.Length);
|
||||
Assert.Equal("good apple", vmview.Items[0].Title);
|
||||
Assert.Equal("orange", vmview.Items[1].Title);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price);
|
||||
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
||||
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
||||
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||
@ -680,7 +680,7 @@ donation:
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
invoices = await user.BitPay.GetInvoicesAsync();
|
||||
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
||||
Assert.NotNull(appleInvoice);
|
||||
Assert.Equal("good apple", appleInvoice.ItemDesc);
|
||||
@ -689,7 +689,7 @@ donation:
|
||||
var action = Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, choiceKey: "donation").Result);
|
||||
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
invoices = await user.BitPay.GetInvoicesAsync();
|
||||
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
|
||||
Assert.NotNull(donationInvoice);
|
||||
Assert.Equal("CAD", donationInvoice.Currency);
|
||||
|
@ -988,14 +988,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
|
||||
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
|
||||
Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view");
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".posItem:not(.d-none)")).Count);
|
||||
Assert.Equal(6, s.Driver.FindElements(By.CssSelector(".posItem.posItem--displayed")).Count);
|
||||
|
||||
var drinks = s.Driver.FindElement(By.CssSelector("label[for='Category-Drinks']"));
|
||||
Assert.Equal("Drinks", drinks.Text);
|
||||
drinks.Click();
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector(".posItem:not(.d-none)")));
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector(".posItem.posItem--displayed")));
|
||||
s.Driver.FindElement(By.CssSelector("label[for='Category-*']")).Click();
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".posItem:not(.d-none)")).Count);
|
||||
Assert.Equal(6, s.Driver.FindElements(By.CssSelector(".posItem.posItem--displayed")).Count);
|
||||
|
||||
s.Driver.Url = posBaseUrl + "/static";
|
||||
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
|
||||
@ -2064,7 +2064,6 @@ namespace BTCPayServer.Tests
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
@ -2101,7 +2100,6 @@ namespace BTCPayServer.Tests
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
@ -2176,7 +2174,6 @@ namespace BTCPayServer.Tests
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
@ -2199,6 +2196,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
s.Driver.WaitForElement(By.Id("PosItems"));
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
|
||||
var posUrl = s.Driver.Url;
|
||||
|
||||
// Select and clear
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click();
|
||||
@ -2207,34 +2205,81 @@ namespace BTCPayServer.Tests
|
||||
Thread.Sleep(250);
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
|
||||
|
||||
// Select items
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
// Select simple items
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("3,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Select item with inventory - two of it
|
||||
Assert.Equal("5 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(3, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("5,40 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Select items with minimum amount
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(4, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("7,20 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Select items with adjusted minimum amount
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) input[name='amount']")).Clear();
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) input[name='amount']")).SendKeys("2.3");
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("9,50 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Select items with custom amount
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).Clear();
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).SendKeys(".2");
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(6, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("9,70 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Select items with another custom amount
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).Clear();
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).SendKeys(".3");
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(7, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("10,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Discount: 10%
|
||||
s.Driver.ElementDoesNotExist(By.Id("CartDiscount"));
|
||||
s.Driver.FindElement(By.Id("Discount")).SendKeys("10");
|
||||
Assert.Contains("10% = 0,30 €", s.Driver.FindElement(By.Id("CartDiscount")).Text);
|
||||
Assert.Equal("2,70 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
Assert.Contains("10% = 1,00 €", s.Driver.FindElement(By.Id("CartDiscount")).Text);
|
||||
Assert.Equal("9,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Tip: 10%
|
||||
s.Driver.ElementDoesNotExist(By.Id("CartTip"));
|
||||
s.Driver.FindElement(By.Id("Tip-10")).Click();
|
||||
Assert.Contains("10% = 0,27 €", s.Driver.FindElement(By.Id("CartTip")).Text);
|
||||
Assert.Equal("2,97 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
Assert.Contains("10% = 0,90 €", s.Driver.FindElement(By.Id("CartTip")).Text);
|
||||
Assert.Equal("9,90 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Pay
|
||||
// Check values on checkout page
|
||||
s.Driver.FindElement(By.Id("CartSubmit")).Click();
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
|
||||
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
|
||||
Assert.Contains("2,97 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
Assert.Contains("9,90 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
|
||||
// Pay
|
||||
s.PayInvoice();
|
||||
|
||||
// Check inventory got updated and is now 3 instead of 5
|
||||
s.Driver.Navigate().GoToUrl(posUrl);
|
||||
Assert.Equal("3 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -292,7 +292,7 @@ retry:
|
||||
[Fact]
|
||||
public async Task CanGetRateCryptoCurrenciesByDefault()
|
||||
{
|
||||
string[] brokenShitcoins = { };
|
||||
string[] brokenShitcoins = { "BTG", "BTX" };
|
||||
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
@ -306,9 +306,13 @@ retry:
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
if (brokenShitcoins.Contains(key.Left))
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {key} because it is marked as broken");
|
||||
continue;
|
||||
}
|
||||
|
||||
TestLogs.LogInformation($"Testing {key}");
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
|
||||
@ -325,9 +329,12 @@ retry:
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
if (brokenShitcoins.Contains(key.Left))
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {key} because it is marked as broken");
|
||||
continue;
|
||||
}
|
||||
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
|
@ -2859,7 +2859,7 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId);
|
||||
|
||||
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
|
||||
@ -2874,7 +2874,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(StorageProvider.FileSystem,
|
||||
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
|
||||
|
||||
|
||||
await CanUploadRemoveFiles(controller);
|
||||
}
|
||||
|
||||
@ -2906,7 +2905,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
//create a temporary link to file
|
||||
var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
|
||||
new UIServerController.CreateTemporaryFileUrlViewModel()
|
||||
new UIServerController.CreateTemporaryFileUrlViewModel
|
||||
{
|
||||
IsDownload = true,
|
||||
TimeAmount = 1,
|
||||
|
@ -137,7 +137,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Watch Include="Views\**\*.*"></Watch>
|
||||
<Content Remove="Views\UIReports\StoreReports.cshtml" />
|
||||
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
||||
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
|
||||
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
||||
|
@ -53,7 +53,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
private static LightningAutomatedPayoutSettings ToModel(PayoutProcessorData data)
|
||||
{
|
||||
var blob = data.HasTypedBlob<LightningAutomatedPayoutBlob>().GetBlob();
|
||||
var blob = data.HasTypedBlob<LightningAutomatedPayoutBlob>().GetBlob() ?? new LightningAutomatedPayoutBlob();
|
||||
return new LightningAutomatedPayoutSettings()
|
||||
{
|
||||
PaymentMethod = data.PaymentMethod,
|
||||
|
@ -123,6 +123,7 @@ namespace BTCPayServer.Controllers
|
||||
var additionalData = metaData
|
||||
.Where(dict => !InvoiceAdditionalDataExclude.Contains(dict.Key))
|
||||
.ToDictionary(dict => dict.Key, dict => dict.Value);
|
||||
|
||||
var model = new InvoiceDetailsModel
|
||||
{
|
||||
StoreId = store.Id,
|
||||
@ -149,7 +150,6 @@ namespace BTCPayServer.Controllers
|
||||
StatusException = invoice.ExceptionStatus,
|
||||
Events = invoice.Events,
|
||||
Metadata = metaData,
|
||||
AdditionalData = additionalData,
|
||||
Archived = invoice.Archived,
|
||||
CanRefund = invoiceState.CanRefund(),
|
||||
Refunds = invoice.Refunds,
|
||||
@ -166,6 +166,13 @@ namespace BTCPayServer.Controllers
|
||||
model.CryptoPayments = details.CryptoPayments;
|
||||
model.Payments = details.Payments;
|
||||
model.Overpaid = details.Overpaid;
|
||||
|
||||
if (additionalData.ContainsKey("receiptData"))
|
||||
{
|
||||
model.ReceiptData = (Dictionary<string, object>)additionalData["receiptData"];
|
||||
additionalData.Remove("receiptData");
|
||||
}
|
||||
model.AdditionalData = additionalData;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
@ -227,14 +234,18 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
string txId = paymentData.GetPaymentId();
|
||||
string? link = GetTransactionLink(paymentMethodId, txId);
|
||||
|
||||
var paymentMethod = i.GetPaymentMethod(paymentMethodId);
|
||||
var amount = paymentData.GetValue();
|
||||
var rate = paymentMethod.Rate;
|
||||
var paid = (amount - paymentEntity.NetworkFee) * rate;
|
||||
|
||||
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
|
||||
{
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.PaidAmount.Net,
|
||||
Amount = amount,
|
||||
Paid = paid,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Net, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaidFormatted = _displayFormatter.Currency(paid, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Link = link,
|
||||
Id = txId,
|
||||
|
@ -10,6 +10,7 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MimeKit;
|
||||
|
||||
@ -75,33 +76,22 @@ namespace BTCPayServer.Controllers
|
||||
if (command.StartsWith("test", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var rule = vm.Rules[index];
|
||||
if (string.IsNullOrEmpty(rule.Subject) || string.IsNullOrEmpty(rule.Body) || string.IsNullOrEmpty(rule.To))
|
||||
try
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = "Please fill all required fields before testing"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var emailSettings = blob.EmailSettings;
|
||||
using var client = await emailSettings.CreateSmtpClient();
|
||||
var message = emailSettings.CreateMailMessage(MailboxAddress.Parse(rule.To), "(test) " + rule.Subject, rule.Body, true);
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Rule email saved and sent to {rule.To}. Please verify you received it.";
|
||||
var emailSettings = blob.EmailSettings;
|
||||
using var client = await emailSettings.CreateSmtpClient();
|
||||
var message = emailSettings.CreateMailMessage(MailboxAddress.Parse(rule.To), "(test) " + rule.Subject, rule.Body, true);
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Rule email saved and sent to {rule.To}. Please verify you received it.";
|
||||
|
||||
blob.EmailRules = vm.Rules;
|
||||
store.SetStoreBlob(blob);
|
||||
await _Repo.UpdateStore(store);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Error: " + ex.Message;
|
||||
}
|
||||
blob.EmailRules = vm.Rules;
|
||||
store.SetStoreBlob(blob);
|
||||
await _Repo.UpdateStore(store);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Error: " + ex.Message;
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -128,10 +118,18 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Required]
|
||||
public WebhookEventType Trigger { get; set; }
|
||||
|
||||
public bool CustomerEmail { get; set; }
|
||||
|
||||
[Required]
|
||||
[MailboxAddress]
|
||||
public string To { get; set; }
|
||||
public string Body { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Body { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/email-settings")]
|
||||
|
@ -238,7 +238,7 @@ namespace BTCPayServer.Data
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool CelebratePayment { get; set; } = true;
|
||||
|
||||
[DefaultValue(true)]
|
||||
[DefaultValue(false)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool PlaySoundOnPayment { get; set; } = false;
|
||||
|
||||
|
@ -203,6 +203,9 @@ public class UIFormsController : Controller
|
||||
if (store is null)
|
||||
return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
var request = _formDataService.GenerateInvoiceParametersFromForm(form);
|
||||
var inv = await invoiceController.CreateInvoiceCoreRaw(request, store, Request.GetAbsoluteRoot());
|
||||
if (inv.Price == 0 && inv.Type == InvoiceType.Standard && inv.ReceiptOptions?.Enabled is not false)
|
||||
@ -210,5 +213,15 @@ public class UIFormsController : Controller
|
||||
return RedirectToAction("InvoiceReceipt", "UIInvoice", new { invoiceId = inv.Id });
|
||||
}
|
||||
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = inv.Id });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Could not generate invoice: "+ e.Message
|
||||
});
|
||||
return await GetFormView(formData, form);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@ -50,45 +51,47 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}).Where(tuple => tuple.Data != null && tuple.Items.Any(item =>
|
||||
item.Inventory.HasValue &&
|
||||
updateAppInventory.Items.ContainsKey(item.Id)));
|
||||
foreach (var valueTuple in apps)
|
||||
updateAppInventory.Items.FirstOrDefault(i => i.Id == item.Id) != null));
|
||||
foreach (var app in apps)
|
||||
{
|
||||
foreach (var item1 in valueTuple.Items.Where(item =>
|
||||
updateAppInventory.Items.ContainsKey(item.Id)))
|
||||
foreach (var cartItem in updateAppInventory.Items)
|
||||
{
|
||||
var item = app.Items.FirstOrDefault(item => item.Id == cartItem.Id);
|
||||
if (item == null) continue;
|
||||
|
||||
if (updateAppInventory.Deduct)
|
||||
{
|
||||
item1.Inventory -= updateAppInventory.Items[item1.Id];
|
||||
item.Inventory -= cartItem.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
item1.Inventory += updateAppInventory.Items[item1.Id];
|
||||
item.Inventory += cartItem.Count;
|
||||
}
|
||||
}
|
||||
|
||||
switch (valueTuple.Data.AppType)
|
||||
switch (app.Data.AppType)
|
||||
{
|
||||
case PointOfSaleAppType.AppType:
|
||||
((PointOfSaleSettings)valueTuple.Settings).Template =
|
||||
AppService.SerializeTemplate(valueTuple.Items);
|
||||
((PointOfSaleSettings)app.Settings).Template =
|
||||
AppService.SerializeTemplate(app.Items);
|
||||
break;
|
||||
case CrowdfundAppType.AppType:
|
||||
((CrowdfundSettings)valueTuple.Settings).PerksTemplate =
|
||||
AppService.SerializeTemplate(valueTuple.Items);
|
||||
((CrowdfundSettings)app.Settings).PerksTemplate =
|
||||
AppService.SerializeTemplate(app.Items);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
valueTuple.Data.SetSettings(valueTuple.Settings);
|
||||
await _appService.UpdateOrCreateApp(valueTuple.Data);
|
||||
app.Data.SetSettings(app.Settings);
|
||||
await _appService.UpdateOrCreateApp(app.Data);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
else if (evt is InvoiceEvent invoiceEvent)
|
||||
{
|
||||
Dictionary<string, int> cartItems = null;
|
||||
List<PosCartItem> cartItems = null;
|
||||
bool deduct;
|
||||
switch (invoiceEvent.Name)
|
||||
{
|
||||
@ -104,8 +107,8 @@ namespace BTCPayServer.HostedServices
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
|
||||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems)))
|
||||
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
|
||||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems))
|
||||
{
|
||||
var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice);
|
||||
|
||||
@ -114,13 +117,18 @@ namespace BTCPayServer.HostedServices
|
||||
return;
|
||||
}
|
||||
|
||||
var items = cartItems ?? new Dictionary<string, int>();
|
||||
var items = cartItems?.ToList() ?? new List<PosCartItem>();
|
||||
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode))
|
||||
{
|
||||
items.TryAdd(invoiceEvent.Invoice.Metadata.ItemCode, 1);
|
||||
items.Add(new PosCartItem
|
||||
{
|
||||
Id = invoiceEvent.Invoice.Metadata.ItemCode,
|
||||
Count = 1,
|
||||
Price = invoiceEvent.Invoice.Price
|
||||
});
|
||||
}
|
||||
|
||||
_eventAggregator.Publish(new UpdateAppInventory()
|
||||
_eventAggregator.Publish(new UpdateAppInventory
|
||||
{
|
||||
Deduct = deduct,
|
||||
Items = items,
|
||||
@ -134,7 +142,7 @@ namespace BTCPayServer.HostedServices
|
||||
public class UpdateAppInventory
|
||||
{
|
||||
public string[] AppId { get; set; }
|
||||
public Dictionary<string, int> Items { get; set; }
|
||||
public List<PosCartItem> Items { get; set; }
|
||||
public bool Deduct { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
|
@ -127,6 +127,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||
public string NotificationEmail { get; internal set; }
|
||||
public Dictionary<string, object> Metadata { get; set; }
|
||||
public Dictionary<string, object> ReceiptData { get; set; }
|
||||
public Dictionary<string, object> AdditionalData { get; set; }
|
||||
public List<PaymentEntity> Payments { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
@ -147,12 +147,17 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
{
|
||||
|
||||
// not allowing negative tips or discounts
|
||||
if (tip < 0 || discount < 0)
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
}
|
||||
|
||||
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;
|
||||
var currentView = viewType ?? settings.DefaultView;
|
||||
@ -166,7 +171,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
decimal? price;
|
||||
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
Dictionary<string, int> cartItems = null;
|
||||
List<PosCartItem> cartItems = null;
|
||||
ViewPointOfSaleViewModel.Item[] choices = null;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
@ -203,16 +208,15 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
return NotFound();
|
||||
|
||||
title = settings.Title;
|
||||
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
|
||||
// if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
|
||||
price = amount;
|
||||
if (currentView == PosViewType.Cart &&
|
||||
AppService.TryParsePosCartItems(jposData, out cartItems))
|
||||
if (currentView == PosViewType.Cart && AppService.TryParsePosCartItems(jposData, out cartItems))
|
||||
{
|
||||
price = 0.0m;
|
||||
choices = AppService.Parse(settings.Template, false);
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
var itemChoice = choices.FirstOrDefault(c => c.Id == cartItem.Key);
|
||||
var itemChoice = choices.FirstOrDefault(item => item.Id == cartItem.Id);
|
||||
if (itemChoice == null)
|
||||
return NotFound();
|
||||
|
||||
@ -220,20 +224,21 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
switch (itemChoice.Inventory)
|
||||
{
|
||||
case int i when i <= 0:
|
||||
case <= 0:
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
case int inventory when inventory < cartItem.Value:
|
||||
case { } inventory when inventory < cartItem.Count:
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
}
|
||||
}
|
||||
|
||||
decimal expectedCartItemPrice = 0;
|
||||
if (itemChoice.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
{
|
||||
expectedCartItemPrice = itemChoice.Price ?? 0;
|
||||
}
|
||||
var expectedCartItemPrice = itemChoice.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup
|
||||
? itemChoice.Price ?? 0
|
||||
: 0;
|
||||
|
||||
if (cartItem.Price < expectedCartItemPrice)
|
||||
cartItem.Price = expectedCartItemPrice;
|
||||
|
||||
price += expectedCartItemPrice * cartItem.Value;
|
||||
price += cartItem.Price * cartItem.Count;
|
||||
}
|
||||
if (customAmount is { } c)
|
||||
price += c;
|
||||
@ -310,7 +315,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
Amount = price,
|
||||
Currency = settings.Currency,
|
||||
Metadata = new InvoiceMetadata()
|
||||
Metadata = new InvoiceMetadata
|
||||
{
|
||||
ItemCode = choice?.Id,
|
||||
ItemDesc = title,
|
||||
@ -342,9 +347,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
var receiptData = new JObject();
|
||||
if (choice is not null)
|
||||
{
|
||||
receiptData = JObject.FromObject(new Dictionary<string, string>()
|
||||
receiptData = JObject.FromObject(new Dictionary<string, string>
|
||||
{
|
||||
{"Title", choice.Title}, {"Description", choice.Description},
|
||||
{"Title", choice.Title},
|
||||
{"Description", choice.Description},
|
||||
});
|
||||
}
|
||||
else if (jposData is not null)
|
||||
@ -353,31 +359,33 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
receiptData = new JObject();
|
||||
if (cartItems is not null && choices is not null)
|
||||
{
|
||||
var selectedChoices = choices.Where(item => cartItems.Keys.Contains(item.Id))
|
||||
var posCartItems = cartItems.ToList();
|
||||
var selectedChoices = choices
|
||||
.Where(item => posCartItems.Any(cartItem => cartItem.Id == item.Id))
|
||||
.ToDictionary(item => item.Id);
|
||||
var cartData = new JObject();
|
||||
foreach (KeyValuePair<string, int> cartItem in cartItems)
|
||||
foreach (PosCartItem cartItem in posCartItems)
|
||||
{
|
||||
if (selectedChoices.TryGetValue(cartItem.Key, out var selectedChoice))
|
||||
{
|
||||
cartData.Add(selectedChoice.Title ?? selectedChoice.Id,
|
||||
$"{(selectedChoice.Price is null ? "Any price" : $"{_displayFormatter.Currency((decimal)selectedChoice.Price.Value, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)}")} x {cartItem.Value} = {(selectedChoice.Price is null ? "Any price" : $"{_displayFormatter.Currency(((decimal)selectedChoice.Price.Value) * cartItem.Value, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)}")}");
|
||||
|
||||
}
|
||||
if (!selectedChoices.TryGetValue(cartItem.Id, out var selectedChoice)) continue;
|
||||
var singlePrice = _displayFormatter.Currency(cartItem.Price, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var totalPrice = _displayFormatter.Currency(cartItem.Price * cartItem.Count, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var ident = selectedChoice.Title ?? selectedChoice.Id;
|
||||
var key = selectedChoice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})";
|
||||
cartData.Add(key, $"{cartItem.Count} x {singlePrice} = {totalPrice}");
|
||||
}
|
||||
receiptData.Add("Cart", cartData);
|
||||
}
|
||||
|
||||
receiptData.Add("Subtotal", _displayFormatter.Currency(appPosData.Subtotal, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
if (appPosData.DiscountAmount > 0)
|
||||
{
|
||||
receiptData.Add("Discount",
|
||||
$"{_displayFormatter.Currency(appPosData.DiscountAmount, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)} {(appPosData.DiscountPercentage > 0 ? $"({appPosData.DiscountPercentage}%)" : string.Empty)}");
|
||||
var discountFormatted = _displayFormatter.Currency(appPosData.DiscountAmount, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
receiptData.Add("Discount", appPosData.DiscountPercentage > 0 ? $"{appPosData.DiscountPercentage}% = {discountFormatted}" : discountFormatted);
|
||||
}
|
||||
|
||||
if (appPosData.Tip > 0)
|
||||
{
|
||||
receiptData.Add("Tip", _displayFormatter.Currency(appPosData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
}
|
||||
receiptData.Add("Total", _displayFormatter.Currency(appPosData.Total, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
}
|
||||
entity.Metadata.SetAdditionalData("receiptData", receiptData);
|
||||
|
||||
@ -616,7 +624,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
return View("PointOfSale/UpdatePointOfSale", vm);
|
||||
}
|
||||
|
||||
var storeBlob = GetCurrentStore().GetStoreBlob();
|
||||
var settings = new PointOfSaleSettings
|
||||
{
|
||||
Title = vm.Title,
|
||||
@ -635,11 +642,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
RedirectUrl = vm.RedirectUrl,
|
||||
Description = vm.Description,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
RedirectAutomatically =
|
||||
string.IsNullOrEmpty(vm.RedirectAutomatically) ? null : bool.Parse(vm.RedirectAutomatically)
|
||||
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? null : bool.Parse(vm.RedirectAutomatically),
|
||||
FormId = vm.FormId
|
||||
};
|
||||
|
||||
settings.FormId = vm.FormId;
|
||||
app.Name = vm.AppName;
|
||||
app.SetSettings(settings);
|
||||
await _appService.UpdateOrCreateApp(app);
|
||||
|
@ -411,7 +411,6 @@ namespace BTCPayServer.Services.Apps
|
||||
return false;
|
||||
if (cartObject is null)
|
||||
return false;
|
||||
|
||||
cartItems = new();
|
||||
foreach (var o in cartObject.OfType<JObject>())
|
||||
{
|
||||
@ -427,6 +426,29 @@ namespace BTCPayServer.Services.Apps
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParsePosCartItems(JObject? posData, [MaybeNullWhen(false)] out List<PosCartItem> cartItems)
|
||||
{
|
||||
cartItems = null;
|
||||
if (posData is null)
|
||||
return false;
|
||||
if (!posData.TryGetValue("cart", out var cartObject))
|
||||
return false;
|
||||
|
||||
cartItems = new List<PosCartItem>();
|
||||
foreach (var o in cartObject.OfType<JObject>())
|
||||
{
|
||||
var id = o.GetValue("id", StringComparison.InvariantCulture)?.ToString();
|
||||
if (id == null) continue;
|
||||
var countStr = o.GetValue("count", StringComparison.InvariantCulture)?.ToString() ?? string.Empty;
|
||||
var price = o.GetValue("price")?.Value<decimal>() ?? 0m;
|
||||
if (int.TryParse(countStr, out var count))
|
||||
{
|
||||
cartItems.Add(new PosCartItem { Id = id, Count = count, Price = price });
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task SetDefaultSettings(AppData appData, string defaultCurrency)
|
||||
{
|
||||
@ -449,6 +471,13 @@ namespace BTCPayServer.Services.Apps
|
||||
#nullable restore
|
||||
}
|
||||
|
||||
public class PosCartItem
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public int Count { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
|
||||
public class ItemStats
|
||||
{
|
||||
public string ItemCode { get; set; }
|
||||
|
@ -439,7 +439,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
set;
|
||||
}
|
||||
/// <summary>
|
||||
/// Minumum due to consider the invoice paid (can be negative if overpaid)
|
||||
/// Minimum due to consider the invoice paid (can be negative if overpaid)
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal MinimumNetDue { get; set; }
|
||||
|
@ -620,8 +620,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
if (queryObject.InvoiceId.Length > 1)
|
||||
{
|
||||
var statusSet = queryObject.InvoiceId.ToHashSet().ToArray();
|
||||
query = query.Where(i => statusSet.Contains(i.Id));
|
||||
var idSet = queryObject.InvoiceId.ToHashSet().ToArray();
|
||||
query = query.Where(i => idSet.Contains(i.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -662,54 +662,52 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
if (queryObject.OrderId is { Length: > 0 })
|
||||
{
|
||||
var statusSet = queryObject.OrderId.ToHashSet().ToArray();
|
||||
query = query.Where(i => statusSet.Contains(i.OrderId));
|
||||
var orderIdSet = queryObject.OrderId.ToHashSet().ToArray();
|
||||
query = query.Where(i => orderIdSet.Contains(i.OrderId));
|
||||
}
|
||||
if (queryObject.ItemCode is { Length: > 0 })
|
||||
{
|
||||
var statusSet = queryObject.ItemCode.ToHashSet().ToArray();
|
||||
query = query.Where(i => statusSet.Contains(i.ItemCode));
|
||||
var itemCodeSet = queryObject.ItemCode.ToHashSet().ToArray();
|
||||
query = query.Where(i => itemCodeSet.Contains(i.ItemCode));
|
||||
}
|
||||
|
||||
if (queryObject.Status is { Length: > 0 })
|
||||
var statusSet = queryObject.Status is { Length: > 0 }
|
||||
? queryObject.Status.Select(s => s.ToLowerInvariant()).ToHashSet()
|
||||
: new HashSet<string>();
|
||||
var exceptionStatusSet = queryObject.ExceptionStatus is { Length: > 0 }
|
||||
? queryObject.ExceptionStatus.Select(NormalizeExceptionStatus).ToHashSet()
|
||||
: new HashSet<string>();
|
||||
|
||||
// We make sure here that the old filters still work
|
||||
if (statusSet.Contains("paid"))
|
||||
statusSet.Add("processing");
|
||||
if (statusSet.Contains("processing"))
|
||||
statusSet.Add("paid");
|
||||
if (statusSet.Contains("confirmed"))
|
||||
{
|
||||
var statusSet = queryObject.Status.ToHashSet();
|
||||
// We make sure here that the old filters still work
|
||||
foreach (var status in queryObject.Status.Select(s => s.ToLowerInvariant()))
|
||||
{
|
||||
if (status == "paid")
|
||||
statusSet.Add("processing");
|
||||
if (status == "processing")
|
||||
statusSet.Add("paid");
|
||||
if (status == "confirmed")
|
||||
{
|
||||
statusSet.Add("complete");
|
||||
statusSet.Add("settled");
|
||||
}
|
||||
if (status == "settled")
|
||||
{
|
||||
statusSet.Add("complete");
|
||||
statusSet.Add("confirmed");
|
||||
}
|
||||
if (status == "complete")
|
||||
{
|
||||
statusSet.Add("settled");
|
||||
statusSet.Add("confirmed");
|
||||
}
|
||||
}
|
||||
query = query.Where(i => statusSet.Contains(i.Status));
|
||||
statusSet.Add("complete");
|
||||
statusSet.Add("settled");
|
||||
}
|
||||
if (statusSet.Contains("settled"))
|
||||
{
|
||||
statusSet.Add("complete");
|
||||
statusSet.Add("confirmed");
|
||||
}
|
||||
if (statusSet.Contains("complete"))
|
||||
{
|
||||
statusSet.Add("settled");
|
||||
statusSet.Add("confirmed");
|
||||
}
|
||||
|
||||
if (statusSet.Any() || exceptionStatusSet.Any())
|
||||
{
|
||||
query = query.Where(i => statusSet.Contains(i.Status) || exceptionStatusSet.Contains(i.ExceptionStatus));
|
||||
}
|
||||
|
||||
if (queryObject.Unusual != null)
|
||||
{
|
||||
var unused = queryObject.Unusual.Value;
|
||||
query = query.Where(i => unused == (i.Status == "invalid" || !string.IsNullOrEmpty(i.ExceptionStatus)));
|
||||
}
|
||||
|
||||
if (queryObject.ExceptionStatus is { Length: > 0 })
|
||||
{
|
||||
var exceptionStatusSet = queryObject.ExceptionStatus.Select(s => NormalizeExceptionStatus(s)).ToHashSet().ToArray();
|
||||
query = query.Where(i => exceptionStatusSet.Contains(i.ExceptionStatus));
|
||||
var unusual = queryObject.Unusual.Value;
|
||||
query = query.Where(i => unusual == (i.Status == "invalid" || !string.IsNullOrEmpty(i.ExceptionStatus)));
|
||||
}
|
||||
|
||||
query = query.OrderByDescending(q => q.Created);
|
||||
@ -719,6 +717,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
if (queryObject.Take != null)
|
||||
query = query.Take(queryObject.Take.Value);
|
||||
|
||||
return query;
|
||||
}
|
||||
public Task<InvoiceEntity[]> GetInvoices(InvoiceQuery queryObject)
|
||||
|
@ -106,13 +106,13 @@ public class OnChainWalletReportProvider : ReportProvider
|
||||
}
|
||||
var objects = await WalletRepository.GetWalletObjects(new GetWalletObjectsQuery()
|
||||
{
|
||||
Ids = queryContext.Data.Select(d => (string)d[2]).ToArray(),
|
||||
Ids = queryContext.Data.Select(d => (string)d[2]!).ToArray(),
|
||||
WalletId = walletId,
|
||||
Type = "tx"
|
||||
});
|
||||
foreach (var row in queryContext.Data)
|
||||
{
|
||||
if (!objects.TryGetValue(new WalletObjectId(walletId, "tx", (string)row[2]), out var txObject))
|
||||
if (!objects.TryGetValue(new WalletObjectId(walletId, "tx", (string)row[2]!), out var txObject))
|
||||
continue;
|
||||
var invoiceId = txObject.GetLinks().Where(t => t.type == "invoice").Select(t => t.id).FirstOrDefault();
|
||||
row[3] = invoiceId;
|
||||
|
@ -78,8 +78,8 @@ public class ProductsReportProvider : ReportProvider
|
||||
}
|
||||
else
|
||||
{
|
||||
var posData = i.Metadata.PosData?.ToObject<PosAppData>();
|
||||
if (posData.Cart is { } cart)
|
||||
var posData = i.Metadata?.PosData?.ToObject<PosAppData>();
|
||||
if (posData?.Cart is { } cart)
|
||||
{
|
||||
foreach (var item in cart)
|
||||
{
|
||||
|
@ -1,3 +1,4 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@ -17,18 +18,20 @@ namespace BTCPayServer.Services.Reporting
|
||||
public DateTimeOffset To { get; }
|
||||
public ViewDefinition? ViewDefinition { get; set; }
|
||||
|
||||
public IList<object> AddData()
|
||||
public IList<object?> AddData()
|
||||
{
|
||||
var l = CreateData();
|
||||
Data.Add(l);
|
||||
return l;
|
||||
}
|
||||
|
||||
public IList<object> CreateData()
|
||||
public IList<object?> CreateData()
|
||||
{
|
||||
return new List<object>(ViewDefinition.Fields.Count);
|
||||
if (ViewDefinition is null)
|
||||
throw new InvalidOperationException("You need to initialize ViewDefinition first");
|
||||
return new List<object?>(ViewDefinition.Fields.Count);
|
||||
}
|
||||
|
||||
public IList<IList<object>> Data { get; set; } = new List<IList<object>>();
|
||||
public IList<IList<object?>> Data { get; set; } = new List<IList<object?>>();
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage
|
||||
BlobUrlAccess access = BlobUrlAccess.Read)
|
||||
{
|
||||
|
||||
var localFileDescriptor = new TemporaryLocalFileDescriptor()
|
||||
var localFileDescriptor = new TemporaryLocalFileDescriptor
|
||||
{
|
||||
Expiry = expiry,
|
||||
FileId = storedFile.Id,
|
||||
@ -60,9 +60,11 @@ namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage
|
||||
};
|
||||
var name = Guid.NewGuid().ToString();
|
||||
var fullPath = Path.Combine(_datadirs.Value.TempStorageDir, name);
|
||||
if (!File.Exists(fullPath))
|
||||
var fileInfo = new FileInfo(fullPath);
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
File.Create(fullPath).Dispose();
|
||||
fileInfo.Directory?.Create();
|
||||
await File.Create(fileInfo.FullName).DisposeAsync();
|
||||
}
|
||||
|
||||
await File.WriteAllTextAsync(Path.Combine(_datadirs.Value.TempStorageDir, name), JsonConvert.SerializeObject(localFileDescriptor));
|
||||
|
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Mime;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage;
|
||||
|
||||
public class TemporaryLocalFileController : Controller
|
||||
{
|
||||
private readonly StoredFileRepository _storedFileRepository;
|
||||
private readonly IOptions<DataDirectories> _dataDirectories;
|
||||
|
||||
public TemporaryLocalFileController(StoredFileRepository storedFileRepository,
|
||||
IOptions<DataDirectories> dataDirectories)
|
||||
{
|
||||
_storedFileRepository = storedFileRepository;
|
||||
_dataDirectories = dataDirectories;
|
||||
}
|
||||
|
||||
[HttpGet($"~/{FileSystemFileProviderService.LocalStorageDirectoryName}tmp/{{tmpFileId}}")]
|
||||
public async Task<IActionResult> GetTmpLocalFile(string tmpFileId)
|
||||
{
|
||||
var path = Path.Combine(_dataDirectories.Value.TempStorageDir, tmpFileId);
|
||||
|
||||
if (!System.IO.File.Exists(path))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var text = await System.IO.File.ReadAllTextAsync(path);
|
||||
var descriptor = JsonConvert.DeserializeObject<TemporaryLocalFileDescriptor>(text);
|
||||
if (descriptor.Expiry < DateTime.UtcNow)
|
||||
{
|
||||
System.IO.File.Delete(path);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var storedFile = _storedFileRepository.GetFile(descriptor.FileId).GetAwaiter().GetResult();
|
||||
|
||||
ControllerContext.HttpContext.Response.Headers["Content-Disposition"] =
|
||||
ControllerContext.HttpContext.Request.Query.ContainsKey("download") ? "attachment" : "inline";
|
||||
ControllerContext.HttpContext.Response.Headers["Content-Security-Policy"] = "script-src ;";
|
||||
ControllerContext.HttpContext.Response.Headers["X-Content-Type-Options"] = "nosniff";
|
||||
path = Path.Combine(_dataDirectories.Value.StorageDir, storedFile.StorageFileName);
|
||||
var fileContent = await System.IO.File.ReadAllBytesAsync(path);
|
||||
return File(fileContent, MediaTypeNames.Application.Octet, storedFile.FileName);
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.FileProviders.Physical;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Storage.Services.Providers.FileSystemStorage
|
||||
{
|
||||
public class TemporaryLocalFileProvider : IFileProvider
|
||||
{
|
||||
private readonly DirectoryInfo _fileRoot;
|
||||
private readonly StoredFileRepository _storedFileRepository;
|
||||
private readonly DirectoryInfo _root;
|
||||
|
||||
public TemporaryLocalFileProvider(DirectoryInfo tmpRoot, DirectoryInfo fileRoot, StoredFileRepository storedFileRepository)
|
||||
{
|
||||
_fileRoot = fileRoot;
|
||||
_storedFileRepository = storedFileRepository;
|
||||
_root = tmpRoot;
|
||||
}
|
||||
public IFileInfo GetFileInfo(string tmpFileId)
|
||||
{
|
||||
tmpFileId = tmpFileId.TrimStart('/', '\\');
|
||||
var path = Path.Combine(_root.FullName, tmpFileId);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return new NotFoundFileInfo(tmpFileId);
|
||||
}
|
||||
|
||||
var text = File.ReadAllText(path);
|
||||
var descriptor = JsonConvert.DeserializeObject<TemporaryLocalFileDescriptor>(text);
|
||||
if (descriptor.Expiry < DateTime.UtcNow)
|
||||
{
|
||||
File.Delete(path);
|
||||
return new NotFoundFileInfo(tmpFileId);
|
||||
}
|
||||
|
||||
var storedFile = _storedFileRepository.GetFile(descriptor.FileId).GetAwaiter().GetResult();
|
||||
return new PhysicalFileInfo(new FileInfo(Path.Combine(_fileRoot.FullName, storedFile.StorageFileName)));
|
||||
}
|
||||
|
||||
public IDirectoryContents GetDirectoryContents(string subpath)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public IChangeToken Watch(string filter)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -41,10 +41,6 @@ namespace BTCPayServer.Storage
|
||||
Directory.CreateDirectory(datadirs.Value.TempDir);
|
||||
}
|
||||
|
||||
var tmpdirInfo = Directory.Exists(datadirs.Value.TempStorageDir)
|
||||
? new DirectoryInfo(datadirs.Value.TempStorageDir)
|
||||
: Directory.CreateDirectory(datadirs.Value.TempStorageDir);
|
||||
|
||||
builder.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
ServeUnknownFileTypes = true,
|
||||
@ -52,14 +48,6 @@ namespace BTCPayServer.Storage
|
||||
FileProvider = new PhysicalFileProvider(dirInfo.FullName),
|
||||
OnPrepareResponse = HandleStaticFileResponse()
|
||||
});
|
||||
builder.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
ServeUnknownFileTypes = true,
|
||||
RequestPath = new PathString($"/{FileSystemFileProviderService.LocalStorageDirectoryName}tmp"),
|
||||
FileProvider = new TemporaryLocalFileProvider(tmpdirInfo, dirInfo,
|
||||
builder.ApplicationServices.GetService<StoredFileRepository>()),
|
||||
OnPrepareResponse = HandleStaticFileResponse()
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -71,10 +59,7 @@ namespace BTCPayServer.Storage
|
||||
{
|
||||
return context =>
|
||||
{
|
||||
if (context.Context.Request.Query.ContainsKey("download"))
|
||||
{
|
||||
context.Context.Response.Headers["Content-Disposition"] = "attachment";
|
||||
}
|
||||
context.Context.Response.Headers["Content-Disposition"] = context.Context.Request.Query.ContainsKey("download")? "attachment" : "inline";
|
||||
context.Context.Response.Headers["Content-Security-Policy"] = "script-src ;";
|
||||
context.Context.Response.Headers["X-Content-Type-Options"] = "nosniff";
|
||||
};
|
||||
|
@ -1,26 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using System.Xml.Linq;
|
||||
using BTCPayServer.Configuration;
|
||||
|
||||
namespace BTCPayServer.TagHelpers;
|
||||
|
||||
|
||||
[HtmlTargetElement(Attributes = "[cheat-mode]")]
|
||||
public class CheatModeTagHelper
|
||||
[HtmlTargetElement(Attributes = "cheat-mode")]
|
||||
public class CheatModeTagHelper : TagHelper
|
||||
{
|
||||
public CheatModeTagHelper(BTCPayServerOptions env)
|
||||
{
|
||||
Env = env;
|
||||
}
|
||||
|
||||
public BTCPayServerOptions Env { get; }
|
||||
BTCPayServerOptions Env { get; }
|
||||
public bool CheatMode { get; set; }
|
||||
public void Process(TagHelperContext context, TagHelperOutput output)
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
if (Env.CheatMode != CheatMode)
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@using BTCPayServer.Services
|
||||
@using Newtonsoft.Json.Linq;
|
||||
@using Newtonsoft.Json.Linq
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
@ -56,22 +56,19 @@
|
||||
@for (var index = 0; index < Model.Items.Length; index++)
|
||||
{
|
||||
var item = Model.Items[index];
|
||||
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var formatted = GetItemPriceFormatted(item);
|
||||
var inStock = item.Inventory is null or > 0;
|
||||
var buttonText = string.IsNullOrEmpty(item.BuyButtonText)
|
||||
? item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText
|
||||
: item.BuyButtonText;
|
||||
buttonText = buttonText.Replace("{0}", formatted).Replace("{Price}", formatted);
|
||||
|
||||
<div class="col posItem" :class="{ 'posItem--inStock': inStock(@index) }" data-index="@index" data-search="@Safe.Raw(item.Title) @Safe.Raw(item.Description)" data-categories="@(new JArray(item.Categories).ToString())">
|
||||
var categories = new JArray(item.Categories ?? new object[] { });
|
||||
<div class="col posItem posItem--displayed" :class="{ 'posItem--inStock': inStock(@index) }" data-index="@index" data-search="@Safe.RawEncode(item.Title + " " + item.Description)" data-categories='@Safe.Json(categories)'>
|
||||
<div class="card h-100 px-0" v-on:click="addToCart(@index)">
|
||||
@if (!string.IsNullOrWhiteSpace(item.Image))
|
||||
{
|
||||
<img class="card-img-top" src="@item.Image" alt="@Safe.Raw(item.Title)" asp-append-version="true">
|
||||
<img class="card-img-top" src="@item.Image" alt="@item.Title" asp-append-version="true">
|
||||
}
|
||||
<div class="card-body p-3 d-flex flex-column gap-2">
|
||||
<h5 class="card-title m-0">@Safe.Raw(item.Title)</h5>
|
||||
@ -86,7 +83,7 @@
|
||||
}
|
||||
@if (item.Inventory.HasValue)
|
||||
{
|
||||
<span class="badge text-bg-warning" v-text="inventoryText(@index)">
|
||||
<span class="badge text-bg-warning inventory" v-text="inventoryText(@index)">
|
||||
@(item.Inventory > 0 ? $"{item.Inventory} left" : "Sold out")
|
||||
</span>
|
||||
}
|
||||
@ -98,11 +95,18 @@
|
||||
</div>
|
||||
@if (inStock)
|
||||
{
|
||||
<div class="card-footer bg-transparent border-0 pt-0 pb-3">
|
||||
<form class="card-footer bg-transparent border-0 pt-0 pb-3">
|
||||
@if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed)
|
||||
{
|
||||
<div class="input-group mb-2">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
<input class="form-control" type="number" min="@(item.Price ?? 0)" step="@Model.Step" name="amount" placeholder="Amount" value="@item.Price" required v-on:click.stop>
|
||||
</div>
|
||||
}
|
||||
<button type="button" class="btn btn-primary w-100" :disabled="!inStock(@index)">
|
||||
@Safe.Raw(buttonText)
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="posItem-added"><vc:icon symbol="checkmark" /></div>
|
||||
}
|
||||
</div>
|
||||
@ -144,7 +148,7 @@
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex align-items-center gap-2 justify-content-end quantity">
|
||||
<span class="badge text-bg-warning" v-if="item.inventory">
|
||||
<span class="badge text-bg-warning inventory" v-if="item.inventory">
|
||||
{{ item.inventory > 0 ? `${item.inventory} left` : "Sold out" }}
|
||||
</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@ -159,7 +163,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle text-end">
|
||||
{{ formatCurrency(item.price, true) }}
|
||||
{{ formatCurrency(item.price||0, true) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -35,7 +35,7 @@
|
||||
: item.BuyButtonText;
|
||||
buttonText = buttonText.Replace("{0}", formatted).Replace("{Price}", formatted);
|
||||
|
||||
<div class="col">
|
||||
<div class="col posItem posItem--displayed">
|
||||
<div class="card h-100 px-0" data-id="@x">
|
||||
@if (!string.IsNullOrWhiteSpace(item.Image))
|
||||
{
|
||||
|
@ -1,77 +1,111 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model (Dictionary<string, object> Items, int Level)
|
||||
|
||||
@functions {
|
||||
|
||||
private bool IsValidURL(string source)
|
||||
{
|
||||
return Uri.TryCreate(source, UriKind.Absolute, out var uriResult) &&
|
||||
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@if (Model.Items.Count > 0)
|
||||
@if (Model.Items.Any())
|
||||
{
|
||||
<table class="table my-0" v-pre>
|
||||
@foreach (var (key, value) in Model.Items)
|
||||
@if (Model.Items.ContainsKey("Cart"))
|
||||
{
|
||||
<tr>
|
||||
@if (value is string str)
|
||||
<tbody>
|
||||
@foreach (var (key, value) in (Dictionary <string, object>)Model.Items["Cart"])
|
||||
{
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
<tr>
|
||||
<td>@key</td>
|
||||
<td class="text-end">@value</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
<tfoot style="border-top-width:3px">
|
||||
@if (Model.Items.ContainsKey("Subtotal"))
|
||||
{
|
||||
<tr>
|
||||
<td>Subtotal</td>
|
||||
<td class="text-end">@Model.Items["Subtotal"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Items.ContainsKey("Discount"))
|
||||
{
|
||||
<tr>
|
||||
<td>Discount</td>
|
||||
<td class="text-end">@Model.Items["Discount"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Items.ContainsKey("Tip"))
|
||||
{
|
||||
<tr>
|
||||
<td>Tip</td>
|
||||
<td class="text-end">@Model.Items["Tip"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Items.ContainsKey("Total"))
|
||||
{
|
||||
<tr style="border-top-width:3px">
|
||||
<td>Total</td>
|
||||
<td class="text-end">@Model.Items["Total"]</td>
|
||||
</tr>
|
||||
}
|
||||
</tfoot>
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var (key, value) in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
@if (value is string str)
|
||||
{
|
||||
<th class="w-150px">@key</th>
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
<th class="w-225px">@key</th>
|
||||
}
|
||||
<td style="white-space:pre-wrap">@* Explicitely remove whitespace at front here *@@if (IsValidURL(str)){<a href="@str" target="_blank" rel="noreferrer noopener">@str</a>}else {@str.Trim()}</td>
|
||||
}
|
||||
<td style="white-space:pre-wrap">@* Explicitely remove whitespace at front here *@@if (IsValidURL(str))
|
||||
{
|
||||
<a href="@str" target="_blank" rel="noreferrer noopener">@str</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
@str.Trim()
|
||||
}
|
||||
</td>
|
||||
}
|
||||
else if (value is Dictionary<string, object> {Count: > 0 } subItems)
|
||||
{
|
||||
<td colspan="2">
|
||||
@{
|
||||
@if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
|
||||
Write(key);
|
||||
Write(Html.Raw($"</h{Model.Level + 3}>"));
|
||||
else if (value is Dictionary<string, object> { Count: > 0 } subItems)
|
||||
{
|
||||
<td colspan="2">
|
||||
@{
|
||||
@if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
|
||||
Write(key);
|
||||
Write(Html.Raw($"</h{Model.Level + 3}>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
<partial name="PosData" model="@((subItems, Model.Level + 1))" />
|
||||
</td>
|
||||
}
|
||||
else if (value is IEnumerable<object> valueArray)
|
||||
{
|
||||
<td colspan="2">
|
||||
@{
|
||||
@if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
|
||||
Write(key);
|
||||
Write(Html.Raw($"</h{Model.Level + 3}>"));
|
||||
<partial name="PosData" model="@((subItems, Model.Level + 1))" />
|
||||
</td>
|
||||
}
|
||||
else if (value is IEnumerable<object> valueArray)
|
||||
{
|
||||
<td colspan="2">
|
||||
@{
|
||||
@if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
Write(Html.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">"));
|
||||
Write(key);
|
||||
Write(Html.Raw($"</h{Model.Level + 3}>"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@foreach (var item in valueArray)
|
||||
{
|
||||
@if (item is Dictionary<string, object> {Count: > 0 } subItems2)
|
||||
@foreach (var item in valueArray)
|
||||
{
|
||||
<partial name="PosData" model="@((subItems2, Model.Level + 1))" />
|
||||
@if (item is Dictionary<string, object> { Count: > 0 } subItems2)
|
||||
{
|
||||
<partial name="PosData" model="@((subItems2, Model.Level + 1))" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<partial name="PosData" model="@((new Dictionary<string, object> { { "", item } }, Model.Level + 1))" />
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<partial name="PosData" model="@((new Dictionary<string, object>() {{"", item}}, Model.Level + 1))" />
|
||||
}
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
}
|
||||
|
@ -234,7 +234,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
this.setEditingItem(null, { id: '', title: '', price: 0, image: '', description: '', categories: [], priceType: 'Fixed', inventory: null, disabled: false });
|
||||
},
|
||||
editItem(index) {
|
||||
this.setEditingItem(index, Object.assign({}, this.config[index]));
|
||||
this.setEditingItem(index, Object.assign({ id: '', title: '', price: 0, image: '', description: '', categories: [], priceType: 'Fixed', inventory: null, disabled: false }, this.config[index]));
|
||||
},
|
||||
saveItem() {
|
||||
// set id from title if not set
|
||||
|
@ -473,7 +473,19 @@
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (Model.AdditionalData.Any())
|
||||
@if (Model.ReceiptData != null && Model.ReceiptData.Any())
|
||||
{
|
||||
<div>
|
||||
<h3 class="mb-3">
|
||||
<span>Receipt Information</span>
|
||||
<a href="https://docs.btcpayserver.org/Development/InvoiceMetadata/" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
</h3>
|
||||
<partial name="PosData" model="(Model.ReceiptData, 1)" />
|
||||
</div>
|
||||
}
|
||||
@if (Model.AdditionalData != null && Model.AdditionalData.Any())
|
||||
{
|
||||
<div>
|
||||
<h3 class="mb-3">
|
||||
|
@ -3,9 +3,6 @@
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Components.QRCode
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Payments
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@{
|
||||
@ -84,25 +81,7 @@
|
||||
{
|
||||
<div class="d-flex flex-column">
|
||||
<dd class="text-muted mb-0 fw-semibold">Order ID</dd>
|
||||
<dt class="fs-5 mb-0 text-break fw-semibold">
|
||||
@if (!string.IsNullOrEmpty(Model.OrderUrl))
|
||||
{
|
||||
<a href="@Model.OrderUrl" rel="noreferrer noopener" target="_blank">
|
||||
@if (string.IsNullOrEmpty(Model.OrderId))
|
||||
{
|
||||
<span>View Order</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@Model.OrderId
|
||||
}
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Model.OrderId</span>
|
||||
}
|
||||
</dt>
|
||||
<dt class="fs-5 mb-0 text-break fw-semibold">@Model.OrderId</dt>
|
||||
</div>
|
||||
}
|
||||
</dl>
|
||||
@ -115,6 +94,15 @@
|
||||
}
|
||||
else if (isSettled)
|
||||
{
|
||||
if (Model.AdditionalData?.Any() is true)
|
||||
{
|
||||
<div id="AdditionalData" class="bg-tile p-3 p-sm-4 rounded">
|
||||
<h2 class="h4 mb-3">Additional Data</h2>
|
||||
<div class="table-responsive my-0">
|
||||
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if (Model.Payments?.Any() is true)
|
||||
{
|
||||
<div id="PaymentDetails" class="bg-tile p-3 p-sm-4 rounded">
|
||||
@ -178,15 +166,10 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if (Model.AdditionalData?.Any() is true)
|
||||
{
|
||||
<div id="AdditionalData" class="bg-tile p-3 p-sm-4 rounded">
|
||||
<h2 class="h4 mb-3">Additional Data</h2>
|
||||
<div class="table-responsive my-0">
|
||||
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.OrderUrl))
|
||||
{
|
||||
<a href="@Model.OrderUrl" class="btn btn-secondary rounded-pill mx-auto mt-3" rel="noreferrer noopener" target="_blank">Return to @(string.IsNullOrEmpty(Model.StoreName) ? "store" : Model.StoreName)</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -234,7 +234,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="d-flex flex-wrap flex-sm-nowrap align-items-center gap-3 mb-4 @(Model.Invoices.Any() ? "col-xxl-8" : null)" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
|
||||
<form class="d-flex flex-wrap flex-sm-nowrap align-items-center gap-3 mb-4 col-xxl-8" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
|
||||
<input asp-for="Count" type="hidden" />
|
||||
<input asp-for="TimezoneOffset" type="hidden" />
|
||||
<input asp-for="SearchTerm" type="hidden" value="@Model.Search.WithoutSearchText()"/>
|
||||
@ -251,16 +251,17 @@
|
||||
}
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="StatusOptionsToggle">
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("status", "invalid")" class="dropdown-item @(HasArrayFilter("status", "invalid") ? "custom-active" : "")">Invalid</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("status", "processing")" class="dropdown-item @(HasArrayFilter("status", "processing") ? "custom-active" : "")">Processing</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("status", "settled")" class="dropdown-item @(HasArrayFilter("status", "settled") ? "custom-active" : "")">Settled</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("status", "processing")" class="dropdown-item @(HasArrayFilter("status", "processing") ? "custom-active" : "")">Processing</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("status", "expired")" class="dropdown-item @(HasArrayFilter("status", "expired") ? "custom-active" : "")">Expired</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("status", "invalid")" class="dropdown-item @(HasArrayFilter("status", "invalid") ? "custom-active" : "")">Invalid</a>
|
||||
<hr class="dropdown-divider">
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("exceptionstatus", "paidLate")" class="dropdown-item @(HasArrayFilter("exceptionstatus", "paidLate") ? "custom-active" : "")">Settled Late</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("exceptionstatus", "paidPartial")" class="dropdown-item @(HasArrayFilter("exceptionstatus", "paidPartial") ? "custom-active" : "")">Settled Partial</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("exceptionstatus", "paidOver")" class="dropdown-item @(HasArrayFilter("exceptionstatus", "paidOver") ? "custom-active" : "")">Settled Over</a>
|
||||
<hr class="dropdown-divider">
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("unusual", "true")" class="dropdown-item @(HasBooleanFilter("unusual") ? "custom-active" : "")">Unusual</a>
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("includearchived", "true")" class="dropdown-item @(HasBooleanFilter("includearchived") ? "custom-active" : "")" id="StatusOptionsIncludeArchived">Archived</a>
|
||||
<hr class="dropdown-divider">
|
||||
<a asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" asp-route-count="@Model.Count" asp-route-searchTerm="@Model.Search.Toggle("includearchived", "true")" class="dropdown-item @(HasBooleanFilter("includearchived") ? "custom-active" : "")" id="StatusOptionsIncludeArchived">Include archived</a>
|
||||
</div>
|
||||
</div>
|
||||
@if (Model.Apps.Any())
|
||||
|
@ -27,7 +27,14 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0">Email rules allow BTCPay Server to send customized emails from your store based on events.</p>
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="mb-0">Email rules allow BTCPay Server to send customized emails from your store based on events.</p>
|
||||
}
|
||||
|
||||
@if (Model.Rules.Any())
|
||||
{
|
||||
@ -53,8 +60,8 @@
|
||||
<div class="form-text">Choose what event sends the email.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rules[index].To" class="form-label">Recipients</label>
|
||||
<input type="text" asp-for="Rules[index].To" class="form-control"/>
|
||||
<label asp-for="Rules[index].To" class="form-label" data-required>Recipients</label>
|
||||
<input type="text" asp-for="Rules[index].To" class="form-control" />
|
||||
<span asp-validation-for="Rules[index].To" class="text-danger"></span>
|
||||
<div class="form-text">Who to send the email to. For multiple emails, separate with a comma.</div>
|
||||
</div>
|
||||
@ -64,12 +71,12 @@
|
||||
<span asp-validation-for="Rules[index].CustomerEmail" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rules[index].Subject" class="form-label" ></label>
|
||||
<input type="text" asp-for="Rules[index].Subject" class="form-control"/>
|
||||
<label asp-for="Rules[index].Subject" class="form-label" data-required></label>
|
||||
<input type="text" asp-for="Rules[index].Subject" class="form-control" />
|
||||
<span asp-validation-for="Rules[index].Subject" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Rules[index].Body" class="form-label" ></label>
|
||||
<label asp-for="Rules[index].Body" class="form-label" data-required></label>
|
||||
<textarea asp-for="Rules[index].Body" class="form-control richtext" rows="4"></textarea>
|
||||
<span asp-validation-for="Rules[index].Body" class="text-danger"></span>
|
||||
</div>
|
||||
|
@ -158,7 +158,7 @@ section dl > div dd {
|
||||
color: var(--btcpay-body-text-muted);
|
||||
}
|
||||
#DefaultLang {
|
||||
width: calc(var(--text-width, 110px) + 3rem);
|
||||
width: calc(var(--text-width, 110px) + 4rem);
|
||||
color: var(--btcpay-body-text-muted);
|
||||
background-color: var(--btcpay-body-bg);
|
||||
box-shadow: none;
|
||||
|
@ -6,7 +6,7 @@
|
||||
version="1.1"
|
||||
id="svg587"
|
||||
sodipodi:docname="supporter.svg"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
@ -96,24 +96,30 @@
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1.9763895"
|
||||
inkscape:cx="642.83887"
|
||||
inkscape:cy="194.79966"
|
||||
inkscape:cx="463.21841"
|
||||
inkscape:cy="49.838354"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1403"
|
||||
inkscape:window-height="1371"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-y="32"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="g821-3"><inkscape:grid
|
||||
inkscape:current-layer="svg587"><inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid701"
|
||||
originx="-2824.7529"
|
||||
originy="-45.146368" /><inkscape:page
|
||||
originy="-45.146368"
|
||||
spacingy="1"
|
||||
spacingx="1"
|
||||
units="px"
|
||||
visible="false" /><inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="150"
|
||||
height="100"
|
||||
id="page312"
|
||||
inkscape:label="walletofsatoshi" /><inkscape:page
|
||||
inkscape:label="walletofsatoshi"
|
||||
margin="0"
|
||||
bleed="0" /><inkscape:page
|
||||
x="485.24454"
|
||||
y="1.6249396"
|
||||
width="150"
|
||||
@ -188,6 +194,17 @@
|
||||
inkscape:label="esc"
|
||||
inkscape:export-filename="supporter_esc.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96" /><inkscape:page
|
||||
x="646.65527"
|
||||
y="114.95992"
|
||||
width="149.99997"
|
||||
height="100"
|
||||
id="page1"
|
||||
margin="0"
|
||||
bleed="0"
|
||||
inkscape:label="opensats"
|
||||
inkscape:export-filename="opensats.svg"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96" /></sodipodi:namedview><g
|
||||
transform="matrix(0.37242094,0,0,0.37242094,0,25.538247)"
|
||||
id="g585"
|
||||
@ -411,19 +428,13 @@
|
||||
transform="translate(3.4375389e-5)" /></g></g><g
|
||||
id="g931"
|
||||
inkscape:label="strike"
|
||||
transform="matrix(0.375,0,0,0.375,-142.2095,99.266315)"><rect
|
||||
style="clip-rule:evenodd;fill:#ffffff;fill-rule:evenodd;stroke-width:3.14727;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"
|
||||
id="rect875"
|
||||
width="400"
|
||||
height="266.66666"
|
||||
x="818.30243"
|
||||
y="329.36099"
|
||||
inkscape:label="bg" /><path
|
||||
d="m 968.71355,501.72507 c 11.80842,0 20.53685,-7.01705 20.53685,-16.68069 0,-4.37281 -1.84852,-8.33914 -5.13424,-10.98337 -2.56585,-1.93417 -7.18837,-4.26992 -12.8343,-6.3045 -3.59419,-1.32214 -5.03135,-2.54388 -5.03135,-3.96638 0,-1.42493 1.33433,-2.441 3.18286,-2.441 1.2315,0 2.9772,0.40658 4.41436,1.01607 l 1.33437,0.50941 2.66872,1.21932 c 1.43966,0.50941 2.67115,0.81526 3.69704,0.91567 3.28817,0.20303 6.47104,-3.15349 6.47104,-6.8138 0,-5.90056 -7.59978,-10.37371 -17.45686,-10.37371 -11.29429,0 -19.61138,7.01703 -19.61138,16.57786 0,4.37279 1.43721,7.93271 4.21123,10.3737 2.36021,1.93423 7.18589,4.16957 14.47714,6.61057 3.18287,1.11891 4.41436,2.13498 4.41436,3.66032 0,1.93417 -1.43716,3.05065 -4.00551,3.05065 -1.64039,0 -3.69701,-0.50677 -5.23458,-1.32214 l -2.46552,-1.21927 -1.33433,-0.71 c -1.7457,-0.91816 -3.28569,-1.32215 -4.72533,-1.32215 -3.48893,0 -6.77462,3.35425 -6.77462,6.91421 0,5.69484 9.44576,11.28691 19.20005,11.28691 z m 38.61065,-0.61209 c 4.7205,0 7.7001,-3.25389 7.7001,-8.43954 v -28.88331 h 3.697 c 5.4403,0 8.4175,-2.441 8.4175,-7.11982 0,-4.882 -2.7715,-7.22265 -8.4199,-7.22265 h -3.697 v -9.45556 c 0,-5.18806 -2.9772,-8.44196 -7.7001,-8.44196 -4.8232,0 -7.7001,3.25632 -7.7001,8.44196 v 9.45798 h -1.1311 c -5.5432,0 -8.31717,2.33823 -8.31717,7.22023 0,4.77923 2.77397,7.11982 8.31717,7.11982 h 1.1311 v 28.88331 c 0,5.18565 2.9773,8.43954 7.7001,8.43954 z m 30.1883,0.10283 c 4.6176,0 7.5972,-3.25632 7.5972,-8.44196 v -19.72892 c 0,-1.52532 0.3059,-3.15348 0.9231,-4.47558 0.7199,-1.72856 1.8485,-2.64424 4.8257,-3.66031 10.2684,-3.96633 7.5997,-2.95026 8.2143,-3.1535 2.7739,-1.22173 3.9027,-3.15348 3.9027,-6.30449 0,-3.96638 -3.5942,-7.42589 -7.5998,-7.42589 -3.5917,0 -6.8798,1.72852 -10.7801,5.79773 -1.6428,-4.06921 -3.4914,-5.49171 -7.0855,-5.49171 -4.8258,0 -7.803,3.15349 -7.803,8.44197 v 36.0007 c 0,5.18806 2.9772,8.44196 7.803,8.44196 z m 35.6286,-56.44454 c 5.3374,0 9.0344,-3.66026 9.0344,-8.84838 0,-5.18559 -3.7974,-8.94874 -8.9317,-8.94874 -5.2394,0 -9.0344,3.76315 -9.0344,8.84839 0,5.18564 3.795,8.94873 8.9317,8.94873 z m 0.1027,56.23888 c 4.6201,0 7.5974,-3.25146 7.5974,-8.43954 v -35.69706 c 0,-5.18565 -2.9773,-8.44197 -7.5974,-8.44197 -4.8256,0 -7.8053,3.15349 -7.8053,8.44197 v 35.69706 c 0,5.28848 2.8768,8.44197 7.8053,8.44197 z m 51.8441,0.10283 c 4.2111,0 7.4945,-3.15107 7.4945,-7.22022 0,-2.64424 -0.6146,-3.76314 -5.1343,-9.25481 l -0.923,-1.1189 -9.96,-12.5087 7.1885,-7.32057 c 4.9285,-5.19055 3.5917,-3.76558 4.1059,-4.37523 1.4372,-1.62816 1.7456,-2.441 1.7456,-3.96633 0,-3.86355 -3.1828,-7.42346 -6.777,-7.42346 -2.8744,0 -5.0314,1.21932 -8.8312,5.28847 l -2.0518,2.23782 -9.7567,10.16805 v -30.50906 c 0,-5.18806 -2.9772,-8.44195 -7.7001,-8.44195 -2.8743,0 -5.4427,1.42497 -6.7771,3.66031 -0.7173,1.32214 -0.923,2.23782 -0.923,4.78164 v 57.56098 c 0,5.28847 2.8744,8.44196 7.7001,8.44196 4.8258,0 7.7001,-3.25389 7.7001,-8.43954 v -15.86536 l 9.9624,12.81471 1.8461,2.43857 c 5.4426,7.22265 7.5997,9.05162 11.091,9.05162 z m 32.9794,0.50941 c 5.8516,0 11.5,-1.42249 16.5314,-4.16957 4.1083,-2.33816 6.1625,-4.9824 6.1625,-7.93271 0,-3.5599 -2.8769,-6.71339 -6.2653,-6.71339 -1.0284,0 -2.5659,0.40869 -4.0032,1.1189 l -1.1311,0.50941 -1.0259,0.50941 -1.1286,0.60966 -1.3368,0.61208 c -2.8744,1.11649 -4.5173,1.52291 -7.0832,1.52291 -5.6484,0 -9.3453,-2.74465 -10.6797,-7.82988 h 27.2111 c 4.62,0 6.3657,-1.83134 6.3657,-6.51016 0,-4.67639 -1.3343,-9.45804 -3.697,-13.52478 -4.6201,-7.72947 -11.9113,-11.89904 -21.0486,-11.89904 -15.3022,0 -25.7738,11.38979 -25.7738,28.068 0,15.15536 11.091,25.62948 26.9025,25.62948 z m -273.88343,-99.31284 43.45355,15.81638 c 0.17402,0.0638 0.34276,0.13447 0.50677,0.21305 0.38917,0.12234 0.77614,0.2547 1.16542,0.39655 13.8455,5.03873 21.03145,20.21365 16.05389,33.8951 -4.97993,13.67897 -20.2406,20.68378 -34.08605,15.64504 l -45.1233,-16.4236 c -2.76905,-1.00875 -4.20869,-4.0447 -3.21223,-6.78195 0.99646,-2.73484 4.04712,-4.13529 6.81623,-3.1266 l 25.07123,9.12508 a 26.089729,26.089729 0 0 1 -0.90592,-11.55136 l -51.47679,-18.73489 c -2.76906,-1.00869 -4.20627,-4.04469 -3.20976,-6.78195 0.99404,-2.73236 4.04712,-4.13281 6.81623,-3.12412 l 51.47432,18.72998 a 26.089729,26.089729 0 0 1 8.11875,-8.26567 l -25.06875,-9.1226 c -2.76664,-1.00627 -4.20627,-4.04227 -3.20981,-6.77952 0.99651,-2.73479 4.04717,-4.13529 6.81622,-3.12898 z m 272.96033,58.12406 c 5.1343,0 8.3171,2.95026 9.2403,8.84839 h -19.0973 c 1.54,-5.79773 4.9286,-8.84839 9.857,-8.84839 z M 905.37933,438.06784 c -3.00167,8.24363 1.31481,17.38089 9.63675,20.4095 8.32442,3.02867 17.50092,-1.19721 20.50258,-9.44084 2.99924,-8.24363 -1.31476,-17.38094 -9.63675,-20.40956 -8.32443,-3.02862 -17.50334,1.19727 -20.50258,9.4409 z"
|
||||
transform="matrix(0.375,0,0,0.375,-142.2095,99.266315)"><path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="m 911.11792,436.63481 c -7.86493,-3.01902 -11.79317,-11.84196 -8.77411,-19.70681 3.01906,-7.86488 11.84204,-11.79311 19.70696,-8.77421 l 56.77346,21.79334 c 0.0619,0.0258 0.1256,0.0499 0.18893,0.0734 l 28.48154,10.93278 c 7.8635,3.01899 16.688,-0.90936 19.7055,-8.77406 3.0205,-7.86485 -0.9078,-16.68798 -8.7739,-19.707 v -9.5e-4 c 0,-6.3e-4 0,-10e-4 0,-0.002 l -28.48022,-10.93266 c -7.8648,-3.01901 -11.79305,-11.84213 -8.77403,-19.7069 3.01901,-7.86493 11.84217,-11.79317 19.70705,-8.77411 l 83.7678,32.15558 c 0.1998,0.0764 0.399,0.15757 0.5944,0.24187 0.3601,0.13257 0.7204,0.26792 1.0804,0.4061 39.3242,15.09495 58.9663,59.21054 43.8697,98.5346 -15.095,39.32457 -59.2104,58.9653 -98.5341,43.87012 -0.7567,-0.28982 -1.5058,-0.59168 -2.249,-0.90299 -0.3605,-0.10969 -0.7194,-0.23172 -1.0762,-0.36915 l -82.11737,-31.52184 c -7.86467,-3.019 -11.79309,-11.84215 -8.77392,-19.707 3.01889,-7.86468 11.84202,-11.79309 19.7067,-8.77393 l 28.47718,10.93141 c -4.8e-4,-0.002 -8.9e-4,-0.004 -0.002,-0.007 l 0.006,0.002 c 7.86486,3.01902 16.68811,-0.90922 19.70621,-8.77407 3.0195,-7.86487 -0.9082,-16.68795 -8.7733,-19.70698 l -12.81624,-4.91969 c 1.6e-4,-6.2e-4 3.2e-4,-9.8e-4 4.7e-4,-0.002 z"
|
||||
fill="currentColor"
|
||||
id="path420"
|
||||
style="stroke-width:2.44837"
|
||||
inkscape:label="logo" /></g><g
|
||||
id="path1"
|
||||
style="clip-rule:evenodd;fill:#000000;fill-rule:evenodd;stroke-width:1.52532;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" /></g><g
|
||||
id="g235"
|
||||
transform="matrix(1.5721144,0,0,1.5721144,345.43403,227.90778)"
|
||||
inkscape:label="coincards"><rect
|
||||
@ -492,4 +503,38 @@
|
||||
rx="16.600775"
|
||||
ry="16.606844"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision;fill:#ffffff;stroke-width:0.121342"
|
||||
id="ellipse2233" /></g></g></svg>
|
||||
id="ellipse2233" /></g></g><g
|
||||
id="g1"
|
||||
transform="matrix(0.02527606,0,0,0.02527606,655.68059,155.48226)"
|
||||
inkscape:label="opensats"
|
||||
style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"><path
|
||||
d="m 0,435.197 229.609,-143.6 v -3.476 L 0,144.259 V 29.0508 L 334.901,245.894 v 87.93 L 0,550.798 Z"
|
||||
fill="#ff3300"
|
||||
id="path1-3" /><path
|
||||
d="m 486.969,623.844 h 415.658 v 95.799 H 486.969 Z"
|
||||
fill="#ff3300"
|
||||
id="path2" /><path
|
||||
d="M 993.879,291.2 C 993.879,106.422 1084.61,0 1214.37,0 c 129.76,0 220.49,106.422 220.49,291.2 0,187.861 -90.73,296.381 -220.49,296.381 -129.76,0 -220.491,-108.52 -220.491,-296.381 z m 351.241,0 c 0,-136.19 -51.96,-215.2033 -130.75,-215.2033 -78.79,0 -130.75,79.0133 -130.75,215.2033 0,139.273 51.96,220.384 130.75,220.384 78.79,0 130.48,-81.111 130.48,-220.384 z"
|
||||
fill="#ff3300"
|
||||
id="path3" /><path
|
||||
d="m 1593.29,154.29 h 70.52 l 6.56,51.08 h 2.76 c 38.18,-35.736 91.58,-61.112 141.31,-61.112 111.52,0 173.58,84.455 173.58,215.597 0,144.256 -90.07,228.056 -190.25,228.056 -38.64,0 -84.23,-19.082 -120.38,-52.457 h -2.1 l 4.14,76.783 v 137.699 h -86.14 z m 306.36,205.565 c 0,-88.586 -32.21,-144.256 -108.44,-144.256 -34.64,0 -73.28,17.114 -111.52,56.522 v 199.991 c 35.1,31.802 74.92,43.343 101.88,43.343 67.18,0.262 118.08,-55.604 118.08,-155.6 z"
|
||||
fill="#ff3300"
|
||||
id="path4" /><path
|
||||
d="m 2118.96,365.035 c 0,-137.699 103.79,-221.105 217.02,-221.105 124.18,0 194.84,81.504 194.84,199.597 -0.15,15.682 -1.47,31.331 -3.94,46.818 H 2178.73 V 327.2 h 294.49 l -18.7,21.049 c 0,-91.8 -44.35,-137.699 -116.05,-137.699 -74.06,0 -134.81,54.62 -134.81,154.354 0,103.929 65.14,155.14 156.13,155.14 47.3,0 85.29,-14.294 123.6,-37.244 l 30.17,54.49 c -48.86,32.596 -106.24,50.088 -164.99,50.292 -128.18,0 -229.61,-81.635 -229.61,-222.547 z"
|
||||
fill="#ff3300"
|
||||
id="path5" /><path
|
||||
d="m 2658.41,154.29 h 70.66 l 6.56,67.407 h 3.41 c 42.51,-43.408 90.79,-77.439 156.13,-77.439 98.93,0 144,60.784 144,170.943 V 577.026 H 2952.9 V 326.152 c 0,-73.833 -24.79,-107.93 -87.51,-107.93 -45.92,0 -77.08,22.622 -120.71,67.341 v 291.463 h -86.27 z"
|
||||
fill="#ff3300"
|
||||
id="path6" /><path
|
||||
d="m 3208.36,504.308 51.1,-59.932 c 43.54,42.11 101.47,66.092 162.04,67.079 71.77,0 111.53,-32.786 111.53,-78.685 0,-55.408 -41.4,-70.817 -97.62,-94.553 l -79.84,-34.884 c -57.93,-22.819 -121.36,-64.719 -121.36,-148.19 0,-88.3244 79.44,-155.141186 191.43,-155.141186 66.37,-0.444776 130.29,25.063586 178.11,71.078886 l -44.88,55.4733 C 3521.22,93.051 3472.3,74.9951 3421.9,75.9985 c -59.7,0 -99.79,27.6055 -99.79,74.0295 0,49.178 48.94,67.145 98.87,86.488 l 76.95,33.9 c 71.11,27.671 124.25,69.112 124.25,151.928 0,91.012 -76.82,165.239 -205.4,165.239 -77.67,0.415 -152.44,-29.46 -208.42,-83.275 z"
|
||||
fill="#000000"
|
||||
id="path7" /><path
|
||||
d="m 3762.96,465.557 c 0,-94.816 86.08,-141.502 299.74,-157.371 -3.08,-52.456 -31.16,-94.29 -106.4,-94.29 -51.17,0 -101.62,23.408 -143.02,47.604 l -32.8,-57.506 c 47.5,-29.31 118.09,-59.932 191.3,-59.932 116.64,0 177.13,66.883 177.13,178.616 v 254.349 h -71.18 l -6.89,-54.883 h -2.76 c -45.26,35.409 -104.11,65.571 -162.04,65.571 -80.88,-0.131 -143.08,-47.67 -143.08,-122.158 z m 299.74,-3.279 v -99.012 c -165.91,11.868 -215.44,45.31 -215.44,95.734 0,42.489 38.12,60.063 83.39,60.063 45.26,0 88.23,-21.18 132.05,-56.785 z"
|
||||
fill="#000000"
|
||||
id="path8" /><path
|
||||
d="M 4388.81,409.884 V 222.941 h -116.64 v -64.128 l 119.99,-4.524 11.28,-134.0273 h 71.58 V 154.289 h 199.62 v 68.652 h -199.62 v 187.205 c 0,72.718 25.71,107.93 102.27,107.93 32.98,-0.095 65.67,-6.229 96.44,-18.097 l 17.51,63.013 c -42.85,15.959 -88.17,24.282 -133.89,24.589 -126.49,0 -168.54,-71.538 -168.54,-177.697 z"
|
||||
fill="#000000"
|
||||
id="path9" /><path
|
||||
d="m 4818.71,521.815 38.38,-55.801 c 52.81,36.934 115.89,56.339 180.34,55.473 64.88,0 95.91,-26.228 95.91,-57.637 0,-29.9 -17.58,-48.85 -124.12,-70.816 -111.52,-23.147 -164.01,-61.834 -164.01,-125.831 0,-70.685 63.64,-123.273 180.74,-123.273 67.31,0 131.6,26.228 173.98,54.948 l -40.48,53.768 c -41.66,-27.978 -90.66,-43.054 -140.85,-43.342 -62.98,0 -87.32,24.851 -87.32,53.834 0,32.785 33.79,45.899 116.84,63.8 137.77,30.163 172.21,65.571 172.21,131.798 0,70.751 -68.62,128.846 -193.53,128.846 -74.33,-0.941 -146.73,-23.822 -208.09,-65.767 z"
|
||||
fill="#000000"
|
||||
id="path10" /></g></svg>
|
||||
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
1
BTCPayServer/wwwroot/img/readme/supporter_opensats.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="150" height="100" style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" version="1.1" id="svg587" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><style id="style324">.st2{fill:#ffc214}.st3{fill:#f9f185}.st0{fill:#222221}.st1{fill:#272425}</style><g id="g1" transform="translate(9.025 40.522) scale(.02528)" style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"><path d="m0 435.197 229.609-143.6v-3.476L0 144.259V29.051l334.901 216.843v87.93L0 550.798Z" fill="#f30" id="path1-3"/><path d="M486.969 623.844h415.658v95.799H486.969Z" fill="#f30" id="path2"/><path d="M993.879 291.2C993.879 106.422 1084.61 0 1214.37 0c129.76 0 220.49 106.422 220.49 291.2 0 187.861-90.73 296.381-220.49 296.381S993.879 479.061 993.879 291.2zm351.241 0c0-136.19-51.96-215.203-130.75-215.203-78.79 0-130.75 79.013-130.75 215.203 0 139.273 51.96 220.384 130.75 220.384 78.79 0 130.48-81.111 130.48-220.384z" fill="#f30" id="path3"/><path d="M1593.29 154.29h70.52l6.56 51.08h2.76c38.18-35.736 91.58-61.112 141.31-61.112 111.52 0 173.58 84.455 173.58 215.597 0 144.256-90.07 228.056-190.25 228.056-38.64 0-84.23-19.082-120.38-52.457h-2.1l4.14 76.783v137.699h-86.14zm306.36 205.565c0-88.586-32.21-144.256-108.44-144.256-34.64 0-73.28 17.114-111.52 56.522v199.991c35.1 31.802 74.92 43.343 101.88 43.343 67.18.262 118.08-55.604 118.08-155.6z" fill="#f30" id="path4"/><path d="M2118.96 365.035c0-137.699 103.79-221.105 217.02-221.105 124.18 0 194.84 81.504 194.84 199.597a316.484 316.484 0 0 1-3.94 46.818h-348.15V327.2h294.49l-18.7 21.049c0-91.8-44.35-137.699-116.05-137.699-74.06 0-134.81 54.62-134.81 154.354 0 103.929 65.14 155.14 156.13 155.14 47.3 0 85.29-14.294 123.6-37.244l30.17 54.49a299.162 299.162 0 0 1-164.99 50.292c-128.18 0-229.61-81.635-229.61-222.547z" fill="#f30" id="path5"/><path d="M2658.41 154.29h70.66l6.56 67.407h3.41c42.51-43.408 90.79-77.439 156.13-77.439 98.93 0 144 60.784 144 170.943v261.825h-86.27V326.152c0-73.833-24.79-107.93-87.51-107.93-45.92 0-77.08 22.622-120.71 67.341v291.463h-86.27z" fill="#f30" id="path6"/><path d="m3208.36 504.308 51.1-59.932a238.681 238.681 0 0 0 162.04 67.079c71.77 0 111.53-32.786 111.53-78.685 0-55.408-41.4-70.817-97.62-94.553l-79.84-34.884c-57.93-22.819-121.36-64.719-121.36-148.19 0-88.324 79.44-155.141 191.43-155.141a254.42 254.42 0 0 1 178.11 71.079l-44.88 55.473a200.053 200.053 0 0 0-136.97-50.555c-59.7 0-99.79 27.605-99.79 74.029 0 49.178 48.94 67.145 98.87 86.488l76.95 33.9c71.11 27.671 124.25 69.112 124.25 151.928 0 91.012-76.82 165.239-205.4 165.239a298.439 298.439 0 0 1-208.42-83.275z" fill="#000" id="path7"/><path d="M3762.96 465.557c0-94.816 86.08-141.502 299.74-157.371-3.08-52.456-31.16-94.29-106.4-94.29-51.17 0-101.62 23.408-143.02 47.604l-32.8-57.506c47.5-29.31 118.09-59.932 191.3-59.932 116.64 0 177.13 66.883 177.13 178.616v254.349h-71.18l-6.89-54.883h-2.76c-45.26 35.409-104.11 65.571-162.04 65.571-80.88-.131-143.08-47.67-143.08-122.158zm299.74-3.279v-99.012c-165.91 11.868-215.44 45.31-215.44 95.734 0 42.489 38.12 60.063 83.39 60.063 45.26 0 88.23-21.18 132.05-56.785z" fill="#000" id="path8"/><path d="M4388.81 409.884V222.941h-116.64v-64.128l119.99-4.524 11.28-134.027h71.58v134.027h199.62v68.652h-199.62v187.205c0 72.718 25.71 107.93 102.27 107.93a270.172 270.172 0 0 0 96.44-18.097l17.51 63.013a391.161 391.161 0 0 1-133.89 24.589c-126.49 0-168.54-71.538-168.54-177.697z" fill="#000" id="path9"/><path d="m4818.71 521.815 38.38-55.801a307.444 307.444 0 0 0 180.34 55.473c64.88 0 95.91-26.228 95.91-57.637 0-29.9-17.58-48.85-124.12-70.816-111.52-23.147-164.01-61.834-164.01-125.831 0-70.685 63.64-123.273 180.74-123.273 67.31 0 131.6 26.228 173.98 54.948l-40.48 53.768a255.28 255.28 0 0 0-140.85-43.342c-62.98 0-87.32 24.851-87.32 53.834 0 32.785 33.79 45.899 116.84 63.8 137.77 30.163 172.21 65.571 172.21 131.798 0 70.751-68.62 128.846-193.53 128.846a377.209 377.209 0 0 1-208.09-65.767z" fill="#000" id="path10"/></g></svg>
|
After Width: | Height: | Size: 4.0 KiB |
@ -1 +1 @@
|
||||
<svg width="150" height="100" style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" version="1.1" id="svg587" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><style id="style324">.st2{fill:#ffc214}.st3{fill:#f9f185}.st0{fill:#222221}.st1{fill:#272425}</style><g id="g931" transform="matrix(.375 0 0 .375 -306.863 -123.51)"><path style="clip-rule:evenodd;fill:#fff;fill-rule:evenodd;stroke-width:3.14727;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" id="rect875" d="M818.302 329.361h400v266.667h-400z"/><path d="M968.714 501.725c11.808 0 20.536-7.017 20.536-16.68 0-4.373-1.848-8.34-5.134-10.984-2.566-1.934-7.188-4.27-12.834-6.304-3.594-1.323-5.031-2.544-5.031-3.967 0-1.425 1.334-2.44 3.182-2.44 1.232 0 2.978.406 4.415 1.015l1.334.51 2.669 1.219c1.44.51 2.671.815 3.697.916 3.288.203 6.47-3.154 6.47-6.814 0-5.9-7.599-10.374-17.456-10.374-11.294 0-19.611 7.017-19.611 16.578 0 4.373 1.437 7.933 4.21 10.374 2.361 1.934 7.187 4.17 14.478 6.61 3.183 1.12 4.414 2.135 4.414 3.66 0 1.935-1.437 3.051-4.005 3.051-1.64 0-3.697-.507-5.235-1.322l-2.465-1.22-1.335-.71c-1.745-.917-3.285-1.321-4.725-1.321-3.489 0-6.774 3.354-6.774 6.914 0 5.695 9.445 11.287 19.2 11.287zm38.61-.612c4.72 0 7.7-3.254 7.7-8.44V463.79h3.697c5.44 0 8.418-2.44 8.418-7.12 0-4.882-2.772-7.222-8.42-7.222h-3.697v-9.456c0-5.188-2.977-8.442-7.7-8.442-4.823 0-7.7 3.256-7.7 8.442v9.458h-1.131c-5.544 0-8.318 2.338-8.318 7.22 0 4.78 2.774 7.12 8.318 7.12h1.13v28.883c0 5.186 2.978 8.44 7.7 8.44zm30.189.103c4.617 0 7.597-3.257 7.597-8.442v-19.73c0-1.524.306-3.153.923-4.475.72-1.728 1.848-2.644 4.826-3.66 10.268-3.966 7.6-2.95 8.214-3.153 2.774-1.222 3.903-3.154 3.903-6.305 0-3.966-3.595-7.426-7.6-7.426-3.592 0-6.88 1.729-10.78 5.798-1.643-4.07-3.492-5.492-7.086-5.492-4.826 0-7.803 3.154-7.803 8.442v36c0 5.189 2.977 8.443 7.803 8.443zm35.628-56.445c5.337 0 9.034-3.66 9.034-8.848 0-5.186-3.797-8.949-8.931-8.949-5.24 0-9.035 3.763-9.035 8.849 0 5.185 3.795 8.948 8.932 8.948zm.103 56.24c4.62 0 7.597-3.252 7.597-8.44v-35.697c0-5.186-2.977-8.442-7.597-8.442-4.826 0-7.805 3.153-7.805 8.442v35.697c0 5.288 2.876 8.442 7.805 8.442zm51.844.102c4.211 0 7.494-3.151 7.494-7.22 0-2.644-.614-3.763-5.134-9.255l-.923-1.119-9.96-12.509 7.189-7.32c4.928-5.19 3.591-3.766 4.106-4.375 1.437-1.629 1.745-2.441 1.745-3.967 0-3.863-3.183-7.423-6.777-7.423-2.874 0-5.031 1.22-8.831 5.288l-2.052 2.238-9.757 10.168V435.11c0-5.188-2.977-8.442-7.7-8.442-2.874 0-5.442 1.425-6.777 3.66-.717 1.323-.923 2.238-.923 4.782v57.561c0 5.288 2.875 8.442 7.7 8.442 4.826 0 7.7-3.254 7.7-8.44v-15.865l9.963 12.815 1.846 2.438c5.442 7.223 7.6 9.052 11.09 9.052zm32.98.51c5.85 0 11.5-1.423 16.53-4.17 4.109-2.338 6.163-4.983 6.163-7.933 0-3.56-2.877-6.713-6.265-6.713-1.029 0-2.566.408-4.003 1.119l-1.131.509-1.026.51-1.129.61-1.337.611c-2.874 1.117-4.517 1.523-7.083 1.523-5.648 0-9.345-2.745-10.68-7.83h27.211c4.62 0 6.366-1.831 6.366-6.51 0-4.676-1.334-9.458-3.697-13.525-4.62-7.73-11.911-11.899-21.048-11.899-15.303 0-25.774 11.39-25.774 28.068 0 15.156 11.09 25.63 26.902 25.63zM884.183 402.31l43.453 15.816c.174.064.343.134.507.213.39.122.776.255 1.166.397 13.845 5.038 21.031 20.213 16.053 33.895-4.98 13.679-20.24 20.683-34.086 15.645l-45.123-16.424c-2.769-1.009-4.209-4.045-3.212-6.782.996-2.735 4.047-4.135 6.816-3.126l25.071 9.125a26.09 26.09 0 0 1-.906-11.552l-51.476-18.735c-2.77-1.008-4.207-4.044-3.21-6.782.994-2.732 4.047-4.132 6.816-3.124l51.474 18.73a26.09 26.09 0 0 1 8.12-8.265l-25.07-9.123c-2.766-1.006-4.206-4.042-3.21-6.78.997-2.734 4.048-4.135 6.817-3.129zm272.96 58.124c5.134 0 8.317 2.95 9.24 8.848h-19.097c1.54-5.798 4.929-8.848 9.857-8.848zM905.38 438.068c-3.001 8.243 1.315 17.38 9.637 20.41 8.325 3.028 17.501-1.198 20.503-9.442 2.999-8.243-1.315-17.38-9.637-20.41-8.325-3.028-17.503 1.198-20.503 9.442z" fill="currentColor" id="path420" style="stroke-width:2.44837"/></g></svg>
|
||||
<svg width="150" height="100" style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" version="1.1" id="svg587" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><style id="style324">.st2{fill:#ffc214}.st3{fill:#f9f185}.st0{fill:#222221}.st1{fill:#272425}</style><g id="g931" transform="matrix(.375 0 0 .375 -306.863 -123.51)"><path fill-rule="evenodd" clip-rule="evenodd" d="M911.118 436.635c-7.865-3.02-11.793-11.842-8.774-19.707s11.842-11.793 19.707-8.774l56.773 21.793c.062.026.126.05.19.074l28.48 10.932c7.864 3.02 16.689-.909 19.706-8.774 3.02-7.865-.908-16.688-8.774-19.707v-.003l-28.48-10.932c-7.865-3.02-11.793-11.842-8.774-19.707 3.02-7.865 11.842-11.793 19.707-8.774l83.768 32.155c.2.077.399.158.594.242a84 84 0 0 1 1.08.406c39.325 15.095 58.967 59.21 43.87 98.535-15.095 39.324-59.21 58.965-98.534 43.87a78.402 78.402 0 0 1-2.249-.903c-.36-.11-.72-.232-1.076-.37l-82.117-31.521c-7.865-3.02-11.793-11.842-8.774-19.707s11.842-11.793 19.707-8.774l28.477 10.931-.002-.007.006.002c7.865 3.02 16.688-.909 19.706-8.774 3.02-7.865-.908-16.688-8.773-19.707l-12.817-4.92v-.001z" fill="currentColor" id="path1" style="clip-rule:evenodd;fill:#000;fill-rule:evenodd;stroke-width:1.52532;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"/></g></svg>
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
@ -81,43 +81,6 @@ header .cart-toggle-btn {
|
||||
min-width: 1.75em;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
max-height: 210px;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
.posItem {
|
||||
position: relative;
|
||||
}
|
||||
.posItem.posItem--inStock {
|
||||
cursor: pointer;
|
||||
}
|
||||
.posItem-added {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--btcpay-success);
|
||||
color: var(--btcpay-success-text);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--btcpay-transition-duration-default) ease-in-out;
|
||||
}
|
||||
.posItem-added .icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
.posItem--added {
|
||||
pointer-events: none;
|
||||
}
|
||||
.posItem--added .posItem-added {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
#cart {
|
||||
left: 0;
|
||||
|
@ -52,35 +52,23 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
return this.cart.reduce((res, item) => res + (parseInt(item.count) || 0), 0)
|
||||
},
|
||||
amountNumeric () {
|
||||
return parseFloat(this.cart.reduce((res, item) => res + item.price * item.count, 0).toFixed(this.currencyInfo.divisibility))
|
||||
return parseFloat(this.cart.reduce((res, item) => res + (item.price||0) * item.count, 0).toFixed(this.currencyInfo.divisibility))
|
||||
},
|
||||
posdata () {
|
||||
const data = {
|
||||
cart: this.cart,
|
||||
subTotal: this.amountNumeric,
|
||||
total: this.totalNumeric
|
||||
}
|
||||
if (this.tipNumeric > 0) data.tip = this.tipNumeric
|
||||
const data = { cart: this.cart, subTotal: this.amountNumeric }
|
||||
if (this.discountNumeric > 0) data.discountAmount = this.discountNumeric
|
||||
if (this.discountPercentNumeric > 0) data.discountPercentage = this.discountPercentNumeric
|
||||
if (this.tipNumeric > 0) data.tip = this.tipNumeric
|
||||
data.total = this.totalNumeric
|
||||
return JSON.stringify(data)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
searchTerm(term) {
|
||||
const t = term.toLowerCase();
|
||||
this.forEachItem(item => {
|
||||
const terms = item.dataset.search.toLowerCase()
|
||||
const included = terms.indexOf(t) !== -1
|
||||
item.classList[included ? 'remove' : 'add']("d-none")
|
||||
})
|
||||
this.updateDisplay()
|
||||
},
|
||||
displayCategory(category) {
|
||||
this.forEachItem(item => {
|
||||
const categories = JSON.parse(item.dataset.categories)
|
||||
const included = category === "*" || categories.includes(category)
|
||||
item.classList[included ? 'remove' : 'add']("d-none")
|
||||
})
|
||||
this.updateDisplay()
|
||||
},
|
||||
cart: {
|
||||
handler(newCart) {
|
||||
@ -121,7 +109,17 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
if (!this.inStock(index)) return false;
|
||||
|
||||
const item = this.items[index];
|
||||
let itemInCart = this.cart.find(lineItem => lineItem.id === item.id);
|
||||
const $posItem = this.$refs.posItems.querySelectorAll('.posItem')[index];
|
||||
|
||||
// Check if price is needed
|
||||
const isFixedPrice = item.priceType.toLowerCase() === 'fixed';
|
||||
if (!isFixedPrice) {
|
||||
const $amount = $posItem.querySelector('input[name="amount"]');
|
||||
if (!$amount.reportValidity()) return false;
|
||||
item.price = parseFloat($amount.value);
|
||||
}
|
||||
|
||||
let itemInCart = this.cart.find(lineItem => lineItem.id === item.id && lineItem.price === item.price);
|
||||
|
||||
// Add new item because it doesn't exist yet
|
||||
if (!itemInCart) {
|
||||
@ -138,7 +136,6 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
itemInCart.count += 1;
|
||||
|
||||
// Animate
|
||||
const $posItem = this.$refs.posItems.querySelectorAll('.posItem')[index];
|
||||
if(!$posItem.classList.contains(POS_ITEM_ADDED_CLASS)) $posItem.classList.add(POS_ITEM_ADDED_CLASS);
|
||||
|
||||
return true;
|
||||
@ -157,10 +154,32 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
},
|
||||
clearCart() {
|
||||
this.cart = [];
|
||||
},
|
||||
displayItem(item) {
|
||||
const inSearch = !this.searchTerm ||
|
||||
decodeURIComponent(item.dataset.search ? item.dataset.search.toLowerCase() : '')
|
||||
.indexOf(this.searchTerm.toLowerCase()) !== -1
|
||||
const inCategories = this.displayCategory === "*" ||
|
||||
(item.dataset.categories ? JSON.parse(item.dataset.categories) : [])
|
||||
.includes(this.displayCategory)
|
||||
return inSearch && inCategories
|
||||
},
|
||||
updateDisplay() {
|
||||
this.forEachItem(item => {
|
||||
item.classList[this.displayItem(item) ? 'add' : 'remove']('posItem--displayed')
|
||||
item.classList.remove('posItem--first')
|
||||
item.classList.remove('posItem--last')
|
||||
})
|
||||
const $displayed = this.$refs.posItems.querySelectorAll('.posItem.posItem--displayed')
|
||||
if ($displayed.length > 0) {
|
||||
$displayed[0].classList.add('posItem--first')
|
||||
$displayed[$displayed.length - 1].classList.add('posItem--last')
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$cart = new bootstrap.Offcanvas(this.$refs.cart, {backdrop: false})
|
||||
this.$cart = new bootstrap.Offcanvas(this.$refs.cart, { backdrop: false })
|
||||
|
||||
window.addEventListener('pagehide', () => {
|
||||
if (this.payButtonLoading) {
|
||||
this.unsetPayButtonLoading();
|
||||
@ -174,6 +193,7 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
}
|
||||
});
|
||||
})
|
||||
this.updateDisplay()
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -6,3 +6,55 @@
|
||||
.lead :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.posItem {
|
||||
display: none;
|
||||
position: relative;
|
||||
}
|
||||
.posItem.posItem--inStock {
|
||||
cursor: pointer;
|
||||
}
|
||||
.posItem-added {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--btcpay-success);
|
||||
color: var(--btcpay-success-text);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--btcpay-transition-duration-default) ease-in-out;
|
||||
}
|
||||
.posItem-added .icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
.posItem--added {
|
||||
pointer-events: none;
|
||||
}
|
||||
.posItem--added .posItem-added {
|
||||
opacity: .8;
|
||||
}
|
||||
.posItem--displayed {
|
||||
display: flex;
|
||||
}
|
||||
.posItem--first {
|
||||
margin-left: auto;
|
||||
}
|
||||
.posItem--last {
|
||||
margin-right: auto;
|
||||
}
|
||||
.posItem .card {
|
||||
width: 100%;
|
||||
}
|
||||
.posItem .card .card-body {
|
||||
flex-grow: 0;
|
||||
}
|
||||
.posItem .card .card-img-top {
|
||||
max-height: 210px;
|
||||
object-fit: scale-down;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
@ -31,6 +31,9 @@ const posCommon = {
|
||||
if (this.tipPercent) {
|
||||
return parseFloat((this.amountMinusDiscountNumeric * (this.tipPercent / 100)).toFixed(this.currencyInfo.divisibility))
|
||||
} else {
|
||||
if (this.tip < 0) {
|
||||
this.tip = 0
|
||||
}
|
||||
const value = parseFloat(this.tip)
|
||||
return isNaN(value) ? 0.0 : parseFloat(value.toFixed(this.currencyInfo.divisibility))
|
||||
}
|
||||
@ -64,6 +67,7 @@ const posCommon = {
|
||||
discountPercent (val) {
|
||||
const value = parseFloat(val)
|
||||
if (isNaN(value)) this.discountPercent = null
|
||||
else if (value < 0) this.discountPercent = '0'
|
||||
else if (value > 100) this.discountPercent = '100'
|
||||
else this.discountPercent = value.toString()
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.11.0</Version>
|
||||
<Version>1.11.2</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
60
Changelog.md
@ -1,5 +1,61 @@
|
||||
# Changelog
|
||||
|
||||
## 1.11.2
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* Language Select box cut off on checkout (#5210) @evanc-ole
|
||||
* POS: Multiple fixes (#5228 #5241 #5252) @dennisreimann
|
||||
* Greenfield: Fix invoice lookup by capitalized status (#5245) @dennisreimann
|
||||
* Fix temporary file downloads for local storage option @Kukks
|
||||
|
||||
### Improvements
|
||||
|
||||
* POS: Handle flexible price items in cart view (#5238) @dennisreimann
|
||||
* POS: Combine search term and category selector (#5241) @dennisreimann
|
||||
* Email Rules: Improve validation (#5234) @dennisreimann
|
||||
* Receipt improvements (#5239) @dennisreimann
|
||||
* Improve invoices status filter (#5248 #5251) @dennisreimann
|
||||
|
||||
## 1.11.1
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* Language Select box cut off on checkout (#5210) @dstrukt
|
||||
* POS Cart view malformed when special characters are in items (#5203 #5211) @Kukks
|
||||
* Errors creating invoice from public form were not shown in the UI (#5208 #5211) @Kukks
|
||||
* Cart view doesn't show item when the amount field is custom (#5204 #5211) @Kukks
|
||||
* Can't save the item when adding a new category in POS (#5205 #5211) @Kukks
|
||||
|
||||
## 1.11.0
|
||||
|
||||
### New Features
|
||||
|
||||
* Complete overhaul of Invoice Reporting (#5095 #5155) @NicolasDorier
|
||||
* POS Cart redesign (#5109 #5171) @dennisreimann @dstrukt
|
||||
* Add product categories to POS apps (#5088 #5078) @NicolasDorier
|
||||
* Checkout v2: Play sound when invoice is paid (#5085 #5113) @dennisreimann @webwworthy
|
||||
* Add support for ExchangeRateHost and FreeCurrencyRates rate providers (#5166) @Kukks
|
||||
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Support NFC on modal (#4251 #5033) @Kukks @dennisreimann
|
||||
* Fixed setting of minimum or custom price for LNURL POS items (#5170 #5172) @Kukks
|
||||
* Preventing entering of negative tips and discounts in POS (#5192 #5198) @rockstardev
|
||||
* Fixing display of amount paid on Receipt page (#5195 #5197) @rockstardev
|
||||
* Form invoice amount adjusters, useful for shipping and other addon amounts to the invoice (#5087 #5158) @Kukks @dennisreimann
|
||||
|
||||
|
||||
### Improvements
|
||||
|
||||
* Improved Payment Requests List View (#3872 #5065) @TChukwuleta
|
||||
* Improve create first store view (#5008 #5181) @dennisreimann
|
||||
* Invoice lists: Show icons for payment methods (#5084 #5137) @dennisreimann
|
||||
* Apps: Add direct file upload in item editor (#5086 #5140) @dennisreimann
|
||||
* Add OpenSats supporters logo (#5202) @Kukks @Pavlenex
|
||||
* Add recommended rate providers for UGX and RSD (#5166) @Kukks
|
||||
|
||||
## 1.10.3
|
||||
|
||||
### New Features
|
||||
@ -95,9 +151,9 @@ This data, generally used for debugging integrations, will be regularly purged.
|
||||
* Crowdfund: Fix redirect URL fallback (#4943) @dennisreimann
|
||||
* Greenfield: Apply store default payment method on invoice creation (#4947) @dennisreimann
|
||||
* POS: Fix Firefox issues (#4950) @r0ckstardev
|
||||
* Fix viewing arrays in the invoice details when set in metadata (#4954) @Kukks
|
||||
* Fix viewing arrays in the invoice details when set in metadata (#4954) @Kukks
|
||||
* Do not crash checkout when attempting LNURL checkout through non-secure page (#4964) @Kukks
|
||||
* NFC: Handle HTTP-related exceptions (#4965) @dennisreimann
|
||||
* NFC: Handle HTTP-related exceptions (#4965) @dennisreimann
|
||||
|
||||
### Improvements
|
||||
|
||||
|
@ -95,7 +95,7 @@
|
||||
* Enhanced privacy & security
|
||||
* Self-hosted
|
||||
* SegWit support
|
||||
* Lightning Network support (LND, c-lightning, Eclair, and Ptarmigan)
|
||||
* Lightning Network support (LND, Core Lightning (CLN), Eclair)
|
||||
* Tor support
|
||||
* Share your instance with friends (multi-tenant)
|
||||
* Invoice management and Payment requests
|
||||
|