Compare commits

...

22 Commits

Author SHA1 Message Date
bab7bf6633 bump 2018-11-27 15:17:32 +09:00
1831692761 Enable shopping cart, add items to cart, enable tips (#410)
Modal cart, remove items, checkout

Fix removal and adding of cart items

Improve cart UI

Add cart bundle, remove unused js files from the view when cart isn't used

Do not enable cart by default

Do not put modal into the view when the cart is disabled

Escape js properties

Work with amounts as cents

Make animation speed look constant

Enable tips in the cart

Fix cart UI
2018-11-27 15:14:32 +09:00
e144d2479b Add POS Data to Invoice UI (#409)
* Add POS Data in Invoice UI

* fix build

* extract in helper and add UTs

* add in unit test coverage through mvc view too
2018-11-27 15:13:09 +09:00
c25831316e bump nbx 2018-11-26 12:02:50 +09:00
60b72aabe8 fix test 2018-11-24 13:38:23 +09:00
c8fcb0ab18 Use framework dependent build for ARM 2018-11-23 16:14:13 +09:00
9911d18390 Do not push latest images to dockerhub 2018-11-23 14:12:47 +09:00
e24630ac1e Remove qemu install requirement for the host 2018-11-23 14:08:14 +09:00
4c1fd3edae More comment on ARM build 2018-11-23 14:02:44 +09:00
f65492dd66 Use stretch slim for arm 2018-11-23 14:00:33 +09:00
5d978c7670 Use manifest image for building arm images 2018-11-23 13:58:21 +09:00
11788cece9 No need to create latest tag 2018-11-23 13:18:35 +09:00
1aaa55dc62 Make test less flaky 2018-11-23 13:09:30 +09:00
ce57a2b8fb Do not tag latest 2018-11-23 12:59:48 +09:00
0604cc5bd0 bump 2018-11-23 11:37:05 +09:00
3d2c0bcc6c Use specific sdk and runtime version for arm 2018-11-23 11:23:27 +09:00
0f222979a6 CircleCI multiarch Docker images (#416)
* Preparing final version of CircleCI docker building

* Removing test job requirement for building Docker images

We'll already monitor build before tagging, would be too many checks

* Adding pushing of manifest for tag

* Easy access to docker/circle config files for edit

* Generalizing script with $DOCKERHUB_REPO variable
2018-11-23 11:21:01 +09:00
a1eb6a14f5 Fix all script because of docker-compose team screwing up (https://github.com/docker/compose/issues/6316) 2018-11-22 16:16:10 +09:00
186ce01022 add pairing code to tokens page after authorize (#412) 2018-11-22 15:13:35 +09:00
0096ec1d12 bump nbxplorer 2018-11-21 20:41:51 +09:00
2929d7bf51 Fixing MONA_BTC rate breaking tests and CircleCI because zaif is down 2018-11-20 15:42:45 -06:00
d90fb5764d Add noindex,nofollow to invoices, checkouts and fix create invoice ui bug (#407)
* add noindex,nofollow on invoices

* fix create invoice button and add noindex,nofollow to checkout and invoice pages
2018-11-19 13:20:48 +09:00
34 changed files with 868 additions and 174 deletions

View File

@ -5,6 +5,7 @@ jobs:
docker_layer_caching: true
steps:
- checkout
test:
machine: true
steps:
@ -23,8 +24,82 @@ jobs:
dotnet test --filter Fast=Fast
docker-compose up -d dev
dotnet test --filter Integration=Integration
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
publish_docker_linuxamd64:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
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
publish_docker_linuxarm:
machine:
docker_layer_caching: true
steps:
- checkout
- run:
command: |
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
#
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
publish_docker_multiarch:
machine:
enabled: true
image: circleci/classic:201808-01
steps:
- run:
command: |
# Turn on Experimental features
sudo mkdir $HOME/.docker
sudo sh -c 'echo "{ \"experimental\": \"enabled\" }" >> $HOME/.docker/config.json'
#
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
#
LATEST_TAG=${CIRCLE_TAG:1} #trim v from tag
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
workflows:
version: 2
build_and_test:
jobs:
- test
publish:
jobs:
- publish_docker_linuxamd64:
filters:
# ignore any commit on any branch by default
branches:
ignore: /.*/
# only act on version tags
tags:
only: /v[1-9]+(\.[0-9]+)*/
- publish_docker_linuxarm:
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/
- publish_docker_multiarch:
requires:
- publish_docker_linuxamd64
- publish_docker_linuxarm
filters:
branches:
ignore: /.*/
tags:
only: /v[1-9]+(\.[0-9]+)*/

View File

@ -43,6 +43,7 @@ using System.Security.Cryptography.X509Certificates;
using BTCPayServer.Lightning;
using BTCPayServer.Models.WalletViewModels;
using System.Security.Claims;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Security;
using NBXplorer.Models;
@ -79,7 +80,7 @@ namespace BTCPayServer.Tests
Assert.False(attribute.IsValid("http://"));
Assert.False(attribute.IsValid("httpdsadsa.com"));
}
[Fact]
[Trait("Fast", "Fast")]
public void CanCalculateCryptoDue2()
@ -811,7 +812,7 @@ namespace BTCPayServer.Tests
output.ScriptPubKey = invoiceAddress.ScriptPubKey;
using (var cts = new CancellationTokenSource(10000))
using (var listener = tester.ExplorerClient.CreateNotificationSession())
using (var listener = tester.ExplorerClient.CreateWebsocketNotificationSession())
{
listener.ListenAllDerivationSchemes();
var replaced = tester.ExplorerNode.SignRawTransaction(tx);
@ -1462,15 +1463,16 @@ namespace BTCPayServer.Tests
vmpos.Currency = "CAD";
vmpos.ButtonText = "{0} Purchase";
vmpos.CustomButtonText = "Nicolas Sexy Hair";
vmpos.CustomTipText = "Wanna tip?";
vmpos.Template = @"
apple:
price: 5.0
title: good apple
price: 5.0
title: good apple
orange:
price: 10.0
price: 10.0
donation:
price: 1.02
custom: true
price: 1.02
custom: true
";
Assert.IsType<RedirectToActionResult>(apps.UpdatePointOfSale(appId, vmpos).Result);
vmpos = Assert.IsType<UpdatePointOfSaleViewModel>(Assert.IsType<ViewResult>(apps.UpdatePointOfSale(appId).Result).Model);
@ -1486,7 +1488,8 @@ donation:
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
Assert.Equal("{0} Purchase", vmview.ButtonText);
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
Assert.IsType<RedirectResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
Assert.Equal("Wanna tip?", vmview.CustomTipText);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
//
var invoices = user.BitPay.GetInvoices();
@ -1496,14 +1499,90 @@ donation:
Assert.Equal("orange", orangeInvoice.ItemDesc);
// testing custom amount
Assert.IsType<RedirectResult>(publicApps.ViewPointOfSale(appId, 5, null, null, null, null, "donation").Result);
var action = Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 5, null, null, null, null, "donation").Result);
Assert.Equal(nameof(InvoiceController.Checkout), action.ActionName);
invoices = user.BitPay.GetInvoices();
var donationInvoice = invoices.First(); // expected behavior is that new invoice should now be first
Assert.Equal(5m, donationInvoice.Price);
var donationInvoice = invoices.Single(i => i.Price == 5m);
Assert.NotNull(donationInvoice);
Assert.Equal("CAD", donationInvoice.Currency);
Assert.Equal("donation", donationInvoice.ItemDesc);
}
}
[Fact]
[Trait("Fast", "Fast")]
public void PosDataParser_ParsesCorrectly()
{
var testCases =
new List<(string input, Dictionary<string, string> expectedOutput)>()
{
{ (null, new Dictionary<string, string>())},
{("", new Dictionary<string, string>())},
{("{}", new Dictionary<string, string>())},
{("non-json-content", new Dictionary<string, string>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, string>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, string>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, string>(){ {"key", "True"}})},
{("{ \"key\": \"value\", \"key2\": [\"value\", \"value2\"]}",
new Dictionary<string, string>(){ {"key", "value"}, {"key2", "value,value2"}})},
{("{ invalidjson file here}", new Dictionary<string, string>(){ {String.Empty, "{ invalidjson file here}"}})}
};
testCases.ForEach(tuple =>
{
Assert.Equal(tuple.expectedOutput, InvoiceController.PosDataParser.ParsePosData(tuple.input));
});
}
[Fact]
[Trait("Integration", "Integration")]
public async Task PosDataParser_ParsesCorrectly_Slower()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var controller = tester.PayTester.GetController<InvoiceController>(null);
var testCases =
new List<(string input, Dictionary<string, string> expectedOutput)>()
{
{ (null, new Dictionary<string, string>())},
{("", new Dictionary<string, string>())},
{("{}", new Dictionary<string, string>())},
{("non-json-content", new Dictionary<string, string>(){ {string.Empty, "non-json-content"}})},
{("[1,2,3]", new Dictionary<string, string>(){ {string.Empty, "[1,2,3]"}})},
{("{ \"key\": \"value\"}", new Dictionary<string, string>(){ {"key", "value"}})},
{("{ \"key\": true}", new Dictionary<string, string>(){ {"key", "True"}})},
{("{ \"key\": \"value\", \"key2\": [\"value\", \"value2\"]}",
new Dictionary<string, string>(){ {"key", "value"}, {"key2", "value,value2"}})},
{("{ invalidjson file here}", new Dictionary<string, string>(){ {String.Empty, "{ invalidjson file here}"}})}
};
var tasks = new List<Task>();
foreach (var valueTuple in testCases)
{
tasks.Add(user.BitPay.CreateInvoiceAsync(new Invoice(1, "BTC")
{
PosData = valueTuple.input
}).ContinueWith(async task =>
{
var result = await controller.Invoice(task.Result.Id);
var viewModel =
Assert.IsType<InvoiceDetailsModel>(
Assert.IsType<ViewResult>(result).Model);
Assert.Equal(valueTuple.expectedOutput, viewModel.PosData);
}));
}
await Task.WhenAll(tasks);
}
}
[Fact]
[Trait("Integration", "Integration")]

