Merge remote-tracking branch 'btcpayserver/master' into feature/crowdfund
@@ -1429,6 +1429,7 @@ namespace BTCPayServer.Tests
|
|||||||
vmpos.ButtonText = "{0} Purchase";
|
vmpos.ButtonText = "{0} Purchase";
|
||||||
vmpos.CustomButtonText = "Nicolas Sexy Hair";
|
vmpos.CustomButtonText = "Nicolas Sexy Hair";
|
||||||
vmpos.CustomTipText = "Wanna tip?";
|
vmpos.CustomTipText = "Wanna tip?";
|
||||||
|
vmpos.CustomTipPercentages = "15,18,20";
|
||||||
vmpos.Template = @"
|
vmpos.Template = @"
|
||||||
apple:
|
apple:
|
||||||
price: 5.0
|
price: 5.0
|
||||||
@@ -1454,6 +1455,7 @@ donation:
|
|||||||
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
||||||
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
||||||
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||||
|
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -1598,7 +1600,7 @@ donation:
|
|||||||
|
|
||||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||||
{
|
{
|
||||||
Price = 500,
|
Price = 10,
|
||||||
Currency = "USD",
|
Currency = "USD",
|
||||||
PosData = "posData",
|
PosData = "posData",
|
||||||
OrderId = "orderId",
|
OrderId = "orderId",
|
||||||
@@ -1606,6 +1608,8 @@ donation:
|
|||||||
FullNotifications = true
|
FullNotifications = true
|
||||||
}, Facade.Merchant);
|
}, Facade.Merchant);
|
||||||
|
|
||||||
|
var networkFee = Money.Satoshis(10000);
|
||||||
|
|
||||||
// ensure 0 invoices exported because there are no payments yet
|
// ensure 0 invoices exported because there are no payments yet
|
||||||
var jsonResult = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
|
var jsonResult = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
|
||||||
var result = Assert.IsType<ContentResult>(jsonResult);
|
var result = Assert.IsType<ContentResult>(jsonResult);
|
||||||
@@ -1614,46 +1618,42 @@ donation:
|
|||||||
|
|
||||||
var cashCow = tester.ExplorerNode;
|
var cashCow = tester.ExplorerNode;
|
||||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Satoshis(10);
|
//
|
||||||
|
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3*networkFee;
|
||||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||||
|
Thread.Sleep(1000); // prevent race conditions, ordering payments
|
||||||
|
// look if you can reduce thread sleep, this was min value for me
|
||||||
|
|
||||||
|
// should reduce invoice due by 0 USD because payment = network fee
|
||||||
|
cashCow.SendToAddress(invoiceAddress, networkFee);
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
|
||||||
|
// pay remaining amount
|
||||||
|
cashCow.SendToAddress(invoiceAddress, 4*networkFee);
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
|
||||||
Eventually(() =>
|
Eventually(() =>
|
||||||
{
|
{
|
||||||
var jsonResultPaid = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
|
var jsonResultPaid = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
|
||||||
var paidresult = Assert.IsType<ContentResult>(jsonResultPaid);
|
var paidresult = Assert.IsType<ContentResult>(jsonResultPaid);
|
||||||
Assert.Equal("application/json", paidresult.ContentType);
|
Assert.Equal("application/json", paidresult.ContentType);
|
||||||
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", paidresult.Content);
|
|
||||||
Assert.Contains("\"InvoicePrice\": 500.0", paidresult.Content);
|
|
||||||
Assert.Contains("\"ConversionRate\": 5000.0", paidresult.Content);
|
|
||||||
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", paidresult.Content);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
var parsedJson = JsonConvert.DeserializeObject<object[]>(paidresult.Content);
|
||||||
[
|
Assert.Equal(3, parsedJson.Length);
|
||||||
{
|
|
||||||
"ReceivedDate": "2018-11-30T10:27:13Z",
|
var pay1str = parsedJson[0].ToString();
|
||||||
"StoreId": "FKaSZrXLJ2tcLfCyeiYYfmZp1UM5nZ1LDecQqbwBRuHi",
|
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
|
||||||
"OrderId": "orderId",
|
Assert.Contains("\"InvoiceDue\": 1.5", pay1str);
|
||||||
"InvoiceId": "4XUkgPMaTBzwJGV9P84kPC",
|
Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
|
||||||
"CreatedDate": "2018-11-30T10:27:06Z",
|
Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
|
||||||
"ExpirationDate": "2018-11-30T10:42:06Z",
|
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
|
||||||
"MonitoringDate": "2018-11-30T11:42:06Z",
|
|
||||||
"PaymentId": "6e5755c3357b20fd66f5fc478778d81371eab341e7112ab66ed6122c0ec0d9e5-1",
|
var pay2str = parsedJson[1].ToString();
|
||||||
"CryptoCode": "BTC",
|
Assert.Contains("\"InvoiceDue\": 1.5", pay2str);
|
||||||
"Destination": "mhhSEQuoM993o6vwnBeufJ4TaWov2ZUsPQ",
|
|
||||||
"PaymentType": "OnChain",
|
var pay3str = parsedJson[2].ToString();
|
||||||
"PaymentDue": "0.10020000 BTC",
|
Assert.Contains("\"InvoiceDue\": 0", pay3str);
|
||||||
"PaymentPaid": "0.10009990 BTC",
|
});
|
||||||
"PaymentOverpaid": "0.00000000 BTC",
|
|
||||||
"ConversionRate": 5000.0,
|
|
||||||
"FiatPrice": 500.0,
|
|
||||||
"FiatCurrency": "USD",
|
|
||||||
"ItemCode": null,
|
|
||||||
"ItemDesc": "Some \", description",
|
|
||||||
"Status": "new"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1689,8 +1689,8 @@ donation:
|
|||||||
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
|
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
|
||||||
Assert.Equal("application/csv", paidresult.ContentType);
|
Assert.Equal("application/csv", paidresult.ContentType);
|
||||||
Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content);
|
Assert.Contains($",\"orderId\",\"{invoice.Id}\",", paidresult.Content);
|
||||||
Assert.Contains($",\"OnChain\",\"0.1000999\",\"BTC\",\"5000.0\",\"500.0\"", paidresult.Content);
|
Assert.Contains($",\"OnChain\",\"BTC\",\"0.1000999\",\"0.0001\",\"5000.0\"", paidresult.Content);
|
||||||
Assert.Contains($",\"USD\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content);
|
Assert.Contains($",\"USD\",\"0.00050000\",\"500.0\",\"\",\"Some ``, description\",\"new (paidPartial)\"", paidresult.Content);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
<Version>1.0.3.34</Version>
|
<Version>1.0.3.36</Version>
|
||||||
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using System.Text;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Models.AppViewModels;
|
using BTCPayServer.Models.AppViewModels;
|
||||||
@@ -65,6 +68,9 @@ namespace BTCPayServer.Controllers
|
|||||||
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
|
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
|
||||||
public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?";
|
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 CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF;
|
||||||
|
public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = new int[] { 15, 18, 20 };
|
||||||
|
public int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF;
|
||||||
|
|
||||||
|
|
||||||
public string CustomCSSLink { get; set; }
|
public string CustomCSSLink { get; set; }
|
||||||
}
|
}
|
||||||
@@ -87,6 +93,7 @@ namespace BTCPayServer.Controllers
|
|||||||
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||||
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||||
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||||
|
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
|
||||||
CustomCSSLink = settings.CustomCSSLink
|
CustomCSSLink = settings.CustomCSSLink
|
||||||
};
|
};
|
||||||
if (HttpContext?.Request != null)
|
if (HttpContext?.Request != null)
|
||||||
@@ -157,6 +164,7 @@ namespace BTCPayServer.Controllers
|
|||||||
ButtonText = vm.ButtonText,
|
ButtonText = vm.ButtonText,
|
||||||
CustomButtonText = vm.CustomButtonText,
|
CustomButtonText = vm.CustomButtonText,
|
||||||
CustomTipText = vm.CustomTipText,
|
CustomTipText = vm.CustomTipText,
|
||||||
|
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
|
||||||
CustomCSSLink = vm.CustomCSSLink
|
CustomCSSLink = vm.CustomCSSLink
|
||||||
});
|
});
|
||||||
await UpdateAppSettings(app);
|
await UpdateAppSettings(app);
|
||||||
@@ -174,5 +182,21 @@ namespace BTCPayServer.Controllers
|
|||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int[] ListSplit(string list, string separator = ",")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(list))
|
||||||
|
{
|
||||||
|
return Array.Empty<int>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Remove all characters except numeric and comma
|
||||||
|
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
|
||||||
|
list = charsToDestroy.Replace(list, "");
|
||||||
|
|
||||||
|
return list.Split(separator, System.StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ namespace BTCPayServer.Controllers
|
|||||||
ButtonText = settings.ButtonText,
|
ButtonText = settings.ButtonText,
|
||||||
CustomButtonText = settings.CustomButtonText,
|
CustomButtonText = settings.CustomButtonText,
|
||||||
CustomTipText = settings.CustomTipText,
|
CustomTipText = settings.CustomTipText,
|
||||||
CustomCSSLink = settings.CustomCSSLink
|
CustomTipPercentages = settings.CustomTipPercentages,
|
||||||
|
CustomCSSLink = settings.CustomCSSLink,
|
||||||
|
AppId = appId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers
|
|||||||
string storeId,
|
string storeId,
|
||||||
string cryptoCode,
|
string cryptoCode,
|
||||||
string command,
|
string command,
|
||||||
int account = 0)
|
string keyPath = "")
|
||||||
{
|
{
|
||||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
@@ -67,7 +67,10 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
if (command == "getxpub")
|
if (command == "getxpub")
|
||||||
{
|
{
|
||||||
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
|
var k = KeyPath.Parse(keyPath);
|
||||||
|
if (k.Indexes.Length == 0)
|
||||||
|
throw new FormatException("Invalid key path");
|
||||||
|
var getxpubResult = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
|
||||||
result = getxpubResult;
|
result = getxpubResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,6 +175,7 @@ namespace BTCPayServer.Controllers
|
|||||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||||
|
storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath));
|
||||||
store.SetStoreBlob(storeBlob);
|
store.SetStoreBlob(storeBlob);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -410,8 +410,8 @@ namespace BTCPayServer.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
|
|
||||||
var cryptoCode = walletId.CryptoCode;
|
var cryptoCode = walletId.CryptoCode;
|
||||||
var storeBlob = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||||
var derivationScheme = GetPaymentMethod(walletId, storeBlob).DerivationStrategyBase;
|
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
|
||||||
|
|
||||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||||
|
|
||||||
@@ -476,15 +476,6 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
catch { throw new FormatException("Invalid value for subtract fees"); }
|
catch { throw new FormatException("Invalid value for subtract fees"); }
|
||||||
}
|
}
|
||||||
if (command == "getinfo")
|
|
||||||
{
|
|
||||||
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
|
||||||
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
|
|
||||||
{
|
|
||||||
throw new Exception($"This store is not configured to use this ledger");
|
|
||||||
}
|
|
||||||
result = new GetInfoResult();
|
|
||||||
}
|
|
||||||
if (command == "test")
|
if (command == "test")
|
||||||
{
|
{
|
||||||
result = await hw.Test(normalOperationTimeout.Token);
|
result = await hw.Test(normalOperationTimeout.Token);
|
||||||
@@ -514,10 +505,20 @@ namespace BTCPayServer.Controllers
|
|||||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||||
}
|
}
|
||||||
|
|
||||||
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
|
var storeBlob = storeData.GetStoreBlob();
|
||||||
if (foundKeyPath == null)
|
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||||
|
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
|
||||||
|
// Some deployment have the wallet root key path saved in the store blob
|
||||||
|
// If it does, we only have to make 1 call to the hw to check if it can sign the given strategy,
|
||||||
|
if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token))
|
||||||
{
|
{
|
||||||
|
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
||||||
|
foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
|
||||||
|
if (foundKeyPath == null)
|
||||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||||
|
storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath);
|
||||||
|
storeData.SetStoreBlob(storeBlob);
|
||||||
|
await Repository.UpdateStore(storeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||||
|
|||||||
@@ -366,6 +366,23 @@ namespace BTCPayServer.Data
|
|||||||
|
|
||||||
[Obsolete("Use GetExcludedPaymentMethods instead")]
|
[Obsolete("Use GetExcludedPaymentMethods instead")]
|
||||||
public string[] ExcludedPaymentMethods { get; set; }
|
public string[] ExcludedPaymentMethods { get; set; }
|
||||||
|
#pragma warning disable CS0618 // Type or member is obsolete
|
||||||
|
public void SetWalletKeyPathRoot(PaymentMethodId paymentMethodId, KeyPath keyPath)
|
||||||
|
{
|
||||||
|
if (keyPath == null)
|
||||||
|
WalletKeyPathRoots.Remove(paymentMethodId.ToString());
|
||||||
|
else
|
||||||
|
WalletKeyPathRoots.AddOrReplace(paymentMethodId.ToString().ToLowerInvariant(), keyPath.ToString());
|
||||||
|
}
|
||||||
|
public KeyPath GetWalletKeyPathRoot(PaymentMethodId paymentMethodId)
|
||||||
|
{
|
||||||
|
if (WalletKeyPathRoots.TryGetValue(paymentMethodId.ToString().ToLowerInvariant(), out var k))
|
||||||
|
return KeyPath.Parse(k);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
#pragma warning restore CS0618 // Type or member is obsolete
|
||||||
|
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
|
||||||
|
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
|
||||||
|
|
||||||
public IPaymentFilter GetExcludedPaymentMethods()
|
public IPaymentFilter GetExcludedPaymentMethods()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,8 +32,11 @@ namespace BTCPayServer.Models.AppViewModels
|
|||||||
public string CustomButtonText { get; set; }
|
public string CustomButtonText { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
[MaxLength(30)]
|
[MaxLength(30)]
|
||||||
[Display(Name = "Do you want to leave a tip?")]
|
[Display(Name = "Text to display in the tip input")]
|
||||||
public string CustomTipText { get; set; }
|
public string CustomTipText { get; set; }
|
||||||
|
[MaxLength(30)]
|
||||||
|
[Display(Name = "Tip percentage amounts (comma separated)")]
|
||||||
|
public string CustomTipPercentages { get; set; }
|
||||||
|
|
||||||
[MaxLength(500)]
|
[MaxLength(500)]
|
||||||
[Display(Name = "Custom bootstrap CSS file")]
|
[Display(Name = "Custom bootstrap CSS file")]
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ namespace BTCPayServer.Models.AppViewModels
|
|||||||
public string ButtonText { get; set; }
|
public string ButtonText { get; set; }
|
||||||
public string CustomButtonText { get; set; }
|
public string CustomButtonText { get; set; }
|
||||||
public string CustomTipText { get; set; }
|
public string CustomTipText { get; set; }
|
||||||
|
public int[] CustomTipPercentages { get; set; }
|
||||||
|
|
||||||
public string CustomCSSLink { get; set; }
|
public string CustomCSSLink { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
} = new List<(string KeyPath, string Address)>();
|
} = new List<(string KeyPath, string Address)>();
|
||||||
|
|
||||||
public string CryptoCode { get; set; }
|
public string CryptoCode { get; set; }
|
||||||
|
public string KeyPath { get; set; }
|
||||||
[Display(Name = "Hint address")]
|
[Display(Name = "Hint address")]
|
||||||
public string HintAddress { get; set; }
|
public string HintAddress { get; set; }
|
||||||
public bool Confirmation { get; set; }
|
public bool Confirmation { get; set; }
|
||||||
|
|||||||
@@ -89,20 +89,19 @@ namespace BTCPayServer.Services
|
|||||||
return new LedgerTestResult() { Success = true };
|
return new LedgerTestResult() { Success = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, int account, CancellationToken cancellation)
|
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
if (network == null)
|
if (network == null)
|
||||||
throw new ArgumentNullException(nameof(network));
|
throw new ArgumentNullException(nameof(network));
|
||||||
|
|
||||||
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
|
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
|
||||||
var path = network.GetRootKeyPath().Derive(account, true);
|
var pubkey = await GetExtPubKey(Ledger, network, keyPath, false, cancellation);
|
||||||
var pubkey = await GetExtPubKey(Ledger, network, path, false, cancellation);
|
|
||||||
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
|
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
|
||||||
{
|
{
|
||||||
P2SH = segwit,
|
P2SH = segwit,
|
||||||
Legacy = !segwit
|
Legacy = !segwit
|
||||||
});
|
});
|
||||||
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
|
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = keyPath };
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
|
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
|
||||||
@@ -129,7 +128,13 @@ namespace BTCPayServer.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
|
public async Task<bool> CanSign(BTCPayNetwork network, DirectDerivationStrategy strategy, KeyPath keyPath, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
var hwKey = await GetExtPubKey(Ledger, network, keyPath, true, cancellation);
|
||||||
|
return hwKey.ExtPubKey.PubKey == strategy.Root.PubKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KeyPath> FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
|
||||||
{
|
{
|
||||||
List<KeyPath> derivations = new List<KeyPath>();
|
List<KeyPath> derivations = new List<KeyPath>();
|
||||||
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
|
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||||
@@ -164,7 +169,17 @@ namespace BTCPayServer.Services
|
|||||||
KeyPath changeKeyPath,
|
KeyPath changeKeyPath,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
try
|
||||||
|
{
|
||||||
|
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
||||||
|
if (signedTransaction == null)
|
||||||
|
throw new Exception("The ledger failed to sign the transaction");
|
||||||
|
return signedTransaction;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception("The ledger failed to sign the transaction", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ namespace BTCPayServer.Services.Invoices.Export
|
|||||||
private IEnumerable<ExportInvoiceHolder> convertFromDb(InvoiceEntity invoice)
|
private IEnumerable<ExportInvoiceHolder> convertFromDb(InvoiceEntity invoice)
|
||||||
{
|
{
|
||||||
var exportList = new List<ExportInvoiceHolder>();
|
var exportList = new List<ExportInvoiceHolder>();
|
||||||
|
|
||||||
|
var invoiceDue = invoice.ProductInformation.Price;
|
||||||
// in this first version we are only exporting invoices that were paid
|
// in this first version we are only exporting invoices that were paid
|
||||||
foreach (var payment in invoice.GetPayments())
|
foreach (var payment in invoice.GetPayments())
|
||||||
{
|
{
|
||||||
@@ -63,6 +65,9 @@ namespace BTCPayServer.Services.Invoices.Export
|
|||||||
var pdata = payment.GetCryptoPaymentData();
|
var pdata = payment.GetCryptoPaymentData();
|
||||||
|
|
||||||
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks);
|
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks);
|
||||||
|
var paymentFee = pmethod.GetPaymentMethodDetails().GetTxFee();
|
||||||
|
var paidAfterNetworkFees = pdata.GetValue() - paymentFee;
|
||||||
|
invoiceDue -= paidAfterNetworkFees * pmethod.Rate;
|
||||||
|
|
||||||
var target = new ExportInvoiceHolder
|
var target = new ExportInvoiceHolder
|
||||||
{
|
{
|
||||||
@@ -73,6 +78,13 @@ namespace BTCPayServer.Services.Invoices.Export
|
|||||||
PaymentType = payment.GetPaymentMethodId().PaymentType == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain",
|
PaymentType = payment.GetPaymentMethodId().PaymentType == Payments.PaymentTypes.BTCLike ? "OnChain" : "OffChain",
|
||||||
Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork(cryptoCode)),
|
Destination = payment.GetCryptoPaymentData().GetDestination(Networks.GetNetwork(cryptoCode)),
|
||||||
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture),
|
Paid = pdata.GetValue().ToString(CultureInfo.InvariantCulture),
|
||||||
|
PaidCurrency = (pdata.GetValue() * pmethod.Rate).ToString(CultureInfo.InvariantCulture),
|
||||||
|
// Adding NetworkFee because Paid doesn't take into account network fees
|
||||||
|
// so if fee is 10000 satoshis, customer can essentially send infinite number of tx
|
||||||
|
// and merchant effectivelly would receive 0 BTC, invoice won't be paid
|
||||||
|
// while looking just at export you could sum Paid and assume merchant "received payments"
|
||||||
|
NetworkFee = paymentFee.ToString(CultureInfo.InvariantCulture),
|
||||||
|
InvoiceDue = invoiceDue,
|
||||||
OrderId = invoice.OrderId,
|
OrderId = invoice.OrderId,
|
||||||
StoreId = invoice.StoreId,
|
StoreId = invoice.StoreId,
|
||||||
InvoiceId = invoice.Id,
|
InvoiceId = invoice.Id,
|
||||||
@@ -112,12 +124,14 @@ namespace BTCPayServer.Services.Invoices.Export
|
|||||||
public string PaymentId { get; set; }
|
public string PaymentId { get; set; }
|
||||||
public string Destination { get; set; }
|
public string Destination { get; set; }
|
||||||
public string PaymentType { get; set; }
|
public string PaymentType { get; set; }
|
||||||
public string Paid { get; set; }
|
|
||||||
public string CryptoCode { get; set; }
|
public string CryptoCode { get; set; }
|
||||||
|
public string Paid { get; set; }
|
||||||
|
public string NetworkFee { get; set; }
|
||||||
public decimal ConversionRate { get; set; }
|
public decimal ConversionRate { get; set; }
|
||||||
|
public string PaidCurrency { get; set; }
|
||||||
public decimal InvoicePrice { get; set; }
|
|
||||||
public string InvoiceCurrency { get; set; }
|
public string InvoiceCurrency { get; set; }
|
||||||
|
public decimal InvoiceDue { get; set; }
|
||||||
|
public decimal InvoicePrice { get; set; }
|
||||||
public string InvoiceItemCode { get; set; }
|
public string InvoiceItemCode { get; set; }
|
||||||
public string InvoiceItemDesc { get; set; }
|
public string InvoiceItemDesc { get; set; }
|
||||||
public string InvoiceFullStatus { get; set; }
|
public string InvoiceFullStatus { get; set; }
|
||||||
|
|||||||
@@ -681,7 +681,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Money NetworkFee { get; set; }
|
public Money NetworkFee { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimum required to be paid in order to accept invocie as paid
|
/// Minimum required to be paid in order to accept invoice as paid
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Money MinimumTotalDue { get; set; }
|
public Money MinimumTotalDue { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath)
|
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath)
|
||||||
{
|
{
|
||||||
int retryCount = 0;
|
int retryCount = 0;
|
||||||
retry:
|
retry:
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_Engine = new DBreezeEngine(dbreezePath);
|
_Engine = new DBreezeEngine(dbreezePath);
|
||||||
@@ -385,7 +385,8 @@ namespace BTCPayServer.Services.Invoices
|
|||||||
var paymentEntity = ToObject<PaymentEntity>(p.Blob, null);
|
var paymentEntity = ToObject<PaymentEntity>(p.Blob, null);
|
||||||
paymentEntity.Accounted = p.Accounted;
|
paymentEntity.Accounted = p.Accounted;
|
||||||
return paymentEntity;
|
return paymentEntity;
|
||||||
}).ToList();
|
})
|
||||||
|
.OrderBy(a => a.ReceivedTime).ToList();
|
||||||
#pragma warning restore CS0618
|
#pragma warning restore CS0618
|
||||||
var state = invoice.GetInvoiceState();
|
var state = invoice.GetInvoiceState();
|
||||||
entity.ExceptionStatus = state.ExceptionStatus;
|
entity.ExceptionStatus = state.ExceptionStatus;
|
||||||
|
|||||||
@@ -72,6 +72,11 @@
|
|||||||
<input asp-for="CustomTipText" class="form-control" />
|
<input asp-for="CustomTipText" class="form-control" />
|
||||||
<span asp-validation-for="CustomTipText" class="text-danger"></span>
|
<span asp-validation-for="CustomTipText" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="CustomTipPercentages" class="control-label"></label>
|
||||||
|
<input asp-for="CustomTipPercentages" class="form-control" />
|
||||||
|
<span asp-validation-for="CustomTipPercentages" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="CustomCSSLink" class="control-label"></label>
|
<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>
|
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = Model.Title;
|
ViewData["Title"] = Model.Title;
|
||||||
Layout = null;
|
Layout = null;
|
||||||
|
int[] CustomTipPercentages = Model.CustomTipPercentages;
|
||||||
}
|
}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -14,6 +15,11 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
|
||||||
|
<link rel="apple-touch-startup-image" href="~/img/splash.png">
|
||||||
|
|
||||||
|
<link rel="manifest" href="~/manifest.json">
|
||||||
|
|
||||||
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
|
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||||
@if (Model.CustomCSSLink != null)
|
@if (Model.CustomCSSLink != null)
|
||||||
{
|
{
|
||||||
@@ -23,55 +29,259 @@
|
|||||||
|
|
||||||
@if (Model.EnableShoppingCart)
|
@if (Model.EnableShoppingCart)
|
||||||
{
|
{
|
||||||
|
<link rel="stylesheet" href="~/cart/css/style.css">
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||||
</script>
|
</script>
|
||||||
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
|
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
|
||||||
}
|
}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
<script id="template-cart-item" type="text/template">
|
||||||
|
<tr data-id="{id}">
|
||||||
|
{image}
|
||||||
|
<td class="align-middle pr-0 pl-2"><b>{title}</b></td>
|
||||||
|
<td class="align-middle px-0" align="right">
|
||||||
|
<a class="js-cart-item-remove btn btn-link" href="#"><i class="fa fa-trash text-muted"></i></a>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle px-0" align="right">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<a class="js-cart-item-minus btn btn-link px-2" href="#"><i class="fa fa-minus-circle fa-fw text-danger"></i></a>
|
||||||
|
</div>
|
||||||
|
<input class="js-cart-item-count form-control form-control-sm pull-left" type="text"name="count" placeholder="Qty" value="{count}" data-prev="{count}">
|
||||||
|
<div class="input-group-append"><a class="js-cart-item-plus btn btn-link px-2" href="#">
|
||||||
|
<i class="fa fa-plus-circle fa-fw text-success"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="align-middle" align="right">{price}</td>
|
||||||
|
</tr>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="template-cart-item-image" type="text/template">
|
||||||
|
<td class="align-middle pr-0" width="1%"><img src="{image}" width="50"></td>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="template-cart-custom-amount" type="text/template">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
|
||||||
|
</div>
|
||||||
|
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Pay what you want">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="template-cart-extra" type="text/template">
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="border-0 pb-0">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
|
||||||
|
</div>
|
||||||
|
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" value="{customAmount}" placeholder="Pay what you want">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="border-top-0">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
|
||||||
|
</div>
|
||||||
|
<input class="js-cart-discount form-control" type="number" min="0" step="@Model.Step" value="{discount}" name="discount" placeholder="Discount in %">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<a class="js-cart-discount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="template-cart-tip" type="text/template">
|
||||||
|
<tr class="h5">
|
||||||
|
<td colspan="5" class="border-top-0 pt-4">@Model.CustomTipText</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="border-0">
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
|
||||||
|
</div>
|
||||||
|
<input class="js-cart-tip form-control" type="number" min="0" step="@Model.Step" value="{tip}" name="tip" placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol != null ? Model.CurrencyInfo.CurrencySymbol : Model.CurrencyCode)">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<a class="js-cart-tip-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-1">
|
||||||
|
@if (CustomTipPercentages != null && CustomTipPercentages.Length > 0) {
|
||||||
|
@for (int i = 0; i < CustomTipPercentages.Length; i++) {
|
||||||
|
var percentage = CustomTipPercentages[i];
|
||||||
|
<div class="col">
|
||||||
|
<a class="js-cart-tip-btn btn btn-light btn-block border mb-2" href="#" data-tip="@percentage">@percentage%</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="template-cart-total" type="text/template">
|
||||||
|
<tr class="h4 table-light">
|
||||||
|
<td colspan="1" class="pb-4">Total</td>
|
||||||
|
<td colspan="4" align="right" class="pb-4">
|
||||||
|
<span class="js-cart-total">{total}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</script>
|
||||||
|
|
||||||
<body class="h-100">
|
<body class="h-100">
|
||||||
@if (Model.EnableShoppingCart)
|
@if (Model.EnableShoppingCart)
|
||||||
{
|
{
|
||||||
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
|
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="modal-dialog" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header bg-primary text-white border-0">
|
||||||
<h5 class="modal-title">Shopping cart</h5>
|
<h5 class="modal-title">Confirmation</h5>
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
|
||||||
<span aria-hidden="true">×</span>
|
<span aria-hidden="true"><i class="fa fa-times fa-fw"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body p-0">
|
||||||
<table id="js-cart-list" class="table mt-2 mb-3">
|
<table id="js-cart-summary" class="table m-0">
|
||||||
<thead class="thead-dark">
|
<tbody class="my-3">
|
||||||
<tr>
|
<tr class="h5">
|
||||||
<th colspan="2">Product</th>
|
<td colspan="2" class="border-top-0">Summary</td>
|
||||||
<th class="text-right" width="80">Quantity</th>
|
|
||||||
<th class="text-right" width="25%">Price</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
<tr class="h6">
|
||||||
<tbody></tbody>
|
<td class="border-0 pb-0">Total products</td>
|
||||||
|
<td align="right" class="border-0 pb-0">
|
||||||
|
<span class="js-cart-summary-products text-nowrap"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="h6">
|
||||||
|
<td class="border-0 pb-y">Discount</td>
|
||||||
|
<td align="right" class="border-0 pb-y">
|
||||||
|
<span class="js-cart-summary-discount text-nowrap"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="h6">
|
||||||
|
<td class="border-top-0 pt-0">Tip</td>
|
||||||
|
<td align="right" class="border-top-0 pt-0">
|
||||||
|
<span class="js-cart-summary-tip text-nowrap"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="h3 table-light">
|
||||||
|
<td>Total</td>
|
||||||
|
<td align="right">
|
||||||
|
<span class="js-cart-summary-total text-nowrap"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer bg-light">
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
|
||||||
<form method="post" asp-antiforgery="false" data-buy>
|
<form method="post" asp-antiforgery="false" data-buy>
|
||||||
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
|
<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>
|
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Page Content -->
|
||||||
|
<div id="content">
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-4 col-lg-3 order-sm-last text-right mb-2">
|
||||||
|
<a class="js-cart btn btn-warning text-white text-right" href="#"><i class="fa fa-shopping-basket"></i> <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-8 col-lg-9 mb-2">
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<input type="text" class="js-search form-control" placeholder="Find product">
|
||||||
|
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute;right: 0px; z-index: 9999; display: none;"><i class="fa fa-times-circle fa-lg"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="js-pos-list" class="text-center mx-auto px-4">
|
||||||
|
<div class="row">
|
||||||
|
@for (int i = 0; i < Model.Items.Length; i++)
|
||||||
|
{
|
||||||
|
var item = Model.Items[i];
|
||||||
|
var image = item.Image;
|
||||||
|
var description = item.Description;
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-lg-3 my-3 px-2 card-wrapper">
|
||||||
|
<div class="js-add-cart card" data-id="@i">
|
||||||
|
@if (!String.IsNullOrWhiteSpace(image))
|
||||||
|
{
|
||||||
|
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||||
|
}
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<h6 class="card-title mb-0">@item.Title</h6>
|
||||||
|
@if (!String.IsNullOrWhiteSpace(description))
|
||||||
|
{
|
||||||
|
<p class="card-text">@description</p>
|
||||||
|
}
|
||||||
|
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav id="sidebar" class="bg-dark">
|
||||||
|
<div class="bg-warning p-3 clearfix">
|
||||||
|
<h3 class="text-white m-0 pull-left">Cart</h3>
|
||||||
|
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#"><i class="fa fa-times fa-lg"></i></a>
|
||||||
|
<a class="js-cart-destroy btn btn-sm bg-white text-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="js-cart-list" class="table table-responsive bg-light mt-0 mb-0">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th colspan="3" width="55%">Product</th>
|
||||||
|
<th class="text-center" width="20%"><div style="width: 84px">Quantity</div></th>
|
||||||
|
<th class="text-right" width="25%"><div style="min-width: 50px">Price</div></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table id="js-cart-extra" class="table bg-light mt-0 mb-0">
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit"><b>Confirm</b></button>
|
||||||
|
|
||||||
|
<div class="text-center mb-5 pb-5">
|
||||||
|
<img src="~/img/logo-white.png" height="40">
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
} else {
|
||||||
<div class="container d-flex h-100">
|
<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;">
|
<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>
|
<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> <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
|
|
||||||
}
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@for (int i = 0; i < Model.Items.Length; i++)
|
@for (int i = 0; i < Model.Items.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -143,5 +353,6 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
|
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="CryptoCurrency" asp-for="CryptoCode" type="hidden" />
|
<input id="CryptoCurrency" asp-for="CryptoCode" type="hidden" />
|
||||||
|
<input id="KeyPath" asp-for="KeyPath" type="hidden" />
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="DerivationScheme"></label>
|
<label asp-for="DerivationScheme"></label>
|
||||||
<input asp-for="DerivationScheme" class="form-control" />
|
<input asp-for="DerivationScheme" class="form-control" />
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
@for (int i = 0; i < 4; i++)
|
@for (int i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
|
<li><a class="ledger-info-recommended" data-ledgerkeypath="@Model.RootKeyPath.Derive(i, true)" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,6 +96,7 @@
|
|||||||
<span>Please check that your @Model.CryptoCode wallet is generating the same addresses as below.</span>
|
<span>Please check that your @Model.CryptoCode wallet is generating the same addresses as below.</span>
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" asp-for="Confirmation" />
|
<input type="hidden" asp-for="Confirmation" />
|
||||||
|
<input id="KeyPath" asp-for="KeyPath" type="hidden" />
|
||||||
<input type="hidden" asp-for="DerivationScheme" />
|
<input type="hidden" asp-for="DerivationScheme" />
|
||||||
<input type="hidden" asp-for="Enabled" />
|
<input type="hidden" asp-for="Enabled" />
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -28,10 +28,6 @@
|
|||||||
<p id="hw-loading"><span class="fa fa-question-circle" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
|
<p id="hw-loading"><span class="fa fa-question-circle" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
|
||||||
<p id="hw-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="hw-label">An error happened</span></p>
|
<p id="hw-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="hw-label">An error happened</span></p>
|
||||||
<p id="hw-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="hw-label">Detecting hardware wallet...</span></p>
|
<p id="hw-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="hw-label">Detecting hardware wallet...</span></p>
|
||||||
|
|
||||||
<p id="check-loading" style="display:none;"><span class="fa fa-question-circle" style="color:orange"></span> <span class="check-label">Detecting hardware wallet...</span></p>
|
|
||||||
<p id="check-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="check-label">An error happened</span></p>
|
|
||||||
<p id="check-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
128
BTCPayServer/wwwroot/cart/css/style.css
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
.modal-content {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
border-top-left-radius: 0.4rem;
|
||||||
|
border-top-right-radius: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-img-top {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 180px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.js-cart-added {
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.js-cart-added .fa {
|
||||||
|
height: 50px;
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.js-add-cart:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#js-cart-confirm {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------
|
||||||
|
SIDEBAR STYLE
|
||||||
|
----------------------------------------------------- */
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar {
|
||||||
|
width: 400px;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
z-index: 999;
|
||||||
|
background: #e1e6ea;
|
||||||
|
transition: all 0.3s;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
#sidebar .js-cart {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#sidebar.active {
|
||||||
|
margin-right: -400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------
|
||||||
|
CONTENT STYLE
|
||||||
|
----------------------------------------------------- */
|
||||||
|
|
||||||
|
#content {
|
||||||
|
width: calc(100% - 400px);
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content.active {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray {
|
||||||
|
background-color: #aaa;
|
||||||
|
}
|
||||||
|
.text-black {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------
|
||||||
|
MEDIAQUERIES
|
||||||
|
----------------------------------------------------- */
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#sidebar {
|
||||||
|
margin-right: -400px;
|
||||||
|
}
|
||||||
|
#sidebar .js-cart {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
#sidebar.active {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#content.active {
|
||||||
|
width: calc(100% - 400px);
|
||||||
|
}
|
||||||
|
#sidebarCollapse span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 575px) {
|
||||||
|
#sidebar {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: -575px;
|
||||||
|
}
|
||||||
|
#content.active {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +1,47 @@
|
|||||||
$.fn.addAnimate = function(completeCallback) {
|
$.fn.addAnimate = function(completeCallback) {
|
||||||
var documentHeight = $(document).height(),
|
if ($(this).find('.js-cart-added').length === 0) {
|
||||||
itemPos = $(this).offset(),
|
$(this).append('<div class="js-cart-added"><i class="fa fa-check fa-3x text-white align-middle"></i></div>');
|
||||||
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
|
// Animate the element
|
||||||
var animationSpeed = (Math.log(itemY) * (documentHeight / Math.log2(documentHeight - itemY))) / 2;
|
$(this).find('.js-cart-added').fadeIn(200, function(){
|
||||||
|
var self = this;
|
||||||
// Add the cart item badge and animate it
|
// Show it for 200ms
|
||||||
$('body').after(tempItem);
|
setTimeout(function(){
|
||||||
$('#js-cart-temp-item').animate({
|
// Hide and remove
|
||||||
easing: 'swing',
|
$(self).fadeOut(100, function(){
|
||||||
top: cartPos.top,
|
|
||||||
left: cartPos.left
|
|
||||||
}, animationSpeed, function() {
|
|
||||||
$(this).remove();
|
$(this).remove();
|
||||||
|
|
||||||
completeCallback && completeCallback();
|
completeCallback && completeCallback();
|
||||||
|
})
|
||||||
|
}, 200);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAccents(input){
|
||||||
|
var accents = 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇČçčÐĎďÌÍÎÏìíîïĽľÙÚÛÜùúûüÑŇñňŠšŤťŸÿýŽž ́',
|
||||||
|
accentsOut = 'AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCCccDDdIIIIiiiiLlUUUUuuuuNNnnSsTtYyyZz ',
|
||||||
|
output = '',
|
||||||
|
index = -1;
|
||||||
|
|
||||||
|
for( var i = 0; i < input.length; i++ ) {
|
||||||
|
index = accents.indexOf(input[i]);
|
||||||
|
|
||||||
|
if( index != -1 ) {
|
||||||
|
output += typeof accentsOut[index] != 'undefined' ? accentsOut[index] : '';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
output += typeof input[i] != 'undefined' ? input[i] : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
jQuery.expr[':'].icontains = function (a, i, m) {
|
||||||
|
var string = removeAccents(jQuery(a).text().toLowerCase());
|
||||||
|
|
||||||
|
return string.indexOf(removeAccents(m[3].toLowerCase())) >= 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
$(document).ready(function(){
|
$(document).ready(function(){
|
||||||
@@ -31,27 +51,86 @@ $(document).ready(function(){
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
var $btn = $(event.target),
|
var $btn = $(event.target),
|
||||||
|
self = this;
|
||||||
id = $btn.closest('.card').data('id'),
|
id = $btn.closest('.card').data('id'),
|
||||||
item = srvModel.items[id];
|
item = srvModel.items[id],
|
||||||
|
items = cart.items;
|
||||||
|
|
||||||
// Animate adding and then add then save
|
// Is event catching disabled?
|
||||||
|
if (!$(this).hasClass('disabled')) {
|
||||||
|
// Disable catching events for this element
|
||||||
|
$(this).addClass('disabled');
|
||||||
|
|
||||||
|
// Add-to-cart animation only once
|
||||||
$(this).addAnimate(function(){
|
$(this).addAnimate(function(){
|
||||||
|
// Enable the event
|
||||||
|
$(self).removeClass('disabled');
|
||||||
|
});
|
||||||
|
|
||||||
cart.addItem({
|
cart.addItem({
|
||||||
id: id,
|
id: id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
price: item.price,
|
price: item.price,
|
||||||
image: typeof item.image != 'underfined' ? item.image : null
|
image: typeof item.image != 'underfined' ? item.image : null
|
||||||
});
|
});
|
||||||
});
|
cart.listItems();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Destroy the cart when the "pay button is clicked"
|
// Destroy the cart when the "pay button is clicked"
|
||||||
$('#js-cart-pay').click(function(){
|
$('#js-cart-pay').click(function(){
|
||||||
cart.destroy();
|
cart.destroy(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Repopulate cart items in the modal when it opens
|
$('.js-cart').on('click', function () {
|
||||||
$('#cartModal').on('show.bs.modal', function () {
|
$('#sidebar, #content').toggleClass('active');
|
||||||
cart.listItems();
|
$('.collapse.in').toggleClass('in');
|
||||||
|
$('a[aria-expanded=true]').attr('aria-expanded', 'false');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.js-search').keyup(function(event){
|
||||||
|
var str = $(this).val();
|
||||||
|
|
||||||
|
$('#js-pos-list').find(".card-wrapper").show();
|
||||||
|
|
||||||
|
if (str.length > 1) {
|
||||||
|
var $list = $('#js-pos-list').find(".card-title:not(:icontains('" + str + "'))");
|
||||||
|
$list.parents('.card-wrapper').hide();
|
||||||
|
$('.js-search-reset').show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.js-search-reset').click(function(event){
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
$('.js-search').val('');
|
||||||
|
$('.js-search').trigger('keyup');
|
||||||
|
$(this).hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#js-cart-summary').find('tbody').prepend(cart.template($('#template-cart-tip'), {
|
||||||
|
'tip': cart.fromCents(cart.getTip()) || ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
$('#cartModal').one('show.bs.modal', function () {
|
||||||
|
cart.updateDiscount();
|
||||||
|
cart.updateTip();
|
||||||
|
cart.updateSummaryProducts();
|
||||||
|
cart.updateSummaryTotal();
|
||||||
|
|
||||||
|
// Change total when tip is changed
|
||||||
|
$('.js-cart-tip').inputAmount(cart, 'tip');
|
||||||
|
// Remove tip
|
||||||
|
$('.js-cart-tip-remove').removeAmount(cart, 'tip');
|
||||||
|
|
||||||
|
$('.js-cart-tip-btn').click(function(event){
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var $tip = $('.js-cart-tip'),
|
||||||
|
discount = cart.percentage(cart.getTotalProducts(), cart.getDiscount());
|
||||||
|
|
||||||
|
$tip.val(cart.percentage(cart.getTotalProducts() - discount, parseInt($(this).data('tip'))));
|
||||||
|
$tip.trigger('input');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2,126 +2,313 @@ function Cart() {
|
|||||||
this.items = 0;
|
this.items = 0;
|
||||||
this.totalAmount = 0;
|
this.totalAmount = 0;
|
||||||
this.content = [];
|
this.content = [];
|
||||||
this.tip = 0;
|
|
||||||
|
|
||||||
this.loadLocalStorage();
|
this.loadLocalStorage();
|
||||||
this.itemsCount();
|
this.buildUI();
|
||||||
|
|
||||||
|
this.$list = $('#js-cart-list');
|
||||||
|
this.$items = $('#js-cart-items');
|
||||||
|
this.$total = $('.js-cart-total');
|
||||||
|
this.$summaryProducts = $('.js-cart-summary-products');
|
||||||
|
this.$summaryDiscount = $('.js-cart-summary-discount');
|
||||||
|
this.$summaryTotal = $('.js-cart-summary-total');
|
||||||
|
this.$summaryTip = $('.js-cart-summary-tip');
|
||||||
|
this.$destroy = $('.js-cart-destroy');
|
||||||
|
this.$confirm = $('#js-cart-confirm');
|
||||||
|
|
||||||
this.listItems();
|
this.listItems();
|
||||||
|
this.bindEmptyCart();
|
||||||
|
|
||||||
|
this.updateItemsCount();
|
||||||
this.updateAmount();
|
this.updateAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart.prototype.addItem = function(item) {
|
Cart.prototype.setCustomAmount = function(amount) {
|
||||||
// Increment the existing item count
|
this.customAmount = this.toNumber(amount);
|
||||||
var result = this.content.filter(function(obj){
|
|
||||||
if (obj.id === item.id){
|
if (this.customAmount > 0) {
|
||||||
obj.count++;
|
localStorage.setItem(this.getStorageKey('cartCustomAmount'), this.customAmount);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(this.getStorageKey('cartCustomAmount'));
|
||||||
|
}
|
||||||
|
return this.customAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.getCustomAmount = function() {
|
||||||
|
return this.toCents(this.customAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.setTip = function(amount) {
|
||||||
|
this.tip = this.toNumber(amount);
|
||||||
|
|
||||||
|
if (this.tip > 0) {
|
||||||
|
localStorage.setItem(this.getStorageKey('cartTip'), this.tip);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(this.getStorageKey('cartTip'));
|
||||||
|
}
|
||||||
|
return this.tip;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.getTip = function() {
|
||||||
|
return this.toCents(this.tip);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.setDiscount = function(amount) {
|
||||||
|
this.discount = this.toNumber(amount);
|
||||||
|
|
||||||
|
if (this.discount > 0) {
|
||||||
|
localStorage.setItem(this.getStorageKey('cartDiscount'), this.discount);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(this.getStorageKey('cartDiscount'));
|
||||||
|
}
|
||||||
|
return this.discount;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.getDiscount = function() {
|
||||||
|
return this.toCents(this.discount);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.getDiscountAmount = function(amount) {
|
||||||
|
return this.percentage(amount, this.getDiscount());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total amount of products
|
||||||
|
Cart.prototype.getTotalProducts = function() {
|
||||||
|
var amount = 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' &&
|
||||||
|
!this.content[key].disabled
|
||||||
|
) {
|
||||||
|
var price = this.toCents(this.content[key].price.value);
|
||||||
|
amount += (this.content[key].count * price);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj.id === item.id
|
// Add custom amount
|
||||||
|
amount += this.getCustomAmount();
|
||||||
|
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get absolute total amount
|
||||||
|
Cart.prototype.getTotal = function(includeTip) {
|
||||||
|
this.totalAmount = this.getTotalProducts();
|
||||||
|
|
||||||
|
if (this.getDiscount() > 0) {
|
||||||
|
this.totalAmount -= this.getDiscountAmount(this.totalAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeTip) {
|
||||||
|
this.totalAmount += this.getTip();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fromCents(this.totalAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Data manipulation
|
||||||
|
*/
|
||||||
|
// Add item to the cart or update its count
|
||||||
|
Cart.prototype.addItem = function(item) {
|
||||||
|
var id = item.id,
|
||||||
|
result = this.content.filter(function(obj){
|
||||||
|
return obj.id === id;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add new item because it doesn't exist yet
|
// Add new item because it doesn't exist yet
|
||||||
if (!result.length) {
|
if (!result.length) {
|
||||||
this.content.push({id: item.id, title: item.title, price: item.price, count: 1, image: item.image})
|
this.content.push({id: id, title: item.title, price: item.price, count: 0, image: item.image});
|
||||||
|
this.emptyCartToggle();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.items++;
|
// Increment item count
|
||||||
this.saveLocalStorage();
|
this.incrementItem(id);
|
||||||
this.itemsCount();
|
}
|
||||||
this.updateTotal();
|
|
||||||
this.updateAmount();
|
Cart.prototype.incrementItem = function(id) {
|
||||||
|
var self = this;
|
||||||
|
this.items = 0; // Calculate total # of items from scratch just to make sure
|
||||||
|
|
||||||
|
this.content.filter(function(obj){
|
||||||
|
// Increment the item count
|
||||||
|
if (obj.id === id){
|
||||||
|
obj.count++;
|
||||||
|
delete(obj.disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the total # of items
|
||||||
|
self.items += obj.count;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable cart item so it doesn't count towards total amount
|
||||||
|
Cart.prototype.disableItem = function(id) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this.content.filter(function(obj){
|
||||||
|
if (obj.id === id){
|
||||||
|
obj.disabled = true;
|
||||||
|
self.items -= obj.count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable cart item so it counts towards total amount
|
||||||
|
Cart.prototype.enableItem = function(id) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this.content.filter(function(obj){
|
||||||
|
if (obj.id === id){
|
||||||
|
delete(obj.disabled);
|
||||||
|
self.items += obj.count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart.prototype.decrementItem = function(id) {
|
Cart.prototype.decrementItem = function(id) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
this.items = 0; // Calculate total # of items from scratch just to make sure
|
||||||
|
|
||||||
// Decrement the existing item count
|
|
||||||
this.content.filter(function(obj, index, arr){
|
this.content.filter(function(obj, index, arr){
|
||||||
|
// Decrement the item count
|
||||||
if (obj.id === id)
|
if (obj.id === id)
|
||||||
{
|
{
|
||||||
obj.count--;
|
obj.count--;
|
||||||
|
delete(obj.disabled);
|
||||||
|
|
||||||
// It's the last item with the same ID, remove it
|
// It's the last item with the same ID, remove it
|
||||||
if (obj.count === 0) {
|
if (obj.count <= 0) {
|
||||||
self.removeItem(id, index, arr);
|
self.removeItem(id, index, arr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.items += obj.count;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.items--;
|
this.updateAll();
|
||||||
this.saveLocalStorage();
|
|
||||||
this.itemsCount();
|
|
||||||
this.updateTotal();
|
|
||||||
this.updateAmount();
|
|
||||||
|
|
||||||
if (this.items === 0) {
|
|
||||||
this.emptyList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart.prototype.removeItemAll = function(id) {
|
Cart.prototype.removeItemAll = function(id) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
this.items = 0;
|
||||||
|
|
||||||
|
// Remove by item
|
||||||
|
if (typeof id != 'undefined') {
|
||||||
this.content.filter(function(obj, index, arr){
|
this.content.filter(function(obj, index, arr){
|
||||||
if (obj.id === id)
|
if (obj.id === id) {
|
||||||
{
|
|
||||||
self.removeItem(id, index, arr);
|
self.removeItem(id, index, arr);
|
||||||
|
|
||||||
for (var i = 0; i < obj.count; i++) {
|
for (var i = 0; i < obj.count; i++) {
|
||||||
self.items--;
|
self.items--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.items += obj.count;
|
||||||
});
|
});
|
||||||
|
} else { // Remove all
|
||||||
this.saveLocalStorage();
|
this.$list.find('tbody').empty();
|
||||||
this.itemsCount();
|
this.content = [];
|
||||||
this.updateTotal();
|
|
||||||
this.updateAmount();
|
|
||||||
|
|
||||||
if (this.items === 0) {
|
|
||||||
this.emptyList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.emptyCartToggle();
|
||||||
|
this.updateAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart.prototype.removeItem = function(id, index, arr) {
|
Cart.prototype.removeItem = function(id, index, arr) {
|
||||||
// Remove from the array
|
// Remove from the array
|
||||||
arr.splice(index, 1);
|
arr.splice(index, 1);
|
||||||
// Remove from the DOM
|
// Remove from the DOM
|
||||||
$('#js-cart-list').find('tr').eq(index+1).remove();
|
this.$list.find('tr').eq(index+1).remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart.prototype.setTip = function(tip) {
|
/*
|
||||||
return this.tip = tip;
|
* Update DOM
|
||||||
|
*/
|
||||||
|
// Update all data elements
|
||||||
|
Cart.prototype.updateAll = function() {
|
||||||
|
this.saveLocalStorage();
|
||||||
|
this.updateItemsCount();
|
||||||
|
this.updateDiscount();
|
||||||
|
this.updateSummaryProducts();
|
||||||
|
this.updateSummaryTotal();
|
||||||
|
this.updateTotal();
|
||||||
|
this.updateAmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart.prototype.itemsCount = function() {
|
// Update number of cart items
|
||||||
$('#js-cart-items').text(this.items);
|
Cart.prototype.updateItemsCount = function() {
|
||||||
}
|
this.$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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update total products (including the custom amount and discount) in the cart
|
||||||
Cart.prototype.updateTotal = function() {
|
Cart.prototype.updateTotal = function() {
|
||||||
$('#js-cart-total').text(this.formatCurrency(this.getTotal(), srvModel.currencyCode, srvModel.currencySymbol));
|
this.$total.text(this.formatCurrency(this.getTotal()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update total amount in the summary
|
||||||
|
Cart.prototype.updateSummaryTotal = function() {
|
||||||
|
this.$summaryTotal.text(this.formatCurrency(this.getTotal(true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update total products amount in the summary
|
||||||
|
Cart.prototype.updateSummaryProducts = function() {
|
||||||
|
this.$summaryProducts.text(this.formatCurrency(this.fromCents(this.getTotalProducts())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update discount amount in the summary
|
||||||
|
Cart.prototype.updateDiscount = function(amount) {
|
||||||
|
var discount = 0;
|
||||||
|
|
||||||
|
if (typeof amount != 'undefined') {
|
||||||
|
discount = amount;
|
||||||
|
} else {
|
||||||
|
discount = this.percentage(this.getTotalProducts(), this.getDiscount());
|
||||||
|
discount = this.fromCents(discount);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$summaryDiscount.text((discount > 0 ? '-' : '') + this.formatCurrency(discount));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update tip amount in the summary
|
||||||
|
Cart.prototype.updateTip = function(amount) {
|
||||||
|
var tip = typeof amount != 'undefined' ? amount : this.fromCents(this.getTip());
|
||||||
|
|
||||||
|
this.$summaryTip.text(this.formatCurrency(tip));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update hidden total amount value to be sent to the checkout page
|
||||||
Cart.prototype.updateAmount = function() {
|
Cart.prototype.updateAmount = function() {
|
||||||
$('#js-cart-amount').val(this.getTotal());
|
$('#js-cart-amount').val(this.getTotal(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cart.prototype.resetDiscount = function() {
|
||||||
|
this.setDiscount(0);
|
||||||
|
this.updateDiscount(0);
|
||||||
|
$('.js-cart-discount').val('');
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.resetTip = function() {
|
||||||
|
this.setTip(0);
|
||||||
|
this.updateTip(0);
|
||||||
|
$('.js-cart-tip').val('');
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.resetCustomAmount = function() {
|
||||||
|
this.setCustomAmount(0);
|
||||||
|
$('.js-cart-custom-amount').val('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape html characters
|
||||||
Cart.prototype.escape = function(input) {
|
Cart.prototype.escape = function(input) {
|
||||||
return ('' + input) /* Forces the conversion to string. */
|
return ('' + input) /* Forces the conversion to string. */
|
||||||
.replace(/&/g, '&') /* This MUST be the 1st replacement. */
|
.replace(/&/g, '&') /* This MUST be the 1st replacement. */
|
||||||
@@ -132,8 +319,51 @@ Cart.prototype.escape = function(input) {
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the template
|
||||||
|
Cart.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the cart skeleton
|
||||||
|
Cart.prototype.buildUI = function() {
|
||||||
|
var $table = $('#js-cart-extra').find('tbody'),
|
||||||
|
list = [];
|
||||||
|
|
||||||
|
tableTemplate = this.template($('#template-cart-extra'), {
|
||||||
|
'discount': this.escape(this.fromCents(this.getDiscount()) || ''),
|
||||||
|
'customAmount': this.escape(this.fromCents(this.getCustomAmount()) || '')
|
||||||
|
});
|
||||||
|
list.push($(tableTemplate));
|
||||||
|
|
||||||
|
tableTemplate = this.template($('#template-cart-total'), {
|
||||||
|
'total': this.escape(this.formatCurrency(this.getTotal()))
|
||||||
|
});
|
||||||
|
list.push($(tableTemplate));
|
||||||
|
|
||||||
|
// Add the list to DOM
|
||||||
|
$table.append(list);
|
||||||
|
|
||||||
|
// Change total when discount is changed
|
||||||
|
$('.js-cart-discount').inputAmount(this, 'discount');
|
||||||
|
// Remove discount
|
||||||
|
$('.js-cart-discount-remove').removeAmount(this, 'discount');
|
||||||
|
|
||||||
|
// Change total when discount is changed
|
||||||
|
$('.js-cart-custom-amount').inputAmount(this, 'customAmount');
|
||||||
|
// Remove discount
|
||||||
|
$('.js-cart-custom-amount-remove').removeAmount(this, 'customAmount');
|
||||||
|
}
|
||||||
|
|
||||||
|
// List cart items and bind their events
|
||||||
Cart.prototype.listItems = function() {
|
Cart.prototype.listItems = function() {
|
||||||
var $table = $('#js-cart-list').find('tbody'),
|
var $table = this.$list.find('tbody'),
|
||||||
self = this,
|
self = this,
|
||||||
list = [],
|
list = [],
|
||||||
tableTemplate = '';
|
tableTemplate = '';
|
||||||
@@ -142,75 +372,75 @@ Cart.prototype.listItems = function() {
|
|||||||
// Prepare the list of items in the cart
|
// Prepare the list of items in the cart
|
||||||
for (var key in this.content) {
|
for (var key in this.content) {
|
||||||
var item = this.content[key],
|
var item = this.content[key],
|
||||||
id = this.escape(item.id),
|
image = this.escape(item.image);
|
||||||
title = this.escape(item.title),
|
|
||||||
image = this.escape(item.image),
|
|
||||||
count = this.escape(item.count),
|
|
||||||
price = this.escape(item.price.formatted),
|
|
||||||
total = this.escape(this.formatCurrency(this.getTotal(), srvModel.currencyCode, srvModel.currencySymbol)),
|
|
||||||
step = this.escape(srvModel.step),
|
|
||||||
tip = this.escape(this.tip || ''),
|
|
||||||
customTipText = this.escape(srvModel.customTipText);
|
|
||||||
|
|
||||||
tableTemplate = '<tr data-id="' + id + '">' +
|
tableTemplate = this.template($('#template-cart-item'), {
|
||||||
(image !== null ? '<td class="align-middle pr-0" width="60"><img src="' + image + '" width="100%"></td>' : '') +
|
'id': this.escape(item.id),
|
||||||
'<td class="align-middle pr-0"><b>' + title + '</b></td>' +
|
'image': image ? this.template($('#template-cart-item-image'), {
|
||||||
'<td class="align-middle pr-0" align="right"><div class="input-group">' +
|
'image' : image
|
||||||
' <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>' +
|
'title': this.escape(item.title),
|
||||||
'</div></td>' +
|
'count': this.escape(item.count),
|
||||||
'<td class="align-middle" align="right">' + price + '</td>' +
|
'price': this.escape(item.price.formatted)
|
||||||
'</tr>';
|
});
|
||||||
list.push($(tableTemplate));
|
list.push($(tableTemplate));
|
||||||
}
|
}
|
||||||
|
|
||||||
tableTemplate = '<tr><td colspan="4"><div class="row"><div class="col-sm-7 py-2">' + customTipText + '</div><div class="col-sm-5">' +
|
|
||||||
'<div class="input-group">' +
|
|
||||||
'<div class="input-group-prepend">' +
|
|
||||||
'<span class="input-group-text"><i class="fa fa-money"></i></span>' +
|
|
||||||
'</div>' +
|
|
||||||
'<input class="js-cart-tip form-control" type="number" min="0" step="' + step + '" value="' + tip + '" name="tip" placeholder="Amount">' +
|
|
||||||
'</div>' +
|
|
||||||
'</div></div></td></tr>';
|
|
||||||
list.push($(tableTemplate));
|
|
||||||
|
|
||||||
tableTemplate = '<tr class="bg-light h4"><td colspan="1">Total</td><td colspan="3" align="right"><span id="js-cart-total">' + total + '</span></td></tr>';
|
|
||||||
list.push($(tableTemplate));
|
|
||||||
|
|
||||||
// Add the list to DOM
|
// Add the list to DOM
|
||||||
$table.html(list);
|
$table.html(list);
|
||||||
|
list = [];
|
||||||
|
|
||||||
// Update the cart when number of items is changed
|
// Update the cart when number of items is changed
|
||||||
$('.js-cart-item-count').off().on('input', function(event){
|
$('.js-cart-item-count').off().on('input', function(event){
|
||||||
var _this = this,
|
var _this = this,
|
||||||
id = $(this).closest('tr').data('id'),
|
id = $(this).closest('tr').data('id'),
|
||||||
count = parseInt($(this).val()),
|
qty = parseInt($(this).val()),
|
||||||
prevCount = parseInt($(this).data('prev')),
|
isQty = !isNaN(qty),
|
||||||
increased = count > prevCount;
|
prevQty = parseInt($(this).data('prev')),
|
||||||
|
qtyDiff = Math.abs(qty - prevQty),
|
||||||
|
qtyIncreased = qty > prevQty;
|
||||||
|
|
||||||
// User hasn't inputed any number so stop here
|
if (isQty) {
|
||||||
if (isNaN(count)) {
|
$(this).data('prev', qty);
|
||||||
return false;
|
} else {
|
||||||
|
// User hasn't inputed any quantity
|
||||||
|
qty = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$(this).data('prev', count);
|
self.resetTip();
|
||||||
|
|
||||||
|
// Quantity was increased
|
||||||
|
if (qtyIncreased) {
|
||||||
var item = self.content.filter(function(obj){
|
var item = self.content.filter(function(obj){
|
||||||
return obj.id === id
|
return obj.id === id;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Must be in the loop because user may change the count manually by more than 1
|
// Quantity may have been increased by more than one
|
||||||
for (var i = 0; i < Math.abs(count - prevCount); i++) {
|
for (var i = 0; i < qtyDiff; i++) {
|
||||||
if (increased) {
|
|
||||||
self.addItem({
|
self.addItem({
|
||||||
id: id,
|
id: id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
price: item.price,
|
price: item.price,
|
||||||
image: typeof item.image != 'underfined' ? item.image : null
|
image: typeof item.image != 'underfined' ? item.image : null
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
} else if (!qtyIncreased) { // Quantity decreased
|
||||||
|
// No quantity set (e.g. empty string)
|
||||||
|
if (!isQty) {
|
||||||
|
// Disable the item so it doesn't count towards total amount
|
||||||
|
self.disableItem(id);
|
||||||
} else {
|
} else {
|
||||||
|
// Quantity vas decreased
|
||||||
|
if (qtyDiff > 0) {
|
||||||
|
// Quantity may have been decreased by more than one
|
||||||
|
for (var i = 0; i < qtyDiff; i++) {
|
||||||
self.decrementItem(id);
|
self.decrementItem(id);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Quantity hasn't changed, enable the item so it counts towards the total amount
|
||||||
|
self.enableItem(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -218,29 +448,71 @@ Cart.prototype.listItems = function() {
|
|||||||
$('.js-cart-item-remove').off().on('click', function(event){
|
$('.js-cart-item-remove').off().on('click', function(event){
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
var id = $(this).closest('tr').data('id');
|
self.resetTip();
|
||||||
|
self.removeItemAll($(this).closest('tr').data('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Increment item
|
||||||
|
$('.js-cart-item-plus').off().on('click', function(event){
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var $val = $(this).parents('.input-group').find('.js-cart-item-count'),
|
||||||
|
val = parseInt($val.val() || $val.data('prev')) + 1;
|
||||||
|
|
||||||
|
$val.val(val);
|
||||||
|
$val.data('prev', val);
|
||||||
|
self.resetTip();
|
||||||
|
self.incrementItem($(this).closest('tr').data('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Decrement item
|
||||||
|
$('.js-cart-item-minus').off().on('click', function(event){
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var $val = $(this).parents('.input-group').find('.js-cart-item-count'),
|
||||||
|
id = $(this).closest('tr').data('id'),
|
||||||
|
val = parseInt($val.val() || $val.data('prev')) - 1;
|
||||||
|
|
||||||
|
self.resetTip();
|
||||||
|
|
||||||
|
if (val === 0) {
|
||||||
self.removeItemAll(id);
|
self.removeItemAll(id);
|
||||||
|
} else {
|
||||||
|
$val.val(val);
|
||||||
|
$val.data('prev', val);
|
||||||
|
self.decrementItem(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() {
|
Cart.prototype.bindEmptyCart = function() {
|
||||||
var $table = $('#js-cart-list').find('tbody');
|
var self = this;
|
||||||
|
|
||||||
$table.html('<tr><td colspan="4">The cart is empty.</td></tr>');
|
this.emptyCartToggle();
|
||||||
|
|
||||||
|
this.$destroy.click(function(event){
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
self.destroy();
|
||||||
|
self.emptyCartToggle();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart.prototype.formatCurrency = function(amount, currency, symbol) {
|
Cart.prototype.emptyCartToggle = function() {
|
||||||
|
if (this.content.length > 0 || this.getCustomAmount()) {
|
||||||
|
this.$destroy.show();
|
||||||
|
this.$confirm.removeAttr('disabled');
|
||||||
|
} else {
|
||||||
|
this.$destroy.hide();
|
||||||
|
this.$confirm.attr('disabled', 'disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Currencies and numbers
|
||||||
|
*/
|
||||||
|
Cart.prototype.formatCurrency = function(amount) {
|
||||||
var amt = '',
|
var amt = '',
|
||||||
thousandsSep = '',
|
thousandsSep = '',
|
||||||
decimalSep = ''
|
decimalSep = ''
|
||||||
@@ -252,12 +524,14 @@ Cart.prototype.formatCurrency = function(amount, currency, symbol) {
|
|||||||
if (srvModel.currencyInfo.symbolSpace) {
|
if (srvModel.currencyInfo.symbolSpace) {
|
||||||
prefix = prefix + ' ';
|
prefix = prefix + ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
postfix = srvModel.currencyInfo.currencySymbol;
|
postfix = srvModel.currencyInfo.currencySymbol;
|
||||||
if (srvModel.currencyInfo.symbolSpace) {
|
if (srvModel.currencyInfo.symbolSpace) {
|
||||||
postfix = ' ' + postfix;
|
postfix = ' ' + postfix;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
thousandsSep = srvModel.currencyInfo.thousandSeparator;
|
thousandsSep = srvModel.currencyInfo.thousandSeparator;
|
||||||
decimalSep = srvModel.currencyInfo.decimalSeparator;
|
decimalSep = srvModel.currencyInfo.decimalSeparator;
|
||||||
@@ -267,8 +541,9 @@ Cart.prototype.formatCurrency = function(amount, currency, symbol) {
|
|||||||
var splittedAmount = amt.split('.');
|
var splittedAmount = amt.split('.');
|
||||||
amt = (splittedAmount[0] + '.').replace(/(\d)(?=(\d{3})+\.)/g, '$1' + thousandsSep);
|
amt = (splittedAmount[0] + '.').replace(/(\d)(?=(\d{3})+\.)/g, '$1' + thousandsSep);
|
||||||
amt = amt.substr(0, amt.length - 1);
|
amt = amt.substr(0, amt.length - 1);
|
||||||
if(splittedAmount.length == 2)
|
if(splittedAmount.length == 2) {
|
||||||
amt = amt + '.' + splittedAmount[1];
|
amt = amt + decimalSep + splittedAmount[1];
|
||||||
|
}
|
||||||
if (srvModel.currencyInfo.divisibility !== 0) {
|
if (srvModel.currencyInfo.divisibility !== 0) {
|
||||||
amt[amt.length - srvModel.currencyInfo.divisibility - 1] = decimalSep;
|
amt[amt.length - srvModel.currencyInfo.divisibility - 1] = decimalSep;
|
||||||
}
|
}
|
||||||
@@ -277,6 +552,10 @@ Cart.prototype.formatCurrency = function(amount, currency, symbol) {
|
|||||||
return amt;
|
return amt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cart.prototype.toNumber = function(num) {
|
||||||
|
return (num * 1) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
Cart.prototype.toCents = function(num) {
|
Cart.prototype.toCents = function(num) {
|
||||||
return num * Math.pow(10, srvModel.currencyInfo.divisibility);
|
return num * Math.pow(10, srvModel.currencyInfo.divisibility);
|
||||||
}
|
}
|
||||||
@@ -285,27 +564,108 @@ Cart.prototype.fromCents = function(num) {
|
|||||||
return num / Math.pow(10, srvModel.currencyInfo.divisibility);
|
return num / Math.pow(10, srvModel.currencyInfo.divisibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart.prototype.getStorageKey = function () { return ('cart' + srvModel.appId + srvModel.currencyCode); }
|
Cart.prototype.percentage = function(amount, percentage) {
|
||||||
|
return this.fromCents((amount / 100) * percentage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Storage
|
||||||
|
*/
|
||||||
|
Cart.prototype.getStorageKey = function (name) {
|
||||||
|
return (name + srvModel.appId + srvModel.currencyCode);
|
||||||
|
}
|
||||||
|
|
||||||
Cart.prototype.saveLocalStorage = function() {
|
Cart.prototype.saveLocalStorage = function() {
|
||||||
localStorage.setItem(this.getStorageKey(), JSON.stringify(this.content));
|
localStorage.setItem(this.getStorageKey('cart'), JSON.stringify(this.content));
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart.prototype.loadLocalStorage = function() {
|
Cart.prototype.loadLocalStorage = function() {
|
||||||
this.content = $.parseJSON(localStorage.getItem(this.getStorageKey())) || [];
|
this.content = $.parseJSON(localStorage.getItem(this.getStorageKey('cart'))) || [];
|
||||||
|
|
||||||
// Get number of cart items
|
// Get number of cart items
|
||||||
for (var key in this.content) {
|
for (var key in this.content) {
|
||||||
if (this.content.hasOwnProperty(key) && typeof this.content[key] != 'undefined' && this.content[key] != null) {
|
if (this.content.hasOwnProperty(key) && typeof this.content[key] != 'undefined' && this.content[key] != null) {
|
||||||
this.items += this.content[key].count;
|
this.items += this.content[key].count;
|
||||||
|
|
||||||
|
// Delete the disabled flag if any
|
||||||
|
delete(this.content[key].disabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.discount = localStorage.getItem(this.getStorageKey('cartDiscount'));
|
||||||
|
this.customAmount = localStorage.getItem(this.getStorageKey('cartCustomAmount'));
|
||||||
|
this.tip = localStorage.getItem(this.getStorageKey('cartTip'));
|
||||||
}
|
}
|
||||||
|
|
||||||
Cart.prototype.destroy = function() {
|
Cart.prototype.destroy = function(keepAmount) {
|
||||||
localStorage.removeItem(this.getStorageKey());
|
this.resetDiscount();
|
||||||
|
this.resetTip();
|
||||||
|
this.resetCustomAmount();
|
||||||
|
|
||||||
|
// When form is sent
|
||||||
|
if (keepAmount) {
|
||||||
this.content = [];
|
this.content = [];
|
||||||
this.items = 0;
|
this.items = 0;
|
||||||
this.totalAmount = 0;
|
} else {
|
||||||
this.tip = 0;
|
this.removeItemAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem(this.getStorageKey('cart'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* jQuery helpers
|
||||||
|
*/
|
||||||
|
$.fn.inputAmount = function(obj, type) {
|
||||||
|
$(this).off().on('input', function(event){
|
||||||
|
var val = obj.toNumber($(this).val());
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'customAmount':
|
||||||
|
obj.setCustomAmount(val);
|
||||||
|
obj.updateDiscount();
|
||||||
|
obj.updateSummaryProducts();
|
||||||
|
obj.updateTotal();
|
||||||
|
obj.resetTip();
|
||||||
|
break;
|
||||||
|
case 'discount':
|
||||||
|
obj.setDiscount(val);
|
||||||
|
obj.updateDiscount();
|
||||||
|
obj.updateSummaryProducts();
|
||||||
|
obj.updateTotal();
|
||||||
|
obj.resetTip();
|
||||||
|
break;
|
||||||
|
case 'tip':
|
||||||
|
obj.setTip(val);
|
||||||
|
obj.updateTip();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.updateSummaryTotal();
|
||||||
|
obj.updateAmount();
|
||||||
|
obj.emptyCartToggle();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.fn.removeAmount = function(obj, type) {
|
||||||
|
$(this).off().on('click', function(event){
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'customAmount':
|
||||||
|
obj.resetCustomAmount();
|
||||||
|
obj.updateSummaryProducts();
|
||||||
|
break;
|
||||||
|
case 'discount':
|
||||||
|
obj.resetDiscount();
|
||||||
|
obj.updateSummaryProducts();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.resetTip();
|
||||||
|
obj.updateTotal();
|
||||||
|
obj.updateSummaryTotal();
|
||||||
|
obj.emptyCartToggle();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
BIN
BTCPayServer/wwwroot/img/icons/icon-128x128.png
Executable file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-144x144.png
Executable file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-152x152.png
Executable file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-192x192.png
Executable file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-384x384.png
Executable file
|
After Width: | Height: | Size: 10 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-512x512.png
Executable file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-72x72.png
Executable file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-96x96.png
Executable file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
BTCPayServer/wwwroot/img/splash.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
@@ -31,15 +31,17 @@
|
|||||||
showFeedback("no-ledger-info");
|
showFeedback("no-ledger-info");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$("#DerivationScheme").change(function () {
|
||||||
|
$("#KeyPath").val("");
|
||||||
|
});
|
||||||
$(".ledger-info-recommended").on("click", function (elem) {
|
$(".ledger-info-recommended").on("click", function (elem) {
|
||||||
elem.preventDefault();
|
elem.preventDefault();
|
||||||
|
|
||||||
showFeedback("ledger-validate");
|
showFeedback("ledger-validate");
|
||||||
|
|
||||||
var account = elem.currentTarget.getAttribute("data-ledgeraccount");
|
var keypath = elem.currentTarget.getAttribute("data-ledgerkeypath");
|
||||||
var cryptoCode = GetSelectedCryptoCode();
|
var cryptoCode = GetSelectedCryptoCode();
|
||||||
bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode + "&account=" + account)
|
bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode + "&keypath=" + keypath)
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
if (cryptoCode !== GetSelectedCryptoCode())
|
if (cryptoCode !== GetSelectedCryptoCode())
|
||||||
return;
|
return;
|
||||||
@@ -48,33 +50,12 @@
|
|||||||
|
|
||||||
$("#DerivationScheme").val(result.extPubKey);
|
$("#DerivationScheme").val(result.extPubKey);
|
||||||
$("#DerivationSchemeFormat").val("BTCPay");
|
$("#DerivationSchemeFormat").val("BTCPay");
|
||||||
|
$("#KeyPath").val(keypath);
|
||||||
})
|
})
|
||||||
.catch(function (reason) { Write('check', 'error', reason); });
|
.catch(function (reason) { Write('check', 'error', reason); });
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
var updateInfo = function () {
|
|
||||||
if (!ledgerDetected)
|
|
||||||
return false;
|
|
||||||
var cryptoCode = GetSelectedCryptoCode();
|
|
||||||
bridge.sendCommand("getxpub", "cryptoCode=" + cryptoCode)
|
|
||||||
.catch(function (reason) { Write('check', 'error', reason); })
|
|
||||||
.then(function (result) {
|
|
||||||
if (!result)
|
|
||||||
return;
|
|
||||||
if (cryptoCode !== GetSelectedCryptoCode())
|
|
||||||
return;
|
|
||||||
if (result.error) {
|
|
||||||
Write('check', 'error', result.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write('check', 'success', 'This store is configured to use your ledger');
|
|
||||||
showFeedback("ledger-info");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
bridge.isSupported()
|
bridge.isSupported()
|
||||||
.then(function (supported) {
|
.then(function (supported) {
|
||||||
if (!supported) {
|
if (!supported) {
|
||||||
@@ -95,7 +76,7 @@
|
|||||||
} else {
|
} else {
|
||||||
Write('hw', 'success', 'Ledger detected');
|
Write('hw', 'success', 'Ledger detected');
|
||||||
ledgerDetected = true;
|
ledgerDetected = true;
|
||||||
updateInfo();
|
showFeedback("ledger-info");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,21 +41,7 @@
|
|||||||
var updateInfo = function () {
|
var updateInfo = function () {
|
||||||
if (!ledgerDetected)
|
if (!ledgerDetected)
|
||||||
return false;
|
return false;
|
||||||
$(".crypto-info").css("display", "none");
|
|
||||||
bridge.sendCommand("getinfo", "cryptoCode=" + cryptoCode)
|
|
||||||
.catch(function (reason) { Write('check', 'error', reason); })
|
|
||||||
.then(function (result) {
|
|
||||||
if (!result)
|
|
||||||
return;
|
|
||||||
if (result.error) {
|
|
||||||
Write('check', 'error', result.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write('check', 'success', 'This store is configured to use your ledger');
|
|
||||||
$(".crypto-info").css("display", "block");
|
$(".crypto-info").css("display", "block");
|
||||||
|
|
||||||
|
|
||||||
var args = "";
|
var args = "";
|
||||||
args += "cryptoCode=" + cryptoCode;
|
args += "cryptoCode=" + cryptoCode;
|
||||||
args += "&destination=" + destination;
|
args += "&destination=" + destination;
|
||||||
@@ -87,8 +73,6 @@
|
|||||||
window.location.replace(successCallback + "?txid=" + result.transactionId);
|
window.location.replace(successCallback + "?txid=" + result.transactionId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
bridge.isSupported()
|
bridge.isSupported()
|
||||||
|
|||||||
50
BTCPayServer/wwwroot/manifest.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "BTCPayServer Point of Sale",
|
||||||
|
"short_name": "BTCPay POS",
|
||||||
|
"theme_color": "#1e7a44",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "img/icons/icon-72x72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "img/icons/icon-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "img/icons/icon-128x128.png",
|
||||||
|
"sizes": "128x128",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "img/icons/icon-144x144.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "img/icons/icon-152x152.png",
|
||||||
|
"sizes": "152x152",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "img/icons/icon-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "img/icons/icon-384x384.png",
|
||||||
|
"sizes": "384x384",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "img/icons/icon-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"splash_pages": null
|
||||||
|
}
|
||||||