diff --git a/.circleci/config.yml b/.circleci/config.yml index b2537cc40..bb01c1f07 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,24 +7,16 @@ jobs: - checkout test: - machine: true + machine: + docker_layer_caching: true steps: - checkout - run: command: | - echo "117.18.232.200 api.nuget.org" | sudo tee -a /etc/hosts - lsb_release -a - wget -q https://packages.microsoft.com/config/ubuntu/14.04/packages-microsoft-prod.deb - sudo dpkg -i packages-microsoft-prod.deb - sudo apt-get install apt-transport-https - sudo apt-get update - sudo apt-get install dotnet-sdk-2.1 - dotnet --info - dotnet build /p:TreatWarningsAsErrors=true cd BTCPayServer.Tests - dotnet test --filter Fast=Fast - docker-compose up -d dev - dotnet test --filter Integration=Integration + docker-compose down --v + docker-compose build + docker-compose run tests # publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined publish_docker_linuxamd64: @@ -36,7 +28,7 @@ jobs: command: | LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag # - sudo docker build --add-host "api.nuget.org:117.18.232.200" --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f Dockerfile.linuxamd64 . + sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f Dockerfile.linuxamd64 . sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64 @@ -50,7 +42,7 @@ jobs: sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag # - sudo docker build --add-host "api.nuget.org:117.18.232.200" --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 . + sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f Dockerfile.linuxarm32v7 . sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 diff --git a/BTCPayServer.Tests/Dockerfile b/BTCPayServer.Tests/Dockerfile index e63b9fbac..1bc9348ed 100644 --- a/BTCPayServer.Tests/Dockerfile +++ b/BTCPayServer.Tests/Dockerfile @@ -1,12 +1,17 @@ -FROM microsoft/dotnet:2.1.403-sdk-alpine3.7 -WORKDIR /app - # caches restore result by copying csproj file separately -COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj +FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT false +RUN apk add --no-cache icu-libs +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 + +# This should be removed soon https://github.com/dotnet/corefx/issues/30003 +RUN apk add --no-cache curl + +WORKDIR /source COPY BTCPayServer/BTCPayServer.csproj BTCPayServer/BTCPayServer.csproj - -WORKDIR /app/BTCPayServer.Tests -RUN dotnet restore -# copies the rest of your code -COPY . ../. - -ENTRYPOINT ["dotnet", "test"] +COPY BTCPayServer.Tests/BTCPayServer.Tests.csproj BTCPayServer.Tests/BTCPayServer.Tests.csproj +RUN dotnet restore BTCPayServer.Tests/BTCPayServer.Tests.csproj +COPY . . +RUN dotnet build +WORKDIR /source/BTCPayServer.Tests +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index 330a71d06..d8f9b24d1 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -1,4 +1,4 @@ -using BTCPayServer.Tests.Logging; +using BTCPayServer.Tests.Logging; using System.Linq; using NBitcoin; using NBitcoin.DataEncoders; @@ -340,13 +340,17 @@ namespace BTCPayServer.Tests { foreach (var test in new[] { - (0.0005m, "$0.0005 (USD)"), - (0.001m, "$0.001 (USD)"), - (0.01m, "$0.01 (USD)"), - (0.1m, "$0.10 (USD)"), + (0.0005m, "$0.0005 (USD)", "USD"), + (0.001m, "$0.001 (USD)", "USD"), + (0.01m, "$0.01 (USD)", "USD"), + (0.1m, "$0.10 (USD)", "USD"), + (0.1m, "0,10 € (EUR)", "EUR"), + (1000m, "¥1,000 (JPY)", "JPY"), + (1000.0001m, "₹ 1,000.00 (INR)", "INR") }) { - var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, "USD"); + var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, test.Item3); + actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well Assert.Equal(test.Item2, actual); } } @@ -884,7 +888,7 @@ namespace BTCPayServer.Tests var result = client.SendAsync(message).GetAwaiter().GetResult(); result.EnsureSuccessStatusCode(); ///////////////////// - + // Have error 403 with bad signature client = new HttpClient(); HttpRequestMessage mess = new HttpRequestMessage(HttpMethod.Get, tester.PayTester.ServerUri.AbsoluteUri + "tokens"); @@ -1467,6 +1471,44 @@ donation: Assert.NotNull(donationInvoice); Assert.Equal("CAD", donationInvoice.Currency); Assert.Equal("donation", donationInvoice.ItemDesc); + + foreach (var test in new[] + { + (Code: "EUR", ExpectedSymbol: "€", ExpectedDecimalSeparator: ",", ExpectedDivisibility: 2, ExpectedThousandSeparator: "\xa0", ExpectedPrefixed: false, ExpectedSymbolSpace: true), + (Code: "INR", ExpectedSymbol: "₹", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 2, ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: true), + (Code: "JPY", ExpectedSymbol: "¥", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 0, ExpectedThousandSeparator: ",", ExpectedPrefixed: true, ExpectedSymbolSpace: false), + (Code: "BTC", ExpectedSymbol: "BTC", ExpectedDecimalSeparator: ".", ExpectedDivisibility: 8, ExpectedThousandSeparator: ",", ExpectedPrefixed: false, ExpectedSymbolSpace: true), + }) + { + Logs.Tester.LogInformation($"Testing for {test.Code}"); + vmpos = Assert.IsType(Assert.IsType(apps.UpdatePointOfSale(appId).Result).Model); + vmpos.Title = "hello"; + vmpos.Currency = test.Item1; + vmpos.ButtonText = "{0} Purchase"; + vmpos.CustomButtonText = "Nicolas Sexy Hair"; + vmpos.CustomTipText = "Wanna tip?"; + vmpos.Template = @" +apple: + price: 1000.0 + title: good apple +orange: + price: 10.0 +donation: + price: 1.02 + custom: true +"; + Assert.IsType(apps.UpdatePointOfSale(appId, vmpos).Result); + publicApps = user.GetController(); + vmview = Assert.IsType(Assert.IsType(publicApps.ViewPointOfSale(appId).Result).Model); + Assert.Equal(test.Code, vmview.CurrencyCode); + Assert.Equal(test.ExpectedSymbol, vmview.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well); + Assert.Equal(test.ExpectedSymbol, vmview.CurrencyInfo.CurrencySymbol.Replace("¥", "¥")); // Hack so JPY test pass on linux as well); + Assert.Equal(test.ExpectedDecimalSeparator, vmview.CurrencyInfo.DecimalSeparator); + Assert.Equal(test.ExpectedThousandSeparator, vmview.CurrencyInfo.ThousandSeparator); + Assert.Equal(test.ExpectedPrefixed, vmview.CurrencyInfo.Prefixed); + Assert.Equal(test.ExpectedDivisibility, vmview.CurrencyInfo.Divisibility); + Assert.Equal(test.ExpectedSymbolSpace, vmview.CurrencyInfo.SymbolSpace); + } } } @@ -1580,10 +1622,9 @@ donation: var jsonResultPaid = user.GetController().Export("json").GetAwaiter().GetResult(); var paidresult = Assert.IsType(jsonResultPaid); Assert.Equal("application/json", paidresult.ContentType); - Assert.Contains("\"ItemDesc\": \"Some \\\", description\"", paidresult.Content); - Assert.Contains("\"FiatPrice\": 500.0", paidresult.Content); + Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", paidresult.Content); + Assert.Contains("\"InvoicePrice\": 500.0", paidresult.Content); Assert.Contains("\"ConversionRate\": 5000.0", paidresult.Content); - Assert.Contains("\"PaymentDue\": \"0.10020000 BTC\"", paidresult.Content); Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", paidresult.Content); }); @@ -1648,14 +1689,9 @@ donation: var paidresult = Assert.IsType(exportResultPaid); Assert.Equal("application/csv", paidresult.ContentType); Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content); - Assert.Contains($",\"OnChain\",\"0.10020000 BTC\",\"0.10009990 BTC\",\"0.00000000 BTC\",\"5000.0\",\"500.0\"", paidresult.Content); - Assert.Contains($",\"USD\",\"\",\"Some ``, description\",\"new\"", paidresult.Content); + Assert.Contains($",\"OnChain\",\"0.1000999\",\"BTC\",\"5000.0\",\"500.0\"", paidresult.Content); + Assert.Contains($",\"USD\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content); }); - - /* -ReceivedDate,StoreId,OrderId,InvoiceId,CreatedDate,ExpirationDate,MonitoringDate,PaymentId,CryptoCode,Destination,PaymentType,PaymentDue,PaymentPaid,PaymentOverpaid,ConversionRate,FiatPrice,FiatCurrency,ItemCode,ItemDesc,Status -"11/30/2018 10:28:42 AM","7AagXzWdWhLLR3Zar25YLiw2uHAJDzVT4oXGKC9bBCis","orderId","GxtJsWbgxxAXXoCurqyeK6","11/30/2018 10:28:40 AM","11/30/2018 10:43:40 AM","11/30/2018 11:43:40 AM","ec0341537f565d213bc64caa352fbbf9e0deb31cab1f91bccf89db0dd1604457-0","BTC","mqWghCp9RVw8fNgQMLjawyKStxpGfWBk1L","OnChain","0.10020000 BTC","0.10009990 BTC","0.00000000 BTC","5000.0","500.0","USD","","Some ``, description","new" - */ } } diff --git a/BTCPayServer.Tests/UtilitiesTests.cs b/BTCPayServer.Tests/UtilitiesTests.cs index 1d65b9749..1b1a25c2d 100644 --- a/BTCPayServer.Tests/UtilitiesTests.cs +++ b/BTCPayServer.Tests/UtilitiesTests.cs @@ -36,8 +36,6 @@ namespace BTCPayServer.Tests Task.WaitAll(langs.Select(async l => { bool isSourceLang = l == "en"; - if (l == "no") - return; var j = await client.GetTransifexAsync($"https://www.transifex.com/api/2/project/btcpayserver/resource/enjson/translation/{l}/"); if(!isSourceLang) { @@ -56,8 +54,12 @@ namespace BTCPayServer.Tests var langFile = Path.Combine(langsDir, langCode + ".json"); var jobj = JObject.Parse(content); jobj["code"] = langCode; + if ((string)jobj["currentLanguage"] == "English" && !isSourceLang) return; // Not translated + if ((string)jobj["currentLanguage"] == "disable") + return; // Not translated + jobj.AddFirst(new JProperty("NOTICE_WARN", "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/")); if (isSourceLang) { diff --git a/BTCPayServer.Tests/docker-compose.yml b/BTCPayServer.Tests/docker-compose.yml index 7aba34f40..ba03e445e 100644 --- a/BTCPayServer.Tests/docker-compose.yml +++ b/BTCPayServer.Tests/docker-compose.yml @@ -19,10 +19,10 @@ services: TESTS_MYSQL: User ID=root;Host=mysql;Port=3306;Database=btcpayserver TESTS_PORT: 80 TESTS_HOSTNAME: tests - TEST_MERCHANTLIGHTNINGD: "type=clightning;server=/etc/merchant_lightningd_datadir/lightning-rpc" - TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=/etc/customer_lightningd_datadir/lightning-rpc" - TEST_MERCHANTCHARGE: "type=charge;server=https://lightning-charged:9112/;api-token=foiewnccewuify;allowinsecure=true" - TEST_MERCHANTLND: "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true" + TEST_MERCHANTLIGHTNINGD: "type=clightning;server=unix://etc/merchant_lightningd_datadir/lightning-rpc" + TEST_CUSTOMERLIGHTNINGD: "type=clightning;server=unix://etc/customer_lightningd_datadir/lightning-rpc" + TEST_MERCHANTCHARGE: "type=charge;server=http://lightning-charged:9112/;api-token=foiewnccewuify" + TEST_MERCHANTLND: "https://lnd:lnd@merchant_lnd:8080/" TESTS_INCONTAINER: "true" expose: - "80" @@ -36,7 +36,7 @@ services: # The dev container is not actually used, it is just handy to run `docker-compose up dev` to start all services dev: - image: nicolasdorier/docker-bitcoin:0.17.0 + image: btcpayserver/bitcoin:0.17.0 environment: BITCOIN_NETWORK: regtest BITCOIN_EXTRA_ARGS: | @@ -53,7 +53,7 @@ services: - merchant_lnd devlnd: - image: nicolasdorier/docker-bitcoin:0.17.0 + image: btcpayserver/bitcoin:0.17.0 environment: BITCOIN_NETWORK: regtest BITCOIN_EXTRA_ARGS: | @@ -94,7 +94,7 @@ services: - litecoind bitcoind: - image: nicolasdorier/docker-bitcoin:0.17.0 + image: btcpayserver/bitcoin:0.17.0 environment: BITCOIN_NETWORK: regtest BITCOIN_EXTRA_ARGS: | @@ -118,7 +118,7 @@ services: - "bitcoin_datadir:/data" customer_lightningd: - image: nicolasdorier/clightning:v0.6.2-3-dev + image: btcpayserver/lightning:v0.6.2-dev stop_signal: SIGKILL restart: unless-stopped environment: @@ -164,7 +164,7 @@ services: - merchant_lightningd merchant_lightningd: - image: nicolasdorier/clightning:v0.6.2-3-dev + image: btcpayserver/lightning:v0.6.2-dev stop_signal: SIGKILL environment: EXPOSE_TCP: "true" @@ -188,7 +188,7 @@ services: - bitcoind litecoind: - image: nicolasdorier/docker-litecoin:0.15.1 + image: nicolasdorier/docker-litecoin:0.16.3 environment: BITCOIN_EXTRA_ARGS: | rpcuser=ceiwHEbqWI83 diff --git a/BTCPayServer.Tests/docker-entrypoint.sh b/BTCPayServer.Tests/docker-entrypoint.sh new file mode 100755 index 000000000..ec8b479b5 --- /dev/null +++ b/BTCPayServer.Tests/docker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +dotnet test --filter Fast=Fast --no-build +dotnet test --filter Integration=Integration --no-build diff --git a/BTCPayServer/BTCPayServer.csproj b/BTCPayServer/BTCPayServer.csproj index 008ca26b2..36e7d9a28 100644 --- a/BTCPayServer/BTCPayServer.csproj +++ b/BTCPayServer/BTCPayServer.csproj @@ -2,7 +2,7 @@ Exe netcoreapp2.1 - 1.0.3.28 + 1.0.3.31 NU1701,CA1816,CA1308,CA1810,CA2208 @@ -136,6 +136,9 @@ $(IncludeRazorContentInPack) + + $(IncludeRazorContentInPack) + $(IncludeRazorContentInPack) diff --git a/BTCPayServer/Configuration/BTCPayServerOptions.cs b/BTCPayServer/Configuration/BTCPayServerOptions.cs index e0272d854..99c7aea30 100644 --- a/BTCPayServer/Configuration/BTCPayServerOptions.cs +++ b/BTCPayServer/Configuration/BTCPayServerOptions.cs @@ -137,6 +137,17 @@ namespace BTCPayServer.Configuration externalLnd($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc"); externalLnd($"{net.CryptoCode}.external.lnd.rest", "lnd-rest"); + + var spark = conf.GetOrDefault($"{net.CryptoCode}.external.spark", string.Empty); + if(spark.Length != 0) + { + if (!SparkConnectionString.TryParse(spark, out var connectionString)) + { + throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine + + $"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'"); + } + ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString)); + } } Logs.Configuration.LogInformation("Supported chains: " + String.Join(',', supportedChains.ToArray())); diff --git a/BTCPayServer/Configuration/DefaultConfiguration.cs b/BTCPayServer/Configuration/DefaultConfiguration.cs index 1f26bd61c..9e49c5d87 100644 --- a/BTCPayServer/Configuration/DefaultConfiguration.cs +++ b/BTCPayServer/Configuration/DefaultConfiguration.cs @@ -50,6 +50,7 @@ namespace BTCPayServer.Configuration app.Option($"--{crypto}explorercookiefile", $"Path to the cookie file (default: {network.NBXplorerNetwork.DefaultSettings.DefaultCookieFile})", CommandOptionType.SingleValue); app.Option($"--{crypto}lightning", $"Easy configuration of lightning for the server administrator: Must be a UNIX socket of c-lightning (lightning-rpc) or URL to a charge server (default: empty)", CommandOptionType.SingleValue); app.Option($"--{crypto}externallndgrpc", $"The LND gRPC configuration BTCPay will expose to easily connect to the internal lnd wallet from Zap wallet (default: empty)", CommandOptionType.SingleValue); + app.Option($"--{crypto}externalspark", $"The connection string to spark server (default: empty)", CommandOptionType.SingleValue); } return app; } diff --git a/BTCPayServer/Configuration/External/ExternalLnd.cs b/BTCPayServer/Configuration/External/ExternalLnd.cs index af072ab9e..838547df1 100644 --- a/BTCPayServer/Configuration/External/ExternalLnd.cs +++ b/BTCPayServer/Configuration/External/ExternalLnd.cs @@ -8,28 +8,23 @@ namespace BTCPayServer.Configuration.External { public abstract class ExternalLnd : ExternalService { - public ExternalLnd(LightningConnectionString connectionString, LndTypes type) + public ExternalLnd(LightningConnectionString connectionString, string type) { ConnectionString = connectionString; Type = type; } - public LndTypes Type { get; set; } + public string Type { get; set; } public LightningConnectionString ConnectionString { get; set; } } - public enum LndTypes - { - gRPC, Rest - } - public class ExternalLndGrpc : ExternalLnd { - public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, LndTypes.gRPC) { } + public ExternalLndGrpc(LightningConnectionString connectionString) : base(connectionString, "lnd-grpc") { } } public class ExternalLndRest : ExternalLnd { - public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, LndTypes.Rest) { } + public ExternalLndRest(LightningConnectionString connectionString) : base(connectionString, "lnd-rest") { } } } diff --git a/BTCPayServer/Configuration/External/ExternalSpark.cs b/BTCPayServer/Configuration/External/ExternalSpark.cs new file mode 100644 index 000000000..ace4e83b9 --- /dev/null +++ b/BTCPayServer/Configuration/External/ExternalSpark.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Configuration.External +{ + public class ExternalSpark : ExternalService + { + public SparkConnectionString ConnectionString { get; } + + public ExternalSpark(SparkConnectionString connectionString) + { + if (connectionString == null) + throw new ArgumentNullException(nameof(connectionString)); + ConnectionString = connectionString; + } + } +} diff --git a/BTCPayServer/Configuration/SparkConnectionString.cs b/BTCPayServer/Configuration/SparkConnectionString.cs new file mode 100644 index 000000000..636f20431 --- /dev/null +++ b/BTCPayServer/Configuration/SparkConnectionString.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Configuration +{ + public class SparkConnectionString + { + public Uri Server { get; private set; } + public string CookeFile { get; private set; } + + public static bool TryParse(string str, out SparkConnectionString result) + { + if (str == null) + throw new ArgumentNullException(nameof(str)); + + result = null; + var resultTemp = new SparkConnectionString(); + foreach(var kv in str.Split(';') + .Select(part => part.Split('=')) + .Where(kv => kv.Length == 2)) + { + switch (kv[0].ToLowerInvariant()) + { + case "server": + if (resultTemp.Server != null) + return false; + if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute)) + return false; + resultTemp.Server = new Uri(kv[1], UriKind.Absolute); + break; + case "cookiefile": + if (resultTemp.CookeFile != null) + return false; + resultTemp.CookeFile = kv[1]; + break; + default: + return false; + } + } + result = resultTemp; + return true; + } + } +} diff --git a/BTCPayServer/Controllers/AppsPublicController.cs b/BTCPayServer/Controllers/AppsPublicController.cs index c8ea7b594..344ec39fd 100644 --- a/BTCPayServer/Controllers/AppsPublicController.cs +++ b/BTCPayServer/Controllers/AppsPublicController.cs @@ -40,25 +40,26 @@ namespace BTCPayServer.Controllers if (app == null) return NotFound(); var settings = app.GetSettings(); - var currency = _AppsHelper.GetCurrencyData(settings.Currency, false); - double step = currency == null ? 1 : Math.Pow(10, -(currency.Divisibility)); - var numberFormatInfo = _AppsHelper.Currencies.GetNumberFormatInfo(currency.Code) ?? _AppsHelper.Currencies.GetNumberFormatInfo("USD"); + var numberFormatInfo = _AppsHelper.Currencies.GetNumberFormatInfo(settings.Currency) ?? _AppsHelper.Currencies.GetNumberFormatInfo("USD"); + double step = Math.Pow(10, -(numberFormatInfo.CurrencyDecimalDigits)); + return View(new ViewPointOfSaleViewModel() { Title = settings.Title, Step = step.ToString(CultureInfo.InvariantCulture), EnableShoppingCart = settings.EnableShoppingCart, ShowCustomAmount = settings.ShowCustomAmount, - CurrencyCode = currency.Code, - CurrencySymbol = currency.Symbol, + CurrencyCode = settings.Currency, + CurrencySymbol = numberFormatInfo.CurrencySymbol, CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData() { - CurrencySymbol = string.IsNullOrEmpty(currency.Symbol) ? currency.Code : currency.Symbol, - Divisibility = currency.Divisibility, + CurrencySymbol = string.IsNullOrEmpty(numberFormatInfo.CurrencySymbol) ? settings.Currency : numberFormatInfo.CurrencySymbol, + Divisibility = numberFormatInfo.CurrencyDecimalDigits, DecimalSeparator = numberFormatInfo.CurrencyDecimalSeparator, ThousandSeparator = numberFormatInfo.NumberGroupSeparator, - Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern) + Prefixed = new[] { 0, 2 }.Contains(numberFormatInfo.CurrencyPositivePattern), + SymbolSpace = new[] { 2, 3 }.Contains(numberFormatInfo.CurrencyPositivePattern) }, Items = _AppsHelper.Parse(settings.Template, settings.Currency), ButtonText = settings.ButtonText, diff --git a/BTCPayServer/Controllers/InvoiceController.UI.cs b/BTCPayServer/Controllers/InvoiceController.UI.cs index be4764024..b0b4badd8 100644 --- a/BTCPayServer/Controllers/InvoiceController.UI.cs +++ b/BTCPayServer/Controllers/InvoiceController.UI.cs @@ -104,7 +104,7 @@ namespace BTCPayServer.Controllers { var m = new InvoiceDetailsModel.Payment(); m.Crypto = payment.GetPaymentMethodId().CryptoCode; - m.DepositAddress = onChainPaymentData.Output.ScriptPubKey.GetDestinationAddress(paymentNetwork.NBitcoinNetwork); + m.DepositAddress = onChainPaymentData.GetDestination(paymentNetwork); int confirmationCount = 0; if ((onChainPaymentData.ConfirmationCount < paymentNetwork.MaxTrackedConfirmation && payment.Accounted) @@ -493,7 +493,7 @@ namespace BTCPayServer.Controllers [BitpayAPIConstraint(false)] public async Task Export(string format, string searchTerm = null) { - var model = new InvoiceExport(); + var model = new InvoiceExport(_NetworkProvider); var invoices = await ListInvoicesProcess(searchTerm, 0, int.MaxValue); var res = model.Process(invoices, format); diff --git a/BTCPayServer/Controllers/ServerController.cs b/BTCPayServer/Controllers/ServerController.cs index c6b2df2d7..524c957d5 100644 --- a/BTCPayServer/Controllers/ServerController.cs +++ b/BTCPayServer/Controllers/ServerController.cs @@ -46,6 +46,7 @@ namespace BTCPayServer.Controllers RateFetcher rateProviderFactory, SettingsRepository settingsRepository, NBXplorerDashboard dashBoard, + IHttpClientFactory httpClientFactory, LightningConfigurationProvider lnConfigProvider, Services.Stores.StoreRepository storeRepository) { @@ -53,6 +54,7 @@ namespace BTCPayServer.Controllers _UserManager = userManager; _SettingsRepository = settingsRepository; _dashBoard = dashBoard; + HttpClientFactory = httpClientFactory; _RateProviderFactory = rateProviderFactory; _StoreRepository = storeRepository; _LnConfigProvider = lnConfigProvider; @@ -395,6 +397,7 @@ namespace BTCPayServer.Controllers { get; set; } + public IHttpClientFactory HttpClientFactory { get; } [Route("server/emails")] public async Task Emails() @@ -431,6 +434,18 @@ namespace BTCPayServer.Controllers { Crypto = cryptoCode, Type = grpcService.Type, + Action = nameof(LndServices), + Index = i++, + }); + } + i = 0; + foreach (var sparkService in _Options.ExternalServicesByCryptoCode.GetServices(cryptoCode)) + { + result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel() + { + Crypto = cryptoCode, + Type = "Spark server", + Action = nameof(SparkServices), Index = i++, }); } @@ -454,6 +469,40 @@ namespace BTCPayServer.Controllers return View(result); } + [Route("server/services/spark/{cryptoCode}/{index}")] + public async Task SparkServices(string cryptoCode, int index, bool showQR = false) + { + if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud)) + { + StatusMessage = $"Error: {cryptoCode} is not fully synched"; + return RedirectToAction(nameof(Services)); + } + var spark = _Options.ExternalServicesByCryptoCode.GetServices(cryptoCode).Skip(index).Select(c => c.ConnectionString).FirstOrDefault(); + if(spark == null) + { + return NotFound(); + } + + SparkServicesViewModel vm = new SparkServicesViewModel(); + vm.ShowQR = showQR; + try + { + var cookie = (spark.CookeFile == "fake" + ? "fake:fake:fake" // If we are testing, it should not crash + : await System.IO.File.ReadAllTextAsync(spark.CookeFile)).Split(':'); + if (cookie.Length >= 3) + { + vm.SparkLink = $"{spark.Server.AbsoluteUri}?access-key={cookie[2]}"; + } + } + catch(Exception ex) + { + StatusMessage = $"Error: {ex.Message}"; + return RedirectToAction(nameof(Services)); + } + return View(vm); + } + [Route("server/services/lnd/{cryptoCode}/{index}")] public IActionResult LndServices(string cryptoCode, int index, uint? nonce) { diff --git a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs index c33f3e963..47fc3630d 100644 --- a/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs +++ b/BTCPayServer/Models/AppViewModels/ViewPointOfSaleViewModel.cs @@ -28,7 +28,8 @@ namespace BTCPayServer.Models.AppViewModels public string CurrencySymbol { get; set; } public string ThousandSeparator { get; set; } public string DecimalSeparator { get; set; } - public int Divisibility { get; internal set; } + public int Divisibility { get; set; } + public bool SymbolSpace { get; set; } } public CurrencyInfoData CurrencyInfo { get; set; } diff --git a/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs b/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs index e3ddab4ea..6e915f1a4 100644 --- a/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs +++ b/BTCPayServer/Models/ServerViewModels/ServicesViewModel.cs @@ -11,8 +11,9 @@ namespace BTCPayServer.Models.ServerViewModels public class LNDServiceViewModel { public string Crypto { get; set; } - public LndTypes Type { get; set; } + public string Type { get; set; } public int Index { get; set; } + public string Action { get; internal set; } } public class ExternalService diff --git a/BTCPayServer/Models/ServerViewModels/SparkServicesViewModel.cs b/BTCPayServer/Models/ServerViewModels/SparkServicesViewModel.cs new file mode 100644 index 000000000..45ff1e791 --- /dev/null +++ b/BTCPayServer/Models/ServerViewModels/SparkServicesViewModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace BTCPayServer.Models.ServerViewModels +{ + public class SparkServicesViewModel + { + public string SparkLink { get; set; } + public bool ShowQR { get; set; } + } +} diff --git a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs index 9026aaf13..d31c0a52e 100644 --- a/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs +++ b/BTCPayServer/Payments/Bitcoin/BitcoinLikePaymentData.cs @@ -78,5 +78,15 @@ namespace BTCPayServer.Payments.Bitcoin } return false; } + + public BitcoinAddress GetDestination(BTCPayNetwork network) + { + return Output.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork); + } + + string CryptoPaymentData.GetDestination(BTCPayNetwork network) + { + return GetDestination(network).ToString(); + } } } diff --git a/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs b/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs index 4f2cc37fb..6d630f3a2 100644 --- a/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs +++ b/BTCPayServer/Payments/Lightning/LightningLikePaymentData.cs @@ -16,6 +16,12 @@ namespace BTCPayServer.Payments.Lightning [JsonConverter(typeof(LightMoneyJsonConverter))] public LightMoney Amount { get; set; } public string BOLT11 { get; set; } + + public string GetDestination(BTCPayNetwork network) + { + return GetPaymentId(); + } + public string GetPaymentId() { return BOLT11; diff --git a/BTCPayServer/Properties/launchSettings.json b/BTCPayServer/Properties/launchSettings.json index 8caa253aa..5bfafb34c 100644 --- a/BTCPayServer/Properties/launchSettings.json +++ b/BTCPayServer/Properties/launchSettings.json @@ -29,6 +29,7 @@ "BTCPAY_BTCLIGHTNING": "type=charge;server=http://127.0.0.1:54938/;api-token=foiewnccewuify", "BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true", "BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true", + "BTCPAY_BTCEXTERNALSPARK": "server=https://127.0.0.1:53280/spark/btc/;cookiefile=fake", "BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/", "ASPNETCORE_ENVIRONMENT": "Development", "BTCPAY_CHAINS": "btc,ltc", diff --git a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs index f71366ec1..543759ee3 100644 --- a/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs +++ b/BTCPayServer/Services/Invoices/Export/InvoiceExport.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using BTCPayServer.Payments.Bitcoin; @@ -10,6 +11,12 @@ namespace BTCPayServer.Services.Invoices.Export { public class InvoiceExport { + public BTCPayNetworkProvider Networks { get; } + + public InvoiceExport(BTCPayNetworkProvider networks) + { + Networks = networks; + } public string Process(InvoiceEntity[] invoices, string fileFormat) { var csvInvoices = new List(); @@ -55,9 +62,7 @@ namespace BTCPayServer.Services.Invoices.Export var cryptoCode = payment.GetPaymentMethodId().CryptoCode; var pdata = payment.GetCryptoPaymentData(); - var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), null); - var accounting = pmethod.Calculate(); - var details = pmethod.GetPaymentMethodDetails(); + var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks); var target = new ExportInvoiceHolder { @@ -65,25 +70,24 @@ namespace BTCPayServer.Services.Invoices.Export PaymentId = pdata.GetPaymentId(), CryptoCode = cryptoCode, ConversionRate = pmethod.Rate, - PaymentType = details.GetPaymentType() == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain", - Destination = details.GetPaymentDestination(), - PaymentDue = $"{accounting.MinimumTotalDue} {cryptoCode}", - PaymentPaid = $"{accounting.CryptoPaid} {cryptoCode}", - PaymentOverpaid = $"{accounting.OverpaidHelper} {cryptoCode}", - + PaymentType = payment.GetPaymentMethodId().PaymentType == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain", + Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork(cryptoCode)), + Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture), OrderId = invoice.OrderId, StoreId = invoice.StoreId, InvoiceId = invoice.Id, - CreatedDate = invoice.InvoiceTime.UtcDateTime, - ExpirationDate = invoice.ExpirationTime.UtcDateTime, - MonitoringDate = invoice.MonitoringExpiration.UtcDateTime, + InvoiceCreatedDate = invoice.InvoiceTime.UtcDateTime, + InvoiceExpirationDate = invoice.ExpirationTime.UtcDateTime, + InvoiceMonitoringDate = invoice.MonitoringExpiration.UtcDateTime, #pragma warning disable CS0618 // Type or member is obsolete - Status = invoice.StatusString, + InvoiceFullStatus = invoice.GetInvoiceState().ToString(), + InvoiceStatus = invoice.StatusString, + InvoiceExceptionStatus = invoice.ExceptionStatusString, #pragma warning restore CS0618 // Type or member is obsolete - ItemCode = invoice.ProductInformation?.ItemCode, - ItemDesc = invoice.ProductInformation?.ItemDesc, - FiatPrice = invoice.ProductInformation?.Price ?? 0, - FiatCurrency = invoice.ProductInformation?.Currency, + InvoiceItemCode = invoice.ProductInformation.ItemCode, + InvoiceItemDesc = invoice.ProductInformation.ItemDesc, + InvoicePrice = invoice.ProductInformation.Price, + InvoiceCurrency = invoice.ProductInformation.Currency, }; exportList.Add(target); @@ -101,23 +105,23 @@ namespace BTCPayServer.Services.Invoices.Export public string StoreId { get; set; } public string OrderId { get; set; } public string InvoiceId { get; set; } - public DateTime CreatedDate { get; set; } - public DateTime ExpirationDate { get; set; } - public DateTime MonitoringDate { get; set; } + public DateTime InvoiceCreatedDate { get; set; } + public DateTime InvoiceExpirationDate { get; set; } + public DateTime InvoiceMonitoringDate { get; set; } public string PaymentId { get; set; } - public string CryptoCode { get; set; } public string Destination { get; set; } public string PaymentType { get; set; } - public string PaymentDue { get; set; } - public string PaymentPaid { get; set; } - public string PaymentOverpaid { get; set; } + public string Paid { get; set; } + public string CryptoCode { get; set; } public decimal ConversionRate { get; set; } - public decimal FiatPrice { get; set; } - public string FiatCurrency { get; set; } - public string ItemCode { get; set; } - public string ItemDesc { get; set; } - public string Status { get; set; } + public decimal InvoicePrice { get; set; } + public string InvoiceCurrency { get; set; } + public string InvoiceItemCode { get; set; } + public string InvoiceItemDesc { get; set; } + public string InvoiceFullStatus { get; set; } + public string InvoiceStatus { get; set; } + public string InvoiceExceptionStatus { get; set; } } } diff --git a/BTCPayServer/Services/Invoices/InvoiceEntity.cs b/BTCPayServer/Services/Invoices/InvoiceEntity.cs index e223f8690..d586d27b3 100644 --- a/BTCPayServer/Services/Invoices/InvoiceEntity.cs +++ b/BTCPayServer/Services/Invoices/InvoiceEntity.cs @@ -982,5 +982,6 @@ namespace BTCPayServer.Services.Invoices bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network); PaymentTypes GetPaymentType(); + string GetDestination(BTCPayNetwork network); } } diff --git a/BTCPayServer/Views/Account/ResetPassword.cshtml b/BTCPayServer/Views/Account/ResetPassword.cshtml index a501ff2df..d802fc462 100644 --- a/BTCPayServer/Views/Account/ResetPassword.cshtml +++ b/BTCPayServer/Views/Account/ResetPassword.cshtml @@ -3,33 +3,39 @@ ViewData["Title"] = "Reset password"; } -

