Merge remote-tracking branch 'btcpayserver/master' into feature/coinswitch

This commit is contained in:
Kukks
2018-12-19 10:41:36 +01:00
36 changed files with 825 additions and 152 deletions

View File

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

View File

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

View File

@@ -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<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(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<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
publicApps = user.GetController<AppsPublicController>();
vmview = Assert.IsType<ViewPointOfSaleViewModel>(Assert.IsType<ViewResult>(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<InvoiceController>().Export("json").GetAwaiter().GetResult();
var paidresult = Assert.IsType<ContentResult>(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<ContentResult>(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"
*/
}
}

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
#!/bin/sh
set -e
dotnet test --filter Fast=Fast --no-build
dotnet test --filter Integration=Integration --no-build

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.28</Version>
<Version>1.0.3.31</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@@ -136,6 +136,9 @@
<Content Update="Views\Home\BitpayTranslator.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SparkServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>

View File

@@ -137,6 +137,17 @@ namespace BTCPayServer.Configuration
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
var spark = conf.GetOrDefault<string>($"{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()));

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,25 +40,26 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
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,

View File

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

View File

@@ -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<IActionResult> 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<ExternalSpark>(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<IActionResult> 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<ExternalSpark>(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)
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -982,5 +982,6 @@ namespace BTCPayServer.Services.Invoices
bool PaymentConfirmed(PaymentEntity entity, SpeedPolicy speedPolicy, BTCPayNetwork network);
PaymentTypes GetPaymentType();
string GetDestination(BTCPayNetwork network);
}
}

View File

@@ -3,33 +3,39 @@
ViewData["Title"] = "Reset password";
}
<h2>@ViewData["Title"]</h2>
<h4>Reset your password.</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Code" type="hidden" />
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Code" type="hidden" />
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Password"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword"></label>
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Reset</button>
</form>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword"></label>
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-primary">Reset</button>
</form>
</div>
</div>
</div>
</section>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")

View File

@@ -3,6 +3,12 @@
}
<h2>@ViewData["Title"]</h2>
<p>
Your password has been reset. Please <a asp-action="Login">click here to log in</a>.
</p>
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
Your password has been reset. Please <a asp-action="Login">click here to log in</a>.
</div>
</div>
</div>
</section>

View File

@@ -3,6 +3,26 @@
ViewData["Title"] = "Update Point of Sale";
}
<section>
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Product management</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Modal body text goes here.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
@@ -58,9 +78,17 @@
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label">Products</label>*
<div class="mb-3">
<a class="js-product-add btn btn-secondary" href="#" data-toggle="modal" data-target="#product-modal"><i class="fa fa-plus fa-fw"></i> Add Product</a>
</div>
<div class="js-products bg-light row p-3">
</div>
</div>
<div class="form-group">
<label asp-for="Template" class="control-label"></label>*
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>
<textarea asp-for="Template" rows="10" cols="40" class="js-product-template form-control"></textarea>
<span asp-validation-for="Template" class="text-danger"></span>
</div>
<div class="form-group">
@@ -101,5 +129,56 @@
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css">
<script src="~/vendor/highlightjs/highlight.min.js"></script>
<script>hljs.initHighlightingOnLoad();</script>
<script id="template-product-item" type="text/template">
<div class="col-sm-4 col-md-3 mb-3">
<div class="card">
{image}
<div class="card-body">
<h6 class="card-title">{title}</h6>
<a href="#" class="js-product-edit btn btn-primary" data-toggle="modal" data-target="#product-modal">Edit</a>
<a href="#" class="js-product-remove btn btn-danger"><i class="fa fa-trash"></i></a>
</div>
</div>
</div>
</script>
<script id="template-product-content" type="text/template">
<div class="mb-3">
<input class="js-product-id" type="hidden" name="id" value="{id}">
<input class="js-product-index" type="hidden" name="index" value="{index}">
<div class="form-row">
<div class="col-sm-6">
<label>Title</label>*
<input type="text" class="js-product-title form-control mb-2" value="{title}" autofocus />
</div>
<div class="col-sm-3">
<label>Price</label>*
<input type="text" class="js-product-price form-control mb-2" value="{price}" />
</div>
<div class="col-sm-3">
<label>Custom price</label>
<select class="js-product-custom form-control">
{custom}
</select>
</div>
</div>
<div class="form-row">
<div class="col">
<label>Image</label>
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
</div>
</div>
<div class="form-row">
<div class="col">
<label>Description</label>
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
</div>
</div>
</div>
</script>
<script src="~/products/js/products.js"></script>
<script src="~/products/js/products.jquery.js"></script>
}

