Compare commits

...

27 Commits

Author SHA1 Message Date
dstrukt
a1270e2257 remove store ID from view request url 2023-08-12 13:41:51 +02:00
Kukks
1c5fcfe094
bump v 2023-08-11 15:55:11 +02:00
Dennis Reimann
45c1fb42ee
Changelog v1.11.2 2023-08-11 15:39:08 +02:00
d11n
64bd493996
POS: Unify item display (#5252)
Display unifications for static and cart view.
2023-08-11 15:37:43 +02:00
d11n
ec6029409e
Improve invoice filter wording (#5251)
Closes #5250.
2023-08-11 15:08:44 +02:00
d11n
c0fc31c69a
Improve invoices status filter (#5248) 2023-08-10 20:23:18 +02:00
d11n
b5d0188f21
Receipt improvements (#5239) 2023-08-10 13:57:54 +02:00
d11n
0ccbaf4bd6
Greenfield: Fix invoice lookup by capitalized status (#5245)
All statuses need to be lowercased before lookup, this wasn't the case for e.g. `Expired`.

Fixes #5244.
2023-08-10 13:34:09 +02:00
d11n
ed43fb2071
POS fixes (#5241) 2023-08-09 14:47:28 +02:00
d11n
d67ebd957e
POS: Handle flexible price items in cart view (#5238) 2023-08-09 09:31:19 +02:00
Ikko Eltociear Ashimine
19d360a543
Fix: typo in InvoiceEntity.cs (#5236)
Minumum -> Minimum
2023-08-07 09:26:37 +02:00
d11n
7dc41ebcea
Email Rules: Improve validation (#5234)
Came across this while testing things and the "Please fill all required fields before testing" message wasn't clear, because the required fields were not marked.

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2023-08-07 09:10:48 +02:00
d11n
1eb7c727f3
POS fixes (#5228) 2023-08-05 10:44:59 +02:00
evanc-ole
ede8171408
Checkout: Fix language select UI bug (#5229) 2023-08-04 07:44:50 +02:00
Kukks
2538f3d8f6
fix https://github.com/Kukks/BTCPayServerPlugins/issues/18 2023-08-03 20:48:46 +02:00
Pavlenex
ac64f5e395
Merge pull request #5227 from dennisreimann/supporters
Update supporters
2023-08-03 19:45:09 +02:00
Dennis Reimann
1a7a731b54
Update supporters
Improve colors and visual balance
2023-08-03 14:58:32 +02:00
ndeet
86f4d48bcb
c-lightning to CLN; remove ptarmigan. (#5220) 2023-08-01 17:21:00 +03:00
Kukks
83536bee88
Fix BTG rate provider 2023-07-29 10:00:34 +02:00
Kukks
abfd6ea1dc
update changelog and version 2023-07-29 09:48:47 +02:00
Kukks
688e873f7a
fixes #5203 2023-07-29 09:15:12 +02:00
Kukks
c88df08350
fixes #5208 2023-07-29 09:15:11 +02:00
Kukks
82586590a7
potentially fixes #5203 2023-07-29 09:15:11 +02:00
Kukks
88c66f30f2
fixes #5204 2023-07-29 09:15:10 +02:00
Kukks
9132592717
fixes #5205 2023-07-29 09:15:10 +02:00
Kukks
c0ffab768a
fix ident 2023-07-29 09:15:10 +02:00
dstrukt
69190081c8
ui+checkout: fix language cutoff bug (#5210) 2023-07-28 21:24:30 +02:00
40 changed files with 675 additions and 441 deletions

View File

@ -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)
{

View File

@ -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",

View File

@ -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);

View File

@ -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]

View File

@ -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}");
}
}

View File

@ -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,

View File

@ -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);
}

View File

@ -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")]

View File

@ -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);
}
}
}

View File

@ -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()

View File

@ -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; }

View File

@ -171,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))
{
@ -208,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();
@ -225,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;
@ -315,7 +315,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
{
Amount = price,
Currency = settings.Currency,
Metadata = new InvoiceMetadata()
Metadata = new InvoiceMetadata
{
ItemCode = choice?.Id,
ItemDesc = title,
@ -347,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)
@ -358,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);
@ -621,7 +624,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
return View("PointOfSale/UpdatePointOfSale", vm);
}
var storeBlob = GetCurrentStore().GetStoreBlob();
var settings = new PointOfSaleSettings
{
Title = vm.Title,
@ -640,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);

View File

@ -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; }

View File

@ -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; }

View File

@ -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)

View File

@ -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));

View File

@ -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);
}
}

View File

@ -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();
}
}
}

View File

@ -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";
};

View File

@ -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>

View File

@ -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))
{

View File

@ -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>
}