@ViewData["Title"]

-

Reset your password.

-
-
-
-
-
- -
- - - +
+
+
+
+
-
- - - +
+
+
+ +
+ +
+ + + +
+
+ + + +
+
+ + + +
+ +
-
- - - -
- - +
-
+
@section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") diff --git a/BTCPayServer/Views/Account/ResetPasswordConfirmation.cshtml b/BTCPayServer/Views/Account/ResetPasswordConfirmation.cshtml index 7967cf437..37c35eb05 100644 --- a/BTCPayServer/Views/Account/ResetPasswordConfirmation.cshtml +++ b/BTCPayServer/Views/Account/ResetPasswordConfirmation.cshtml @@ -3,6 +3,12 @@ }

@ViewData["Title"]

-

- Your password has been reset. Please click here to log in. -

+
+
+
+
+ Your password has been reset. Please click here to log in. +
+
+
+
diff --git a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml index 9edaeaa38..be9f81fda 100644 --- a/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml +++ b/BTCPayServer/Views/Apps/UpdatePointOfSale.cshtml @@ -3,6 +3,26 @@ ViewData["Title"] = "Update Point of Sale"; }
+ +
@@ -58,9 +78,17 @@
+
+ * + +
+
+
* - +
@@ -101,5 +129,56 @@ + + + + + + + } diff --git a/BTCPayServer/Views/Home/Home.cshtml b/BTCPayServer/Views/Home/Home.cshtml index c6d5fb9d6..7a415ea7d 100644 --- a/BTCPayServer/Views/Home/Home.cshtml +++ b/BTCPayServer/Views/Home/Home.cshtml @@ -9,8 +9,8 @@