View File

@@ -9,8 +9,8 @@
<div class="header-content-inner text-white">
<h1>Welcome to BTCPay Server</h1>
<hr />
<p>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.</p>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://github.com/btcpayserver/btcpayserver-doc">Getting started</a>
<p>BTCPay Server is a free and open source server for merchants wanting to accept Bitcoin for their business.</p>
<a style="background-color: #fff;color: #222;display:inline-block;text-align: center;white-space: nowrap;vertical-align: middle;user-select: none;line-height: 1.25;font-size: 1rem;text-decoration:none;font-weight: 700; text-transform: uppercase;border: none;border-radius: 300px;padding: 15px 30px;" href="https://docs.btcpayserver.org">Getting started</a>
</div>
</div>
</header>

View File

@@ -88,10 +88,10 @@
}
@if (Model.RestrictedMacaroon != null)
{
<div class="form-group">
@*<div class="form-group">
<label asp-for="RestrictedMacaroon"></label>
<input asp-for="RestrictedMacaroon" readonly class="form-control" />
</div>
</div>*@
}
@if (Model.CertificateThumbprint != null)
{

View File

@@ -34,16 +34,9 @@
{
<tr>
<td>@lnd.Crypto</td>
<td>LND @lnd.Type.ToString()</td>
<td>@lnd.Type</td>
<td style="text-align:right">
@if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.gRPC)
{
<a asp-action="LNDServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
}
else if (lnd.Type == BTCPayServer.Configuration.External.LndTypes.Rest)
{
<a asp-action="LNDServices" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
}
<a asp-action="@lnd.Action" asp-route-cryptoCode="@lnd.Crypto" asp-route-index="@lnd.Index">See information</a>
</td>
</tr>
}

View File

@@ -0,0 +1,79 @@
@model SparkServicesViewModel
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>Spark service</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
@if (Model.ShowQR)
{
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<div>
<span><b>CONFIDENTIAL:</b> This QR Code is confidential, close this window as soon as you don't need it anymore.<br /></span>
<span>A malicious actor with access to this QR Code could steal the funds on your lightning wallet.</span>
</div>
</div>
}
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<h5>Browser connection</h5>
<p>
<span>You can go to spark from your browser by <a href="@Model.SparkLink" target="_blank">clicking here</a><br /></span>
</p>
</div>
<div class="form-group">
<h5>QR Code connection</h5>
<p>
<span>You can use QR Code to connect to your clightning from your mobile.<br /></span>
</p>
</div>
<div class="form-group">
@if (!Model.ShowQR)
{
<div class="form-group">
<form method="get">
<input type="hidden" asp-for="ShowQR" value="true" />
<button type="submit" class="btn btn-primary">Show Confidential QR Code</button>
</form>
</div>
}
else
{
<div class="form-group">
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.SparkLink)"></div>
</div>
}
</div>
</div>
</div>
@section Scripts {
@await Html.PartialAsync("_ValidationScriptsPartial")
@if (Model.ShowQR)
{
<script type="text/javascript" src="~/js/qrcode.min.js"></script>
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.SparkLink)",
width: 150,
height: 150
});
</script>
}
}

View File

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

View File

@@ -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": "लेनदेनों"
}

View File

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

View File

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

View File

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

View File

@@ -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 ? '<img class="card-img-top" src="' + this.escape(image) + '" alt="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': '<option value="true"' + (custom ? ' selected' : '') + '>Yes</option><option value="false"' + (!custom ? ' selected' : '') + '>No</option>'
});
$('#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, '&amp;') /* This MUST be the 1st replacement. */
.replace(/'/g, '&apos;') /* The 4 other predefined entities, required. */
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
;
}