View File

@ -1 +1,2 @@
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" $args
$bitcoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)
docker exec -ti $bitcoind_container_id bitcoin-cli -datadir="/data" $args

View File

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_bitcoind_1 bitcoin-cli -datadir="/data" "$@"
bitcoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=bitcoind)"
docker exec -ti "$bitcoind_container_id" bitcoin-cli -datadir="/data" "$@"

View File

@ -69,7 +69,7 @@ services:
nbxplorer:
image: nicolasdorier/nbxplorer:1.1.0.18
image: nicolasdorier/nbxplorer:2.0.0.1
restart: unless-stopped
ports:
- "32838:32838"

View File

@ -1 +1,2 @@
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli $args
$customer_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)
docker exec -ti $customer_lightning_container_id lightning-cli $args

View File

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_customer_lightningd_1 lightning-cli "$@"
customer_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=customer_lightningd)"
docker exec -ti $customer_lightning_container_id lightning-cli "$@"

View File

@ -1 +1,2 @@
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" $args
$litecoind_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)
docker exec -ti $litecoind_container_id litecoin-cli -datadir="/data" $args

View File

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_litecoind_1 litecoin-cli -datadir="/data" "$@"
litecoind_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=litecoind)"
docker exec -ti "$litecoind_container_id" litecoin-cli -datadir="/data" "$@"