Welcome to BTCPay Server


-

BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business. The API is compatible with Bitpay service to allow seamless migration.

- Getting started +

BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business.

+ Getting started
diff --git a/BTCPayServer/Views/Server/LndServices.cshtml b/BTCPayServer/Views/Server/LndServices.cshtml index ff0fbe69f..9f547bd61 100644 --- a/BTCPayServer/Views/Server/LndServices.cshtml +++ b/BTCPayServer/Views/Server/LndServices.cshtml @@ -88,10 +88,10 @@ } @if (Model.RestrictedMacaroon != null) { -
+ @*
-
+
*@ } @if (Model.CertificateThumbprint != null) { diff --git a/BTCPayServer/Views/Server/Services.cshtml b/BTCPayServer/Views/Server/Services.cshtml index 2eb1b6565..8991585e6 100644 --- a/BTCPayServer/Views/Server/Services.cshtml +++ b/BTCPayServer/Views/Server/Services.cshtml @@ -34,16 +34,9 @@ { @lnd.Crypto - LND @lnd.Type.ToString() + @lnd.Type - @if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.gRPC) - { - See information - } - else if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.Rest) - { - See information - } + See information } diff --git a/BTCPayServer/Views/Server/SparkServices.cshtml b/BTCPayServer/Views/Server/SparkServices.cshtml new file mode 100644 index 000000000..2ce0c8729 --- /dev/null +++ b/BTCPayServer/Views/Server/SparkServices.cshtml @@ -0,0 +1,79 @@ +@model SparkServicesViewModel +@{ + ViewData.SetActivePageAndTitle(ServerNavPages.Services); +} + + +