View File

@ -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

File diff suppressed because one or more lines are too long

View File

@ -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">

View File

@ -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>

View File

@ -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())

View File

@ -98,7 +98,7 @@
{
<tr>
<td>
<a asp-action="ViewPaymentRequest" asp-route-storeId="@item.StoreId" asp-route-payReqId="@item.Id" id="PaymentRequest-@item.Id">@item.Title</a>
<a asp-action="ViewPaymentRequest" asp-route-payReqId="@item.Id" id="PaymentRequest-@item.Id">@item.Title</a>
</td>
<td>
@(item.ExpiryDate?.ToBrowserDate() ?? new HtmlString("<span class=\"text-muted\">No Expiry</span>"))

View File

@ -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>

View File

@ -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;

View File

@ -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

View File

@ -1,13 +1 @@
<svg width="150" height="100" viewBox="0 0 5220 720" xmlns="http://www.w3.org/2000/svg">
<rect width="5220" height="720" fill="white"/>
<path d="M0 435.197L229.609 291.597V288.121L0 144.259V29.0508L334.901 245.894V333.824L0 550.798V435.197Z" fill="#FF3300"/>
<path d="M486.969 623.844H902.627V719.643H486.969V623.844Z" fill="#FF3300"/>
<path d="M993.879 291.2C993.879 106.422 1084.61 0 1214.37 0C1344.13 0 1434.86 106.422 1434.86 291.2C1434.86 479.061 1344.13 587.581 1214.37 587.581C1084.61 587.581 993.879 479.061 993.879 291.2ZM1345.12 291.2C1345.12 155.01 1293.16 75.9967 1214.37 75.9967C1135.58 75.9967 1083.62 155.01 1083.62 291.2C1083.62 430.473 1135.58 511.584 1214.37 511.584C1293.16 511.584 1344.85 430.473 1344.85 291.2H1345.12Z" fill="#FF3300"/>
<path d="M1593.29 154.29H1663.81L1670.37 205.37H1673.13C1711.31 169.634 1764.71 144.258 1814.44 144.258C1925.96 144.258 1988.02 228.713 1988.02 359.855C1988.02 504.111 1897.95 587.911 1797.77 587.911C1759.13 587.911 1713.54 568.829 1677.39 535.454H1675.29L1679.43 612.237V749.936H1593.29V154.29ZM1899.65 359.855C1899.65 271.269 1867.44 215.599 1791.21 215.599C1756.57 215.599 1717.93 232.713 1679.69 272.121V472.112C1714.79 503.914 1754.61 515.455 1781.57 515.455C1848.75 515.717 1899.65 459.851 1899.65 359.855Z" fill="#FF3300"/>
<path d="M2118.96 365.035C2118.96 227.336 2222.75 143.93 2335.98 143.93C2460.16 143.93 2530.82 225.434 2530.82 343.527C2530.67 359.209 2529.35 374.858 2526.88 390.345H2178.73V327.2H2473.22L2454.52 348.249C2454.52 256.449 2410.17 210.55 2338.47 210.55C2264.41 210.55 2203.66 265.17 2203.66 364.904C2203.66 468.833 2268.8 520.044 2359.79 520.044C2407.09 520.044 2445.08 505.75 2483.39 482.8L2513.56 537.29C2464.7 569.886 2407.32 587.378 2348.57 587.582C2220.39 587.582 2118.96 505.947 2118.96 365.035Z" fill="#FF3300"/>
<path d="M2658.41 154.29H2729.07L2735.63 221.697H2739.04C2781.55 178.289 2829.83 144.258 2895.17 144.258C2994.1 144.258 3039.17 205.042 3039.17 315.201V577.026H2952.9V326.152C2952.9 252.319 2928.11 218.222 2865.39 218.222C2819.47 218.222 2788.31 240.844 2744.68 285.563V577.026H2658.41V154.29Z" fill="#FF3300"/>
<path d="M3208.36 504.308L3259.46 444.376C3303 486.486 3360.93 510.468 3421.5 511.455C3493.27 511.455 3533.03 478.669 3533.03 432.77C3533.03 377.362 3491.63 361.953 3435.41 338.217L3355.57 303.333C3297.64 280.514 3234.21 238.614 3234.21 155.143C3234.21 66.8186 3313.65 0.001814 3425.64 0.001814C3492.01 -0.442962 3555.93 25.0654 3603.75 71.0807L3558.87 126.554C3521.22 93.051 3472.3 74.9951 3421.9 75.9985C3362.2 75.9985 3322.11 103.604 3322.11 150.028C3322.11 199.206 3371.05 217.173 3420.98 236.516L3497.93 270.416C3569.04 298.087 3622.18 339.528 3622.18 422.344C3622.18 513.356 3545.36 587.583 3416.78 587.583C3339.11 587.998 3264.34 558.123 3208.36 504.308V504.308Z" fill="black"/>
<path d="M3762.96 465.557C3762.96 370.741 3849.04 324.055 4062.7 308.186C4059.62 255.73 4031.54 213.896 3956.3 213.896C3905.13 213.896 3854.68 237.304 3813.28 261.5L3780.48 203.994C3827.98 174.684 3898.57 144.062 3971.78 144.062C4088.42 144.062 4148.91 210.945 4148.91 322.678V577.027H4077.73L4070.84 522.144H4068.08C4022.82 557.553 3963.97 587.715 3906.04 587.715C3825.16 587.584 3762.96 540.045 3762.96 465.557ZM4062.7 462.278V363.266C3896.79 375.134 3847.26 408.576 3847.26 459C3847.26 501.489 3885.38 519.063 3930.65 519.063C3975.91 519.063 4018.88 497.883 4062.7 462.278Z" fill="black"/>
<path d="M4388.81 409.884V222.941H4272.17V158.813L4392.16 154.289L4403.44 20.2617H4475.02V154.289H4674.64V222.941H4475.02V410.146C4475.02 482.864 4500.73 518.076 4577.29 518.076C4610.27 517.981 4642.96 511.847 4673.73 499.979L4691.24 562.992C4648.39 578.951 4603.07 587.274 4557.35 587.581C4430.86 587.581 4388.81 516.043 4388.81 409.884Z" fill="black"/>
<path d="M4818.71 521.815L4857.09 466.014C4909.9 502.948 4972.98 522.353 5037.43 521.487C5102.31 521.487 5133.34 495.259 5133.34 463.85C5133.34 433.95 5115.76 415 5009.22 393.034C4897.7 369.887 4845.21 331.2 4845.21 267.203C4845.21 196.518 4908.85 143.93 5025.95 143.93C5093.26 143.93 5157.55 170.158 5199.93 198.878L5159.45 252.646C5117.79 224.668 5068.79 209.592 5018.6 209.304C4955.62 209.304 4931.28 234.155 4931.28 263.138C4931.28 295.923 4965.07 309.037 5048.12 326.938C5185.89 357.101 5220.33 392.509 5220.33 458.736C5220.33 529.487 5151.71 587.582 5026.8 587.582C4952.47 586.641 4880.07 563.76 4818.71 521.815V521.815Z" fill="black"/>
</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="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>

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1 +1 @@
<svg width="155" height="120" viewBox="0 0 155 120" fill="none" xmlns="http://www.w3.org/2000/svg" focusable="false" class="chakra-icon css-13xqvrm"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.06565 43.2477C1.90963 41.2685 -0.665684 35.4843 1.31353 30.3283C3.29274 25.1722 9.07699 22.5969 14.233 24.5761L51.4526 38.8634C51.4937 38.8798 51.535 38.896 51.5765 38.9119L70.2481 46.0792C75.4041 48.0584 81.1883 45.4831 83.1675 40.3271C85.1468 35.1711 82.5714 29.3868 77.4154 27.4076L77.4132 27.4068C77.4139 27.4064 77.4145 27.406 77.4151 27.4056L58.7436 20.2383C53.5876 18.2591 51.0123 12.4749 52.9915 7.31885C54.9707 2.16283 60.755 -0.412485 65.911 1.56673L120.828 22.6473C120.959 22.6977 121.089 22.7506 121.217 22.8059C121.453 22.8928 121.69 22.9815 121.926 23.0721C147.706 32.9681 160.583 61.8894 150.686 87.6695C140.79 113.45 111.869 126.326 86.089 116.43C85.5927 116.24 85.1011 116.042 84.6144 115.838C84.3783 115.766 84.1431 115.686 83.9091 115.596L30.0742 94.9308C24.9182 92.9516 22.3428 87.1673 24.3221 82.0113C26.3013 76.8553 32.0855 74.2799 37.2415 76.2592L55.9106 83.4256C55.9103 83.4242 55.9099 83.4229 55.9095 83.4215L55.9133 83.423C61.0694 85.4022 66.8536 82.8269 68.8328 77.6709C70.812 72.5148 68.2367 66.7306 63.0807 64.7514L54.6786 61.5261C54.6787 61.5257 54.6788 61.5252 54.6789 61.5247L7.06565 43.2477Z" fill="currentColor"></path></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: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -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;

View File

@ -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()
},
});
});

View File

@ -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;
}

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.11.0</Version>
<Version>1.11.2</Version>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,32 @@
# 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
@ -124,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

View File

@ -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