View File

@ -1 +1,2 @@
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli $args
$merchant_lightning_container_id=$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)
docker exec -ti $merchant_lightning_container_id lightning-cli $args

View File

@ -1,3 +1,4 @@
#!/bin/bash
docker exec -ti btcpayservertests_merchant_lightningd_1 lightning-cli "$@"
merchant_lightning_container_id="$(docker ps -q --filter label=com.docker.compose.project=btcpayservertests --filter label=com.docker.compose.service=merchant_lightningd)"
docker exec -ti $merchant_lightning_container_id lightning-cli "$@"

View File

@ -24,7 +24,7 @@ namespace BTCPayServer
DefaultRateRules = new[]
{
"MONA_X = MONA_BTC * BTC_X",
"MONA_BTC = zaif(MONA_BTC)"
"MONA_BTC = bittrex(MONA_BTC)"
},
CryptoImagePath = "imlegacy/monacoin.png",
LightningImagePath = "imlegacy/mona-lightning.svg",

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.16</Version>
<Version>1.0.3.18</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -49,7 +49,7 @@
<PackageReference Include="NBitcoin" Version="4.1.1.71" />
<PackageReference Include="NBitpayClient" Version="1.0.0.30" />
<PackageReference Include="DBreeze" Version="1.92.0" />
<PackageReference Include="NBXplorer.Client" Version="1.0.3.11" />
<PackageReference Include="NBXplorer.Client" Version="2.0.0" />
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.2" />
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.3" />
<PackageReference Include="NicolasDorier.RateLimits" Version="1.0.0.3" />

View File

@ -50,17 +50,21 @@ namespace BTCPayServer.Controllers
" description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" +
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
" custom: true";
EnableShoppingCart = false;
ShowCustomAmount = true;
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public const string BUTTON_TEXT_DEF = "Buy for {0}";
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
public const string CUSTOM_BUTTON_TEXT_DEF = "Pay";
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?";
public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF;
public string CustomCSSLink { get; set; }
}
@ -76,11 +80,13 @@ namespace BTCPayServer.Controllers
var vm = new UpdatePointOfSaleViewModel()
{
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
Currency = settings.Currency,
Template = settings.Template,
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomCSSLink = settings.CustomCSSLink
};
if (HttpContext?.Request != null)
@ -144,11 +150,13 @@ namespace BTCPayServer.Controllers
app.SetSettings(new PointOfSaleSettings()
{
Title = vm.Title,
EnableShoppingCart = vm.EnableShoppingCart,
ShowCustomAmount = vm.ShowCustomAmount,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template,
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomCSSLink = vm.CustomCSSLink
});
await UpdateAppSettings(app);

View File

@ -45,11 +45,13 @@ namespace BTCPayServer.Controllers
{
Title = settings.Title,
Step = step.ToString(CultureInfo.InvariantCulture),
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
CurrencySymbol = currency.Symbol,
Items = _AppsHelper.Parse(settings.Template, settings.Currency),
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
CustomCSSLink = settings.CustomCSSLink
});
}
@ -74,6 +76,10 @@ namespace BTCPayServer.Controllers
if (app == null)
return NotFound();
var settings = app.GetSettings<PointOfSaleSettings>();
if (string.IsNullOrEmpty(choiceKey) && !settings.EnableShoppingCart)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
}
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
{
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
@ -112,7 +118,7 @@ namespace BTCPayServer.Controllers
RedirectURL = redirectUrl,
FullNotifications = true
}, store, HttpContext.Request.GetAbsoluteRoot());
return Redirect(invoice.Data.Url);
return RedirectToAction(nameof(InvoiceController.Checkout), "Invoice", new { invoiceId = invoice.Data.Id });
}
}

View File

@ -14,13 +14,13 @@ using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitcoin;
using NBitpayClient;
using NBXplorer;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Controllers
{
@ -41,7 +41,6 @@ namespace BTCPayServer.Controllers
var dto = invoice.EntityToDTO(_NetworkProvider);
var store = await _StoreRepository.FindStore(invoice.StoreId);
InvoiceDetailsModel model = new InvoiceDetailsModel()
{
StoreName = store.StoreName,
@ -64,7 +63,8 @@ namespace BTCPayServer.Controllers
RedirectUrl = invoice.RedirectURL,
ProductInformation = invoice.ProductInformation,
StatusException = invoice.ExceptionStatus,
Events = invoice.Events
Events = invoice.Events,
PosData = PosDataParser.ParsePosData(dto.PosData)
};
foreach (var data in invoice.GetPaymentMethods(null))
@ -586,5 +586,43 @@ namespace BTCPayServer.Controllers
{
return _UserManager.GetUserId(User);
}
public class PosDataParser
{
public static Dictionary<string, string> ParsePosData(string posData)
{
var result = new Dictionary<string,string>();
if (string.IsNullOrEmpty(posData))
{
return result;
}
try
{
var jObject =JObject.Parse(posData);
foreach (var item in jObject)
{
switch (item.Value.Type)
{
case JTokenType.Array:
result.Add(item.Key, string.Join(',', item.Value.AsEnumerable()));
break;
default:
result.Add(item.Key, item.Value.ToString());
break;
}
}
}
catch
{
result.Add(string.Empty, posData);
}
return result;
}
}
}
}