Spark service

+ + +@if (Model.ShowQR) +{ + +} + +
+
+
+
+
+ +
+ +
+
+
Browser connection
+

+ You can go to spark from your browser by clicking here
+

+
+ +
+
QR Code connection
+

+ You can use QR Code to connect to your clightning from your mobile.
+

+
+
+ @if (!Model.ShowQR) + { +
+
+ + +
+
+ } + else + { +
+
+
+
+ } +
+
+
+ +@section Scripts { + @await Html.PartialAsync("_ValidationScriptsPartial") + + @if (Model.ShowQR) + { + + + } +} diff --git a/BTCPayServer/wwwroot/cart/js/cart.js b/BTCPayServer/wwwroot/cart/js/cart.js index d7fe8269f..4510fa8a5 100644 --- a/BTCPayServer/wwwroot/cart/js/cart.js +++ b/BTCPayServer/wwwroot/cart/js/cart.js @@ -249,9 +249,15 @@ Cart.prototype.formatCurrency = function(amount, currency, symbol) { if (srvModel.currencyInfo.prefixed) { prefix = srvModel.currencyInfo.currencySymbol; + if (srvModel.currencyInfo.symbolSpace) { + prefix = prefix + ' '; + } } else { postfix = srvModel.currencyInfo.currencySymbol; + if (srvModel.currencyInfo.symbolSpace) { + postfix = ' ' + postfix; + } } thousandsSep = srvModel.currencyInfo.thousandSeparator; decimalSep = srvModel.currencyInfo.decimalSeparator; diff --git a/BTCPayServer/wwwroot/locales/hi.json b/BTCPayServer/wwwroot/locales/hi.json new file mode 100644 index 000000000..d0fe9b08d --- /dev/null +++ b/BTCPayServer/wwwroot/locales/hi.json @@ -0,0 +1,47 @@ +{ + "NOTICE_WARN": "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/", + "code": "hi", + "currentLanguage": "हिंदी", + "lang": "भाषा", + "Awaiting Payment...": "भुगतान के लिए प्रतीक्षा कर रहे हैं…", + "Pay with": "इसके साथ भुगतान करें", + "Contact and Refund Email": "संपर्क और धनवापसी ईमेल", + "Contact_Body": "कृपया नीचे एक ईमेल पता प्रदान करें। यदि आपके भुगतान में कोई समस्या होती है तो हम इस पते पर आपसे संपर्क करेंगे।", + "Your email": "आपका ईमेल", + "Continue": "जारी रखें", + "Please enter a valid email address": "कृपया एक वैध ईमेल पता दर्ज करें", + "Order Amount": "ऑर्डर की राशि", + "Network Cost": "नेटवर्क लागत", + "Already Paid": "भुगतान पहले ही किया जा चुका है", + "Due": "देय", + "Scan": "स्कैन करें", + "Copy": "कॉपी करें", + "Conversion": "रूपांतरण", + "Open in wallet": "वॉलेट खोलें", + "CompletePay_Body": "अपना भुगतान पूरा करने के लिए, कृपया नीचे दिए गए पते पर {{btcDue}} {{cryptoCode}} भेजें.", + "Amount": "धनराशि", + "Address": "पता", + "Copied": "कॉपी किया गया", + "ConversionTab_BodyTop": "आप मर्चेंट द्वारा सीधे समर्थित भुगतान विकल्पों के मुकाबले {{btcDue}} {{cryptoCode}} का भुगतान altcoins का उपयोग करके भी कर सकते हैं।.", + "ConversionTab_BodyDesc": "यह सेवा किसी तृतीय पक्ष द्वारा प्रदान की जाती है। कृपया ध्यान रखें कि प्रदाता आपके धन को कैसे आगे बढ़ाएंगे इस पर हमारा कोई नियंत्रण नहीं है। केवल {{cryptoCode}} ब्लॉकचेन पर धन प्राप्त होने के बाद ही चालान को चिह्नित किया जाएगा।.", + "ConversionTab_CalculateAmount_Error": "Retry", + "ConversionTab_LoadCurrencies_Error": "Retry", + "ConversionTab_Lightning": "लाइटनिंग नेटवर्क भुगतान के लिए कोई रूपांतरण प्रदाता उपलब्ध नहीं है।", + "ConversionTab_CurrencyList_Select_Option": "Please select a currency to convert from", + "Invoice expiring soon...": "चालान जल्द ही समाप्त हो जाएगा...", + "Invoice expired": "चालान की समय सीमा समाप्त हो गयी", + "What happened?": "क्या हुआ?", + "InvoiceExpired_Body_1": "इस चालान की समयसीमा समाप्त हो गयी। कोई भी चालान केवल {{maxTimeMinutes}} मिनटों के लिए वैध रहता है। \nयदि आप अपना भुगतान दोबारा जमा करना चाहते हैं तो आप {{storeName}} पर वापस जा सकते हैं।", + "InvoiceExpired_Body_2": "यदि आपने भुगतान भेजने का प्रयास किया है, तो इसे अभी तक नेटवर्क द्वारा स्वीकार नहीं किया गया है। हमें अभी तक आपकी राशि प्राप्त नहीं हुई है।", + "InvoiceExpired_Body_3": "", + "Invoice ID": "चालान आईडी", + "Order ID": "आर्डर आईडी", + "Return to StoreName": "{{storeName}} पर वापस जाएं", + "This invoice has been paid": "इस चालान का भुगतान कर दिया गया है", + "This invoice has been archived": "यह चालान संग्रहीत कर दिया गया है", + "Archived_Body": "ऑर्डर जानकारी या सहायता के लिए स्टोर से संपर्क करें", + "BOLT 11 Invoice": "BOLT 11 चालान", + "Node Info": "नोड जानकारी", + "txCount": "लेनदेन", + "txCount_plural": "लेनदेनों" +} \ No newline at end of file diff --git a/BTCPayServer/wwwroot/locales/pl.json b/BTCPayServer/wwwroot/locales/pl.json index c6bb6df4a..aee8328f2 100644 --- a/BTCPayServer/wwwroot/locales/pl.json +++ b/BTCPayServer/wwwroot/locales/pl.json @@ -5,15 +5,15 @@ "lang": "Język", "Awaiting Payment...": "Oczekiwanie na płatność...", "Pay with": "Płać z", - "Contact and Refund Email": "Email do kontaktu reklamacji", + "Contact and Refund Email": "Email do kontaktu i reklamacji", "Contact_Body": "Proszę podać adres email poniżej. Jeżeli będzie problem z płatnością użyjemy go do kontaktu", "Your email": "Twój email", - "Continue": "Kontynuacja", + "Continue": "Dalej", "Please enter a valid email address": "Proszę podać prawidłowy adres email", "Order Amount": "Kwota zamówienia", "Network Cost": "Koszt sieci", "Already Paid": "Już zapłacone", - "Due": "Z powodu", + "Due": "Razem", "Scan": "Skan", "Copy": "Kopia", "Conversion": "Konwersja", diff --git a/BTCPayServer/wwwroot/locales/sk-SK.json b/BTCPayServer/wwwroot/locales/sk-SK.json index 77d3715b8..5418685e3 100644 --- a/BTCPayServer/wwwroot/locales/sk-SK.json +++ b/BTCPayServer/wwwroot/locales/sk-SK.json @@ -32,8 +32,8 @@ "Invoice expired": "Platnosť faktúry uplynula", "What happened?": "Čo sa stalo?", "InvoiceExpired_Body_1": "Platnosť tejto faktúry vypršala. Faktúra platí iba {{maxTimeMinutes}} minút. \nMôžete sa vrátiť na {{storeName}}, ak chcete platbu znova odoslať.", - "InvoiceExpired_Body_2": "Ak ste sa pokúsili odoslať platbu, sieť ju ešte neprijala. Zatia+l sme nedostali Vaše finančné prostriedky.", - "InvoiceExpired_Body_3": "", + "InvoiceExpired_Body_2": "Ak ste sa pokúsili odoslať platbu, sieť ju ešte neprijala. Zatiaľ sme nedostali Vaše finančné prostriedky.", + "InvoiceExpired_Body_3": "Ak ju obdržíme neskôr, Vašu objednávku buď spracujeme alebo Vás budeme kontaktovať, aby sme sa dohodli na vrátení...", "Invoice ID": "ID faktúry", "Order ID": "ID objednávky", "Return to StoreName": "Vrátiť sa na {{storeName}}", diff --git a/BTCPayServer/wwwroot/products/js/products.jquery.js b/BTCPayServer/wwwroot/products/js/products.jquery.js new file mode 100644 index 000000000..6f89c4bc4 --- /dev/null +++ b/BTCPayServer/wwwroot/products/js/products.jquery.js @@ -0,0 +1,69 @@ +$(document).ready(function(){ + var products = new Products(), + delay = null; + + $('.js-product-template').on('input', function(){ + products.loadFromTemplate(); + + clearTimeout(delay); + + // Delay rebuilding DOM for performance reasons + delay = setTimeout(function(){ + products.showAll(); + }, 1000); + }); + + $('.js-products').on('click', '.js-product-remove', function(event){ + event.preventDefault(); + + var id = $(this).closest('.card').parent().index(); + + products.removeItem(id); + }); + + $('.js-products').on('click', '.js-product-edit', function(event){ + event.preventDefault(); + + var id = $(this).closest('.card').parent().index(); + + products.itemContent(id); + }); + + $('.js-product-save').click(function(event){ + event.preventDefault(); + + var index = $('.js-product-index').val(), + description = $('.js-product-description').val(), + image = $('.js-product-image').val(), + custom = $('.js-product-custom').val(); + obj = { + id: products.escape($('.js-product-id').val()), + price: products.escape($('.js-product-price').val()), + title: products.escape($('.js-product-title').val()), + }; + + // Only continue if price and title is provided + if (obj.price && obj.title) { + if (description) { + obj.description = products.escape(description); + } + if (image) { + obj.image = products.escape(image); + } + if (custom == 'true') { + obj.custom = products.escape(custom); + } + + // Create an id from the title for a new product + if (!Boolean(index)) { + obj.id = products.escape(obj.title.toLowerCase() + ':'); + } + + products.saveItem(obj, index); + } + }); + + $('.js-product-add').click(function(){ + products.itemContent(); + }); +}); \ No newline at end of file diff --git a/BTCPayServer/wwwroot/products/js/products.js b/BTCPayServer/wwwroot/products/js/products.js new file mode 100644 index 000000000..cc595badf --- /dev/null +++ b/BTCPayServer/wwwroot/products/js/products.js @@ -0,0 +1,186 @@ +function Products() { + this.products = []; + + // Get products from template + this.loadFromTemplate(); + + // Show products in the DOM + this.showAll(); +} + +Products.prototype.loadFromTemplate = function() { + var template = $('.js-product-template').val().trim(), + lines = template.split("\n\n"); + + this.products = []; + + // Split products from the template + for (var kl in lines) { + var line = lines[kl], + product = line.split("\n"), + id, price, title, description, image = null, + custom; + + for (var kp in product) { + var productProperty = product[kp].trim(); + + if (kp == 0) { + id = productProperty; + } + + if (productProperty.indexOf('price:') !== -1) { + price = parseFloat(productProperty.replace('price:', '').trim()); + } + if (productProperty.indexOf('title:') !== -1) { + title = productProperty.replace('title:', '').trim(); + } + if (productProperty.indexOf('description:') !== -1) { + description = productProperty.replace('description:', '').trim(); + } + if (productProperty.indexOf('image:') !== -1) { + image = productProperty.replace('image:', '').trim(); + } + if (productProperty.indexOf('custom:') !== -1) { + custom = productProperty.replace('custom:', '').trim(); + } + } + + if (price != null || title != null) { + // Add product to the list + this.products.push({ + 'id': id, + 'title': title, + 'price': price, + 'image': image || null, + 'description': description || null, + 'custom': Boolean(custom) + }); + } + + } +} + +Products.prototype.saveTemplate = function() { + var template = ''; + + // Construct template from the product list + for (var key in this.products) { + var product = this.products[key], + id = product.id, + title = product.title, + price = product.price, + image = product.image + description = product.description, + custom = product.custom; + + template += id + '\n' + + ' price: ' + price + '\n' + + ' title: ' + title + '\n'; + + if (description) { + template += ' description: ' + description + '\n'; + } + if (image) { + template += ' image: ' + image + '\n'; + } + if (custom) { + template += ' custom: true\n'; + } + template += '\n'; + } + + $('.js-product-template').val(template); +} + +Products.prototype.showAll = function() { + var list = []; + + for (var key in this.products) { + var product = this.products[key], + image = product.image; + + list.push(this.template($('#template-product-item'), { + 'title': this.escape(product.title), + 'image': image ? 'Card image cap' : '' + })); + } + + $('.js-products').html(list); +} + +// Load the template +Products.prototype.template = function($template, obj) { + var template = $template.text(); + + for (var key in obj) { + var re = new RegExp('{' + key + '}', 'mg'); + template = template.replace(re, obj[key]); + } + + return template; +} + +Products.prototype.saveItem = function(obj, index) { + // Edit product + if (index) { + this.products[index] = obj; + } else { // Add new product + this.products.push(obj); + } + + this.saveTemplate(); + this.showAll(); + this.modalEmpty(); +} + +Products.prototype.removeItem = function(index) { + if (this.products.length == 1) { + this.products = []; + $('.js-products').html('No products.'); + } else { + this.products.splice(index, 1); + $('.js-products').find('.card').parent().eq(index).remove(); + } + + this.saveTemplate(); +} + +Products.prototype.itemContent = function(index) { + var product = null, + custom = false; + + // Existing product + if (!isNaN(index)) { + product = this.products[index]; + custom = product.custom; + } + + var template = this.template($('#template-product-content'), { + 'id': product != null ? this.escape(product.id) : '', + 'index': isNaN(index) ? '' : this.escape(index), + 'price': product != null ? this.escape(product.price) : '', + 'title': product != null ? this.escape(product.title) : '', + 'description': product != null ? this.escape(product.description) : '', + 'image': product != null ? this.escape(product.image) : '', + 'custom': '' + }); + + $('#product-modal').find('.modal-body').html(template); +} + +Products.prototype.modalEmpty = function() { + var $modal = $('#product-modal'); + + $modal.modal('hide'); + $modal.find('.modal-body').empty(); +} + +Products.prototype.escape = function(input) { + return ('' + input) /* Forces the conversion to string. */ + .replace(/&/g, '&') /* This MUST be the 1st replacement. */ + .replace(/'/g, ''') /* The 4 other predefined entities, required. */ + .replace(/"/g, '"') + .replace(//g, '>') + ; +} \ No newline at end of file