View File

@ -789,7 +789,8 @@ namespace BTCPayServer.Controllers
StatusMessage = "Server initiated pairing code: " + pairingCode;
return RedirectToAction(nameof(ListTokens), new
{
storeId = store.Id
storeId = store.Id,
pairingCode = pairingCode
});
}
else

View File

@ -17,6 +17,8 @@ namespace BTCPayServer.Models.AppViewModels
[MaxLength(5000)]
public string Template { get; set; }
[Display(Name = "Enable shopping cart")]
public bool EnableShoppingCart { get; set; }
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
public string Example1 { get; internal set; }
@ -32,6 +34,10 @@ namespace BTCPayServer.Models.AppViewModels
[MaxLength(30)]
[Display(Name = "Text to display on buttons next to the input allowing the user to enter a custom amount")]
public string CustomButtonText { get; set; }
[Required]
[MaxLength(30)]
[Display(Name = "Do you want to leave a tip?")]
public string CustomTipText { get; set; }
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]

View File

@ -22,6 +22,7 @@ namespace BTCPayServer.Models.AppViewModels
public bool Custom { get; set; }
}
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public string Step { get; set; }
public string Title { get; set; }
@ -30,6 +31,7 @@ namespace BTCPayServer.Models.AppViewModels
public string ButtonText { get; set; }
public string CustomButtonText { get; set; }
public string CustomTipText { get; set; }
public string CustomCSSLink { get; set; }
}

View File

@ -143,5 +143,6 @@ namespace BTCPayServer.Models.InvoicingModels
public DateTimeOffset MonitoringDate { get; internal set; }
public List<Data.InvoiceEventData> Events { get; internal set; }
public string NotificationEmail { get; internal set; }
public Dictionary<string, string> PosData { get; set; }
}
}

View File

@ -51,7 +51,7 @@ namespace BTCPayServer.Payments.Bitcoin
}
CompositeDisposable leases = new CompositeDisposable();
ConcurrentDictionary<string, NotificationSession> _SessionsByCryptoCode = new ConcurrentDictionary<string, NotificationSession>();
ConcurrentDictionary<string, WebsocketNotificationSession> _SessionsByCryptoCode = new ConcurrentDictionary<string, WebsocketNotificationSession>();
private Timer _ListenPoller;
TimeSpan _PollInterval;
@ -114,7 +114,7 @@ namespace BTCPayServer.Payments.Bitcoin
return;
if (_Cts.IsCancellationRequested)
return;
var session = await client.CreateNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
var session = await client.CreateWebsocketNotificationSessionAsync(_Cts.Token).ConfigureAwait(false);
if (!_SessionsByCryptoCode.TryAdd(network.CryptoCode, session))
{
await session.DisposeAsync();
@ -187,7 +187,7 @@ namespace BTCPayServer.Payments.Bitcoin
if (cleanup)
{
Logs.PayServer.LogInformation($"Disconnected from WebSocket of NBXplorer ({network.CryptoCode})");
_SessionsByCryptoCode.TryRemove(network.CryptoCode, out NotificationSession unused);
_SessionsByCryptoCode.TryRemove(network.CryptoCode, out WebsocketNotificationSession unused);
if (_SessionsByCryptoCode.Count == 0 && _Cts.IsCancellationRequested)
{
_RunningTask.TrySetResult(true);

View File

@ -122,7 +122,7 @@ namespace BTCPayServer.Services.Wallets
UTXOChanges result = null;
try
{
result = await _Client.GetUTXOsAsync(strategy, null, false, cancellation).ConfigureAwait(false);
result = await _Client.GetUTXOsAsync(strategy, cancellation).ConfigureAwait(false);
}
catch
{
@ -153,7 +153,7 @@ namespace BTCPayServer.Services.Wallets
public Task<GetTransactionsResponse> FetchTransactions(DerivationStrategyBase derivationStrategyBase)
{
return _Client.GetTransactionsAsync(derivationStrategyBase, null, false);
return _Client.GetTransactionsAsync(derivationStrategyBase);
}
public Task<BroadcastResult[]> BroadcastTransactionsAsync(List<Transaction> transactions)

View File

@ -29,6 +29,10 @@
<input asp-for="Currency" class="form-control" />
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EnableShoppingCart"></label>
<input asp-for="EnableShoppingCart" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="ShowCustomAmount"></label>
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
@ -43,6 +47,11 @@
<input asp-for="CustomButtonText" class="form-control" />
<span asp-validation-for="CustomButtonText" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomTipText" class="control-label"></label>*
<input asp-for="CustomTipText" class="form-control" />
<span asp-validation-for="CustomTipText" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomCSSLink" class="control-label"></label>
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>

View File

@ -1,4 +1,5 @@
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
@{
@ -18,11 +19,59 @@
{
<link href="@Model.CustomCSSLink" rel="stylesheet" />
}
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
@if (Model.EnableShoppingCart)
{
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
</script>
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
}
</head>
<body class="h-100">
@if (Model.EnableShoppingCart)
{
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Shopping cart</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<table id="js-cart-list" class="table mt-2 mb-3">
<thead class="thead-dark">
<tr>
<th colspan="2">Product</th>
<th class="text-right" width="80">Quantity</th>
<th class="text-right" width="25%">Price</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<form method="post" asp-antiforgery="false" data-buy>
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
<button id="js-cart-pay" class="btn btn-primary" type="submit"><b>@Model.CustomButtonText</b></button>
</form>
</div>
</div>
</div>
</div>
}
<div class="container d-flex h-100">
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
<h1 class="mb-4">@Model.Title</h1>
@if (Model.EnableShoppingCart)
{
<a id="js-cart" class="btn btn-warning text-white text-right" href="#" data-toggle="modal" data-target="#cartModal"><i class="fa fa-shopping-basket"></i>&nbsp; <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
}
<div class="row">
@for (int i = 0; i < Model.Items.Length; i++)
{
@ -31,7 +80,7 @@
var image = item.Image;
var description = item.Description;
<div class="@className my-3 px-2">
<div class="card">
<div class="card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<img class="card-img-top" src="@image" alt="Card image cap">
@ -42,7 +91,7 @@
{
<p class="card-text">@description</p>
}
@if (item.Custom)
@if (item.Custom && !Model.EnableShoppingCart)
{
<form method="post" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id" />
@ -61,7 +110,7 @@
else
{
<form method="post" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="btn btn-primary" value="@item.Id">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
</form>
}
@ -94,7 +143,5 @@
}
</div>
</div>
<script src="~/vendor/jquery/jquery.js"></script>
<script src="~/vendor/bootstrap4/js/bootstrap.js"></script>
</body>
</html>

View File

@ -13,6 +13,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<META NAME="robots" CONTENT="noindex,nofollow">
<title>@Model.HtmlTitle</title>
<bundle name="wwwroot/bundles/checkout-bundle.min.css" />

View File

@ -2,6 +2,14 @@
@{
ViewData["Title"] = "Create an invoice";
}
<script>
$(function() {
$("#create-invoice-form").on("submit",
function() {
$(this).find("input[type='submit']").prop('disabled', true);
});
})
</script>
<section>
<div class="container">
<div class="row">
@ -12,7 +20,7 @@
</div>
<div class="row">
<div class="col-lg-12">
<form asp-action="CreateInvoice" method="post">
<form asp-action="CreateInvoice" method="post" id="create-invoice-form">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Amount" class="control-label"></label>*

View File

@ -2,6 +2,9 @@
@{
ViewData["Title"] = "Invoice " + Model.Id;
}
@section HeaderContent{
<META NAME="robots" CONTENT="noindex,nofollow">
}
<style type="text/css">
.linethrough {
@ -39,128 +42,177 @@
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Information</h3>
<table class="table table-sm table-responsive-md">
<tr>
<th>Store</th>
<td><a href="@Model.StoreLink">@Model.StoreName</a></td>
</tr>
<tr>
<th>Id</th>
<td>@Model.Id</td>
</tr>
<tr>
<th>Created date</th>
<td>@Model.CreatedDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Expiration date</th>
<td>@Model.ExpirationDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Monitoring date</th>
<td>@Model.MonitoringDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Transaction speed</th>
<td>@Model.TransactionSpeed</td>
</tr>
<tr>
<th>Status</th>
<td>@Model.Status</td>
</tr>
<tr>
<th>Status Exception</th>
<td>@Model.StatusException</td>
</tr>
<tr>
<th>Refund email</th>
<td><a href="mailto:@Model.RefundEmail">@Model.RefundEmail</a></td>
</tr>
<tr>
<th>Order Id</th>
<td>@Model.OrderId</td>
</tr>
<tr>
<th>Total fiat due</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Notification Email</th>
<td>@Model.NotificationEmail</td>
</tr>
<tr>
<th>Notification Url</th>
<td>@Model.NotificationUrl</td>
</tr>
<tr>
<th>Redirect Url</th>
<td><a href="@Model.RedirectUrl">@Model.RedirectUrl</a></td>
</tr>
</table>
</div>
<div class="col-md-6">
<h3>Buyer information</h3>
<table class="table table-sm table-responsive-md">
<tr>
<th>Name</th>
<td>@Model.BuyerInformation.BuyerName</td>
</tr>
<tr>
<th>Email</th>
<td><a href="mailto:@Model.BuyerInformation.BuyerEmail">@Model.BuyerInformation.BuyerEmail</a></td>
</tr>
<tr>
<th>Phone</th>
<td>@Model.BuyerInformation.BuyerPhone</td>
</tr>
<tr>
<th>Address 1</th>
<td>@Model.BuyerInformation.BuyerAddress1</td>
</tr>
<tr>
<th>Address 2</th>
<td>@Model.BuyerInformation.BuyerAddress2</td>
</tr>
<tr>
<th>City</th>
<td>@Model.BuyerInformation.BuyerCity</td>
</tr>
<tr>
<th>State</th>
<td>@Model.BuyerInformation.BuyerState</td>
</tr>
<tr>
<th>Country</th>
<td>@Model.BuyerInformation.BuyerCountry</td>
</tr>
<tr>
<th>Zip</th>
<td>@Model.BuyerInformation.BuyerZip</td>
</tr>
</table>
<h3>Product information</h3>
<table class="table table-sm table-responsive-md">
<tr>
<th>Item code</th>
<td>@Model.ProductInformation.ItemCode</td>
</tr>
<tr>
<th>Item Description</th>
<td>@Model.ProductInformation.ItemDesc</td>
</tr>
<tr>
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
</tr>
</table>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h3>Information</h3>
<h3>Pos Data</h3>
<table class="table table-sm table-responsive-md">
<tr>
<th>Store</th>
<td><a href="@Model.StoreLink">@Model.StoreName</a></td>
</tr>
<tr>
<th>Id</th>
<td>@Model.Id</td>
</tr>
<tr>
<th>Created date</th>
<td>@Model.CreatedDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Expiration date</th>
<td>@Model.ExpirationDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Monitoring date</th>
<td>@Model.MonitoringDate.ToBrowserDate()</td>
</tr>
<tr>
<th>Transaction speed</th>
<td>@Model.TransactionSpeed</td>
</tr>
<tr>
<th>Status</th>
<td>@Model.Status</td>
</tr>
<tr>
<th>Status Exception</th>
<td>@Model.StatusException</td>
</tr>
<tr>
<th>Refund email</th>
<td><a href="mailto:@Model.RefundEmail">@Model.RefundEmail</a></td>
</tr>
<tr>
<th>Order Id</th>
<td>@Model.OrderId</td>
</tr>
<tr>
<th>Total fiat due</th>
<td>@Model.Fiat</td>
</tr>
<tr>
<th>Notification Email</th>
<td>@Model.NotificationEmail</td>
</tr>
<tr>
<th>Notification Url</th>
<td>@Model.NotificationUrl</td>
</tr>
<tr>
<th>Redirect Url</th>
<td><a href="@Model.RedirectUrl">@Model.RedirectUrl</a></td>
</tr>
@foreach (var posDataItem in Model.PosData)
{
<tr>
@if (!string.IsNullOrEmpty(posDataItem.Key))
{
<th>@posDataItem.Key</th>
<td>@posDataItem.Value</td>
}
else
{
<td colspan="2">@posDataItem.Value</td>
}
</tr>
}
</table>
</div>
<div class="col-md-6">
<h3>Buyer information</h3>
<h3>Addresses</h3>
<table class="table table-sm table-responsive-md">
<thead class="thead-inverse">
<tr>
<th>Name</th>
<td>@Model.BuyerInformation.BuyerName</td>
</tr>
<tr>
<th>Email</th>
<td><a href="mailto:@Model.BuyerInformation.BuyerEmail">@Model.BuyerInformation.BuyerEmail</a></td>
</tr>
<tr>
<th>Phone</th>
<td>@Model.BuyerInformation.BuyerPhone</td>
</tr>
<tr>
<th>Address 1</th>
<td>@Model.BuyerInformation.BuyerAddress1</td>
</tr>
<tr>
<th>Address 2</th>
<td>@Model.BuyerInformation.BuyerAddress2</td>
</tr>
<tr>
<th>City</th>
<td>@Model.BuyerInformation.BuyerCity</td>
</tr>
<tr>
<th>State</th>
<td>@Model.BuyerInformation.BuyerState</td>
</tr>
<tr>
<th>Country</th>
<td>@Model.BuyerInformation.BuyerCountry</td>
</tr>
<tr>
<th>Zip</th>
<td>@Model.BuyerInformation.BuyerZip</td>
</tr>
</table>
<h3>Product information</h3>
<table class="table table-sm table-responsive-md">
<tr>
<th>Item code</th>
<td>@Model.ProductInformation.ItemCode</td>
</tr>
<tr>
<th>Item Description</th>
<td>@Model.ProductInformation.ItemDesc</td>
</tr>
<tr>
<th>Price</th>
<td>@Model.ProductInformation.Price @Model.ProductInformation.Currency</td>
<th class="firstCol">Payment method</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach (var address in Model.Addresses)
{
var current = address.Current ? "font-weight-bold" : "";
<tr>
<td>@address.PaymentMethod</td>
<td class="smMaxWidth text-truncate @current">@address.Destination</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="row">
<div class="col-md-12">
<h3>Paid summary</h3>
<table class="table table-sm table-responsive-md">
@ -255,29 +307,6 @@
</div>
</div>
}
<div class="row">
<div class="col-md-12">
<h3>Addresses</h3>
<table class="table table-sm table-responsive-md">
<thead class="thead-inverse">
<tr>
<th class="firstCol">Payment method</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach (var address in Model.Addresses)
{
var current = address.Current ? "font-weight-bold" : "";
<tr>
<td>@address.PaymentMethod</td>
<td class="smMaxWidth text-truncate @current">@address.Destination</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">

View File

@ -28,6 +28,7 @@
<bundle name="wwwroot/bundles/main-bundle.min.js" />
@RenderSection("HeadScripts", required: false)
@RenderSection("HeaderContent", false)
</head>
<body id="page-top">

View File

@ -52,5 +52,14 @@
"wwwroot/vendor/vex/js/vex.combined.min.js",
"wwwroot/checkout/**/*.js"
]
},
{
"outputFileName": "wwwroot/bundles/cart-bundle.min.js",
"inputFiles": [
"wwwroot/vendor/jquery/jquery.js",
"wwwroot/vendor/bootstrap4/js/bootstrap.js",
"wwwroot/cart/js/cart.js",
"wwwroot/cart/js/cart.jquery.js"
]
}
]

View File

@ -0,0 +1,57 @@
$.fn.addAnimate = function(completeCallback) {
var documentHeight = $(document).height(),
itemPos = $(this).offset(),
itemY = itemPos.top,
cartPos = $('#js-cart').find('.badge').position();
tempItem = '<span id="js-cart-temp-item" class="badge badge-primary text-white badge-pill " style="' +
'position: absolute;' +
'top: ' + itemPos.top + 'px;' +
'left: ' + (itemPos.left + 50) + 'px;">'+
'<i class="fa fa-shopping-basket"></i></span>';
// Make animation speed look constant regardless of how far the object is from the cart
var animationSpeed = (Math.log(itemY) * (documentHeight / Math.log2(documentHeight - itemY))) / 2;
// Add the cart item badge and animate it
$('body').after(tempItem);
$('#js-cart-temp-item').animate({
easing: 'swing',
top: cartPos.top,
left: cartPos.left
}, animationSpeed, function() {
$(this).remove();
completeCallback && completeCallback();
});
};
$(document).ready(function(){
var cart = new Cart();
$('.js-add-cart').click(function(event){
event.preventDefault();
var $btn = $(event.target),
id = $btn.closest('.card').data('id'),
item = srvModel.items[id];
// Animate adding and then add then save
$(this).addAnimate(function(){
cart.addItem({
id: id,
title: item.title,
price: item.price,
image: typeof item.image != 'underfined' ? item.image : null
});
});
});
// Destroy the cart when the "pay button is clicked"
$('#js-cart-pay').click(function(){
cart.destroy();
});
// Repopulate cart items in the modal when it opens
$('#cartModal').on('show.bs.modal', function () {
cart.listItems();
});
});

View File

@ -0,0 +1,282 @@
function Cart() {
this.items = 0;
this.totalAmount = 0;
this.content = [];
this.tip = 0;
this.loadLocalStorage();
this.itemsCount();
this.listItems();
this.updateAmount();
}
Cart.prototype.addItem = function(item) {
// Increment the existing item count
var result = this.content.filter(function(obj){
if (obj.id === item.id){
obj.count++;
}
return obj.id === item.id
});
// Add new item because it doesn't exist yet
if (!result.length) {
this.content.push({id: item.id, title: item.title, price: item.price, count: 1, image: item.image})
}
this.items++;
this.saveLocalStorage();
this.itemsCount();
this.updateTotal();
this.updateAmount();
}
Cart.prototype.decrementItem = function(id) {
var self = this;
// Decrement the existing item count
this.content.filter(function(obj, index, arr){
if (obj.id === id)
{
obj.count--;
// It's the last item with the same ID, remove it
if (obj.count === 0) {
self.removeItem(id, index, arr);
}
}
});
this.items--;
this.saveLocalStorage();
this.itemsCount();
this.updateTotal();
this.updateAmount();
if (this.items === 0) {
this.emptyList();
}
}
Cart.prototype.removeItemAll = function(id) {
var self = this;
this.content.filter(function(obj, index, arr){
if (obj.id === id)
{
self.removeItem(id, index, arr);
for (var i = 0; i < obj.count; i++) {
self.items--;
}
}
});
this.saveLocalStorage();
this.itemsCount();
this.updateTotal();
this.updateAmount();
if (this.items === 0) {
this.emptyList();
}
}
Cart.prototype.removeItem = function(id, index, arr) {
// Remove from the array
arr.splice(index, 1);
// Remove from the DOM
$('#js-cart-list').find('tr').eq(index+1).remove();
}
Cart.prototype.setTip = function(tip) {
return this.tip = tip;
}
Cart.prototype.itemsCount = function() {
$('#js-cart-items').text(this.items);
}
Cart.prototype.getTotal = function(plain) {
this.totalAmount = 0;
// Always calculate the total amount based on the cart content
for (var key in this.content) {
if (this.content.hasOwnProperty(key) && typeof this.content[key] != 'undefined') {
var price = this.toCents(this.content[key].price.value);
this.totalAmount += (this.content[key].count * price);
}
}
this.totalAmount += this.toCents(this.tip);
return this.fromCents(this.totalAmount);
}
Cart.prototype.updateTotal = function() {
$('#js-cart-total').text(this.formatCurrency(this.getTotal()));
}
Cart.prototype.updateAmount = function() {
$('#js-cart-amount').val(this.getTotal());
}
Cart.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;')
;
}
Cart.prototype.listItems = function() {
var $table = $('#js-cart-list').find('tbody'),
self = this,
list = []
tableTemplate = '';
if (this.content.length > 0) {
// Prepare the list of items in the cart
for (var key in this.content) {
var item = this.content[key],
id = this.escape(item.id),
title = this.escape(item.title),
image = this.escape(item.image),
count = this.escape(item.count),
price = this.escape(item.price.formatted),
currencySymbol = this.escape(srvModel.currencySymbol),
step = this.escape(srvModel.step),
customTipText = this.escape(srvModel.customTipText);
tableTemplate = '<tr data-id="' + id + '">' +
(image !== null ? '<td class="align-middle pr-0" width="60"><img src="' + image + '" width="100%"></td>' : '') +
'<td class="align-middle pr-0"><b>' + title + '</b></td>' +
'<td class="align-middle pr-0" align="right"><div class="input-group">' +
' <input class="js-cart-item-count form-control form-control-sm pull-left" type="number" min="0" step="1" name="count" placeholder="Qty" value="' + count + '" data-prev="' + count + '">' +
' <div class="input-group-append"><a class="js-cart-item-remove btn btn-danger btn-sm" href="#"><i class="fa fa-remove"></i></a></div>' +
'</div></td>' +
'<td class="align-middle" align="right">' + price + '</td>' +
'</tr>';
list.push($(tableTemplate));
}
tableTemplate = '<tr><td colspan="4"><div class="row"><div class="col-sm-8 py-2">' + customTipText + '</div><div class="col-sm-4">' +
'<div class="input-group">' +
'<div class="input-group-prepend">' +
'<span class="input-group-text">' + currencySymbol + '</span>' +
'</div>' +
'<input class="js-cart-tip form-control" type="number" min="0" step="' + step + '" name="tip" placeholder="Amount">' +
'</div>' +
'</div></div></td></tr>';
list.push($(tableTemplate));
tableTemplate = '<tr class="bg-light h4"><td colspan="2">Total</td><td colspan="2" align="right"><span id="js-cart-total">' + this.formatCurrency(this.getTotal()) + '</span></td></tr>';
list.push($(tableTemplate));
// Add the list to DOM
$table.html(list);
// Update the cart when number of items is changed
$('.js-cart-item-count').off().on('input', function(event){
var _this = this,
id = $(this).closest('tr').data('id'),
count = parseInt($(this).val()),
prevCount = parseInt($(this).data('prev')),
increased = count > prevCount;
// User hasn't inputed any number so stop here
if (isNaN(count)) {
return false;
}
$(this).data('prev', count);
var item = self.content.filter(function(obj){
return obj.id === id
});
// Must be in the loop because user may change the count manually by more than 1
for (var i = 0; i < Math.abs(count - prevCount); i++) {
if (increased) {
self.addItem({
id: id,
title: item.title,
price: item.price,
image: typeof item.image != 'underfined' ? item.image : null
});
} else {
self.decrementItem(id);
}
}
});
// Remove item from the cart
$('.js-cart-item-remove').off().on('click', function(event){
event.preventDefault();
var id = $(this).closest('tr').data('id');
self.removeItemAll(id);
});
// Change total when tip is changed
$('.js-cart-tip').off().on('input', function(event){
self.setTip($(this).val());
self.updateTotal();
self.updateAmount();
});
} else { // No item in the cart
self.emptyList();
}
}
Cart.prototype.emptyList = function() {
var $table = $('#js-cart-list').find('tbody');
$table.html('<tr><td colspan="4">The cart is empty.</td></tr>');
}
/* Get the currency symbol from an existing amount and use it with the new amount*/
Cart.prototype.formatCurrency = function(amount, example) {
var regex = /([0-9.]+)/gm;
// Get the first item's formated price
if (typeof example == 'undefined' && typeof srvModel != 'undefined') {
example = srvModel.items[0].price.formatted;
}
return example.replace(regex, amount.toFixed(2));
}
Cart.prototype.toCents = function(num) {
return num * 100;
}
Cart.prototype.fromCents = function(num) {
return num / 100;
}
Cart.prototype.saveLocalStorage = function() {
localStorage.setItem('cart', JSON.stringify(this.content));
}
Cart.prototype.loadLocalStorage = function() {
this.content = $.parseJSON(localStorage.getItem('cart')) || [];
// Get number of cart items
for (var key in this.content) {
if (this.content.hasOwnProperty(key) && typeof this.content[key] != 'undefined' && this.content[key] != null) {
this.items += this.content[key].count;
}
}
}
Cart.prototype.destroy = function() {
localStorage.removeItem('cart');
this.content = [];
this.items = 0;
this.totalAmount = 0;
}

View File

@ -1,7 +1,6 @@
FROM microsoft/dotnet:2.1.500-sdk-alpine3.7 AS builder
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer.csproj
# Cache some dependencies
RUN dotnet restore
COPY BTCPayServer/. .
RUN dotnet publish --output /app/ --configuration Release

21
Dockerfile.linuxarm32v7 Normal file
View File

@ -0,0 +1,21 @@
# This is a manifest image, will pull the image with the same arch as the builder machine
FROM microsoft/dotnet:2.1.500-sdk AS builder
WORKDIR /source
COPY BTCPayServer/BTCPayServer.csproj BTCPayServer.csproj
RUN dotnet restore
COPY BTCPayServer/. .
RUN dotnet publish --output /app/ --configuration Release
# Force the builder machine to take make an arm runtime image. This is fine as long as the builder does not run any program
FROM microsoft/dotnet:2.1.6-aspnetcore-runtime-stretch-slim-arm32v7
ENV LC_ALL en_US.UTF-8
ENV LANG en_US.UTF-8
WORKDIR /datadir
WORKDIR /app
ENV BTCPAY_DATADIR=/datadir
VOLUME /datadir
COPY --from=builder "/app" .
ENTRYPOINT ["dotnet", "BTCPayServer.dll"]

View File

@ -7,6 +7,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer", "BTCPayServe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BTCPayServer.Tests", "BTCPayServer.Tests\BTCPayServer.Tests.csproj", "{B373F439-6E75-4A94-985D-10A0C7C500D0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{29290EC7-00E6-4C4B-96D9-4D7E9611DF28}"
ProjectSection(SolutionItems) = preProject
.circleci\config.yml = .circleci\config.yml
Dockerfile.linuxamd64 = Dockerfile.linuxamd64
Dockerfile.linuxarm32v7 = Dockerfile.linuxarm32v7
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU