Compare commits

...

57 Commits

Author SHA1 Message Date
5eac84d3a3 Fix bug: bitcoinAddress field of Invoice was showing ligthning BOLT11 address 2018-03-26 12:38:14 +09:00
a0a2ab6fcd update publish-docker 2018-03-26 11:54:10 +09:00
7730ead8e4 bump 2018-03-26 09:49:03 +09:00
8eee0dd14c Merge pull request #81 from pajasevi/lang-CS
Czech language support
2018-03-26 09:46:59 +09:00
7dd88d8d8f Can send max invoice value for lightning payments 2018-03-26 01:57:44 +09:00
56d1d3e645 Czech language support 2018-03-25 17:17:38 +02:00
c2308675b2 Better doc on the StoreUsers page 2018-03-25 14:09:40 +09:00
cb866a1c05 Make JP a bit shorter 2018-03-24 23:55:23 +09:00
95290e8331 Disable convertir tab for all lightning payments 2018-03-24 23:43:02 +09:00
f5e62c775b Remove BTC mentions from ConversionTab_Lightning 2018-03-24 23:37:18 +09:00
f533309b49 plug japanese translation 2018-03-24 23:02:41 +09:00
d1c70a7cb3 Merge pull request #78 from junderw/fixJA
Fix Japanese
2018-03-24 22:58:26 +09:00
2f8590ca7a Fix Japanese 2018-03-24 22:06:03 +09:00
08badbde56 bump 2018-03-24 20:40:48 +09:00
8e38da80e0 Better UX to set the xpub correctly 2018-03-24 20:40:26 +09:00
cd2e3350b0 Japanese support WIP 2018-03-24 20:15:42 +09:00
a0d2790491 Activate spanish 2018-03-24 14:35:49 +09:00
8ca99e5635 Merge branch 'spanish-language' of https://github.com/marcosrdz/btcpayserver into marcosrdz-spanish-language 2018-03-24 14:35:05 +09:00
5a2563ca7f Spanish translation 2018-03-24 01:15:43 -04:00
a23cd28531 Merge pull request #76 from LinoxBE/french-translation-fix
Fix French translation
2018-03-24 14:12:18 +09:00
58a967b59e Fix French translation 2018-03-23 19:41:27 +01:00
9bf0c20198 bump 2018-03-24 02:40:05 +09:00
6b7ac0e000 Merge pull request #75 from lepipele/dev-i18n
Fixing problems on expiry page for different languages
2018-03-24 02:39:37 +09:00
188c0a9a86 Fixing third line in expiry translation for Dutch 2018-03-23 12:33:57 -05:00
c49479c8ad Styling changes to make expiry text fit in different languages 2018-03-23 12:32:00 -05:00
2072b6e136 Fix english selection when the store has not set default language 2018-03-24 01:58:11 +09:00
08d82390b0 Remove language not yet translated 2018-03-24 01:15:28 +09:00
b845a545e2 Plug Dutch to LanguageService 2018-03-24 01:10:19 +09:00
db958b2401 Add Dutch 2018-03-24 01:06:00 +09:00
7266420eec Plug portugeuse to language service 2018-03-24 01:04:05 +09:00
f36fbe7a76 Merge branch 'patch-1' of https://github.com/rsandrade/btcpayserver into rsandrade-patch-1 2018-03-24 00:59:38 +09:00
8e279b110c use full language code 2018-03-24 00:58:37 +09:00
d626870e46 Create pt.js 2018-03-23 10:48:03 -03:00
df49b094d5 French translation 2018-03-23 18:04:44 +09:00
7d17bf7f2a Can set store default language 2018-03-23 17:27:48 +09:00
e51f3dd1ae Merge branch 'lepipele-dev-i18n' 2018-03-23 16:44:07 +09:00
b810b88c6c Merge branch 'dev-i18n' of https://github.com/lepipele/btcpayserver into lepipele-dev-i18n 2018-03-23 16:39:44 +09:00
39b34ff4ed Can invite user to manage your store 2018-03-23 16:24:57 +09:00
f72fd63113 Merge remote-tracking branch 'source/master' into dev-i18n
# Conflicts:
#	BTCPayServer/Properties/launchSettings.json
2018-03-22 23:35:45 -05:00
97eedc2c9f German test translation completed 2018-03-22 23:33:47 -05:00
db222c53e3 Faster language selection on page load 2018-03-22 23:16:38 -05:00
61e919b88d Removing flicker on invoice load 2018-03-22 23:15:54 -05:00
d14040c142 Foundation for future translations of supported languages 2018-03-22 23:04:42 -05:00
13a3a581d8 Detecting language from querystring 2018-03-22 23:02:53 -05:00
f6dbae1cef Extracting translation strings from core.js 2018-03-22 22:48:16 -05:00
ccbcda86ac Binding translation for Your email placeholder 2018-03-22 13:26:10 -05:00
b74e8cf756 Translating Invoice expired state 2018-03-22 12:57:51 -05:00
8f8266f15d Extracting complex Checkout body structure for easier navigation 2018-03-22 12:08:49 -05:00
ab8d3f5813 Extracting strings for translation 2018-03-22 12:02:55 -05:00
08220dbea5 Reorganizing file structure for i18n support 2018-03-22 11:12:15 -05:00
c3beca27be Bugfix: Pressing enter no longer reloads page when providing email 2018-03-22 00:21:26 -05:00
28b820241f Integrating dropdown for language selection
So hard to find good jquery alternative that drops up
2018-03-22 00:07:30 -05:00
e985224092 Bugfixing IExplorer bug
Doesn't allow i18n without key:value format
2018-03-22 00:06:56 -05:00
59afebaa57 Translating few items and testing how it works 2018-03-20 14:30:37 -05:00
3e06e45054 Cleanup, removing outdated classes and spinner 2018-03-20 14:19:10 -05:00
fe55acb268 Reorganizing Checkout page resources and adding i18n libs 2018-03-20 13:24:11 -05:00
e656813844 Html cleanup, removing comments and extracting bp-spinner 2018-03-20 11:51:52 -05:00
61 changed files with 4951 additions and 926 deletions

View File

@ -56,10 +56,12 @@ namespace BTCPayServer.Tests
public async Task<StoresController> CreateStoreAsync()
{
var store = parent.PayTester.GetController<StoresController>(UserId);
var store = parent.PayTester.GetController<UserStoresController>(UserId);
await store.CreateStore(new CreateStoreViewModel() { Name = "Test Store" });
StoreId = store.CreatedStoreId;
return store;
var store2 = parent.PayTester.GetController<StoresController>(UserId);
store2.CreatedStoreId = store.CreatedStoreId;
return store2;
}
public BTCPayNetwork SupportedNetwork { get; set; }
@ -80,7 +82,6 @@ namespace BTCPayServer.Tests
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
{
DerivationSchemeFormat = "BTCPay",
DerivationScheme = DerivationScheme.ToString(),
Confirmation = true
}, cryptoCode);

View File

@ -806,6 +806,64 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanParseCurrencyValue()
{
Assert.True(CurrencyValue.TryParse("1.50USD", out var result));
Assert.Equal("1.50 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.50 USD", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.50 usd", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1 usd", out result));
Assert.Equal("1 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1usd", out result));
Assert.Equal("1 USD", result.ToString());
Assert.True(CurrencyValue.TryParse("1.501 usd", out result));
Assert.Equal("1.50 USD", result.ToString());
Assert.False(CurrencyValue.TryParse("1.501 WTFF", out result));
Assert.False(CurrencyValue.TryParse("1,501 usd", out result));
Assert.False(CurrencyValue.TryParse("1.501", out result));
}
[Fact]
public void CanParseDerivationScheme()
{
var parser = new DerivationSchemeParser(Network.TestNet, NBXplorer.ChainType.Test);
NBXplorer.DerivationStrategy.DerivationStrategyBase result;
// Passing electrum stuff
// Native
result = parser.Parse("zpub6nL6PUGurpU3DfPDSZaRS6WshpbNc9ctCFFzrCn54cssnheM31SZJZUcFHKtjJJNhAueMbh6ptFMfy1aeiMQJr3RJ4DDt1hAPx7sMTKV48t");
Assert.Equal("tpubD93CJNkmGjLXnsBqE2zGDqfEh1Q8iJ8wueordy3SeWt1RngbbuxXCsqASuVWFywmfoCwUE1rSfNJbaH4cBNcbp8WcyZgPiiRSTazLGL8U9w", result.ToString());
// P2SH
result = parser.Parse("ypub6QqdH2c5z79681jUgdxjGJzGW9zpL4ryPCuhtZE4GpvrJoZqM823XQN6iSQeVbbbp2uCRQ9UgpeMcwiyV6qjvxTWVcxDn2XEAnioMUwsrQ5");
Assert.Equal("tpubD6NzVbkrYhZ4YWjDJUACG9E8fJx2NqNY1iynTiPKEjJrzzRKAgha3nNnwGXr2BtvCJKJHW4nmG7rRqc2AGGy2AECgt16seMyV2FZivUmaJg-[p2sh]", result.ToString());
result = parser.Parse("xpub661MyMwAqRbcGeVGU5e5KBcau1HHEUGf9Wr7k4FyLa8yRPNQrrVa7Ndrgg8Afbe2UYXMSL6tJBFd2JewwWASsePPLjkcJFL1tTVEs3UQ23X");
Assert.Equal("tpubD6NzVbkrYhZ4YSg7vGdAX6wxE8NwDrmih9SR6cK7gUtsAg37w5LfFpJgviCxC6bGGT4G3uckqH5fiV9ZLN1gm5qgQLVuymzFUR5ed7U7ksu-[legacy]", result.ToString());
////////////////
result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o");
Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o", result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("tb1q4s33amqm8l7a07zdxcunqnn3gcsjcfz3xc573l", parser.Network).ScriptPubKey;
result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o");
Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o", result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o");
Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o-[p2sh]", result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("mwD8bHS65cdgUf6rZUUSoVhi3wNQFu1Nfi", parser.Network).ScriptPubKey;
result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o");
Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o-[legacy]", result.ToString());
parser.HintScriptPubKey = BitcoinAddress.Create("2N2humNio3YTApSfY6VztQ9hQwDnhDvaqFQ", parser.Network).ScriptPubKey;
result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o-[legacy]");
Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o-[p2sh]", result.ToString());
result = parser.Parse("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o");
Assert.Equal("tpubD6NzVbkrYhZ4Wc65tjhmcKdWFauAo7bGLRTxvggygkNyp6SMGutJp7iociwsinU33jyNBp1J9j2hJH5yQsayfiS3LEU2ZqXodAcnaygra8o-[p2sh]", result.ToString());
}
[Fact]
public void InvoiceFlowThroughDifferentStatesCorrectly()
{

View File

@ -2,18 +2,22 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<Version>1.0.1.58</Version>
<Version>1.0.1.66</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Build\dockerfiles\**" />
<Compile Remove="wwwroot\bundles\jqueryvalidate\**" />
<Compile Remove="wwwroot\vendor\jquery-nice-select\**" />
<Content Remove="Build\dockerfiles\**" />
<Content Remove="wwwroot\bundles\jqueryvalidate\**" />
<Content Remove="wwwroot\vendor\jquery-nice-select\**" />
<EmbeddedResource Remove="Build\dockerfiles\**" />
<EmbeddedResource Remove="wwwroot\bundles\jqueryvalidate\**" />
<EmbeddedResource Remove="wwwroot\vendor\jquery-nice-select\**" />
<None Remove="Build\dockerfiles\**" />
<None Remove="wwwroot\bundles\jqueryvalidate\**" />
<None Remove="wwwroot\vendor\jquery-nice-select\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Currencies.txt" />
@ -56,7 +60,7 @@
</ItemGroup>
<ItemGroup>
<None Include="wwwroot\js\core.js" />
<None Include="wwwroot\js\checkout\core.js" />
<None Include="wwwroot\js\creative.js" />
<None Include="wwwroot\js\creative.min.js" />
<None Include="wwwroot\js\site.js" />

View File

@ -201,16 +201,18 @@ namespace BTCPayServer.Controllers
var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
var dto = invoice.EntityToDTO(_NetworkProvider);
var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId);
var storeBlob = store.GetStoreBlob();
var currency = invoice.ProductInformation.Currency;
var accounting = paymentMethod.Calculate();
var model = new PaymentModel()
{
CryptoCode = network.CryptoCode,
PaymentMethodId = paymentMethodId.ToString(),
IsLightning = paymentMethodId.PaymentType == PaymentTypes.LightningLike,
ServerUrl = HttpContext.Request.GetAbsoluteRoot(),
OrderId = invoice.OrderId,
InvoiceId = invoice.Id,
DefaultLang = storeBlob.DefaultLang ?? "en-US",
BtcAddress = paymentMethodDetails.GetPaymentDestination(),
OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(),
BtcDue = accounting.Due.ToString(),
@ -233,7 +235,7 @@ namespace BTCPayServer.Controllers
Status = invoice.Status,
CryptoImage = "/" + GetImage(paymentMethodId, network),
NetworkFeeDescription = $"{accounting.TxRequired} transaction{(accounting.TxRequired > 1 ? "s" : "")} x {paymentMethodDetails.GetTxFee()} {network.CryptoCode}",
AllowCoinConversion = store.GetStoreBlob().AllowCoinConversion,
AllowCoinConversion = storeBlob.AllowCoinConversion,
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
.Where(i => i.Network != null)
.Select(kv => new PaymentModel.AvailableCrypto()
@ -417,7 +419,7 @@ namespace BTCPayServer.Controllers
if (stores.Count() == 0)
{
StatusMessage = "Error: You need to create at least one store before creating a transaction";
return RedirectToAction(nameof(StoresController.ListStores), "Stores");
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
return View(new CreateInvoiceModel() { Stores = stores });
}
@ -434,9 +436,18 @@ namespace BTCPayServer.Controllers
return View(model);
}
var store = await _StoreRepository.FindStore(model.StoreId, GetUserId());
StatusMessage = null;
if (store.Role != StoreRoles.Owner)
{
StatusMessage = "Error: You need to be owner of this store to create an invoice";
}
if (store.GetSupportedPaymentMethods(_NetworkProvider).Count() == 0)
{
StatusMessage = "Error: You need to configure the derivation scheme in order to create an invoice";
}
if(StatusMessage != null)
{
return RedirectToAction(nameof(StoresController.UpdateStore), "Stores", new
{
storeId = store.Id

View File

@ -86,15 +86,15 @@ namespace BTCPayServer.Controllers
Network: _NetworkProvider.GetNetwork(c.PaymentId.CryptoCode),
IsAvailable: Task.FromResult(false)))
.Where(c => c.Network != null)
.Select(c =>
.Select(c =>
{
c.IsAvailable = c.Handler.IsAvailable(c.SupportedPaymentMethod, c.Network);
return c;
})
.ToList();
foreach(var supportedPaymentMethod in supportedPaymentMethods.ToList())
foreach (var supportedPaymentMethod in supportedPaymentMethods.ToList())
{
if(!await supportedPaymentMethod.IsAvailable)
if (!await supportedPaymentMethod.IsAvailable)
{
supportedPaymentMethods.Remove(supportedPaymentMethod);
}
@ -105,7 +105,6 @@ namespace BTCPayServer.Controllers
{
InvoiceTime = DateTimeOffset.UtcNow
};
entity.SetSupportedPaymentMethods(supportedPaymentMethods.Select(s => s.SupportedPaymentMethod));
var storeBlob = store.GetStoreBlob();
Uri notificationUri = Uri.IsWellFormedUriString(invoice.NotificationURL, UriKind.Absolute) ? new Uri(invoice.NotificationURL, UriKind.Absolute) : null;
@ -139,6 +138,7 @@ namespace BTCPayServer.Controllers
var rate = await storeBlob.ApplyRateRules(o.Network, _RateProviders.GetRateProvider(o.Network, false)).GetRateAsync(invoice.Currency);
PaymentMethod paymentMethod = new PaymentMethod();
paymentMethod.ParentEntity = entity;
paymentMethod.Network = o.Network;
paymentMethod.SetId(o.SupportedPaymentMethod.PaymentId);
paymentMethod.Rate = rate;
var paymentDetails = await o.Handler.CreatePaymentMethodDetails(o.SupportedPaymentMethod, paymentMethod, o.Network);
@ -153,13 +153,44 @@ namespace BTCPayServer.Controllers
entity.DepositAddress = paymentMethod.DepositAddress;
}
#pragma warning restore CS0618
return paymentMethod;
return (SupportedPaymentMethod: o.SupportedPaymentMethod, PaymentMethod: paymentMethod);
});
var paymentMethods = new PaymentMethodDictionary();
List<ISupportedPaymentMethod> supported = new List<ISupportedPaymentMethod>();
foreach (var method in methods)
{
paymentMethods.Add(await method);
var o = await method;
// Check if Lightning Max value is exceeded
if(o.SupportedPaymentMethod.PaymentId.PaymentType == PaymentTypes.LightningLike &&
storeBlob.LightningMaxValue != null)
{
var lightningMaxValue = storeBlob.LightningMaxValue;
decimal rate = 0.0m;
if (lightningMaxValue.Currency == invoice.Currency)
rate = o.PaymentMethod.Rate;
else
rate = await storeBlob.ApplyRateRules(o.PaymentMethod.Network, _RateProviders.GetRateProvider(o.PaymentMethod.Network, false)).GetRateAsync(lightningMaxValue.Currency);
var lightningMaxValueCrypto = Money.Coins(lightningMaxValue.Value / rate);
if (o.PaymentMethod.Calculate().Due > lightningMaxValueCrypto)
{
continue;
}
}
///////////////
supported.Add(o.SupportedPaymentMethod);
paymentMethods.Add(o.PaymentMethod);
}
if(supported.Count == 0)
{
throw new BitpayHttpException(400, "No derivation strategy are available now for this store");
}
entity.SetSupportedPaymentMethods(supported);
entity.SetPaymentMethods(paymentMethods);
#pragma warning disable CS0618
// Legacy Bitpay clients expect information for BTC information, even if the store do not support it
var legacyBTCisSet = paymentMethods.Any(p => p.GetId().IsBTCOnChain);
@ -177,8 +208,6 @@ namespace BTCPayServer.Controllers
}
#pragma warning restore CS0618
}
entity.SetPaymentMethods(paymentMethods);
entity.PosData = invoice.PosData;
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, _NetworkProvider);
_EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, "invoice_created"));

View File

@ -75,7 +75,7 @@ namespace BTCPayServer.Controllers
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, vm.DerivationSchemeFormat, network);
strategy = ParseDerivationStrategy(vm.DerivationScheme, null, network);
vm.DerivationScheme = strategy.ToString();
}
}
@ -86,8 +86,38 @@ namespace BTCPayServer.Controllers
return View(vm);
}
if (!vm.Confirmation && strategy != null)
return ShowAddresses(vm, strategy);
if (vm.Confirmation || strategy == null)
if (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress))
{
BitcoinAddress address = null;
try
{
address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork);
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address");
return ShowAddresses(vm, strategy);
}
try
{
strategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network);
}
catch
{
ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address");
return ShowAddresses(vm, strategy);
}
vm.HintAddress = "";
vm.StatusMessage = "Address successfully found, please verify that the rest is correct and click on \"Confirm\"";
ModelState.Remove(nameof(vm.HintAddress));
ModelState.Remove(nameof(vm.DerivationScheme));
return ShowAddresses(vm, strategy);
}
else
{
try
{
@ -105,23 +135,24 @@ namespace BTCPayServer.Controllers
StatusMessage = $"Derivation scheme for {network.CryptoCode} has been modified.";
return RedirectToAction(nameof(UpdateStore), new { storeId = storeId });
}
else
{
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(network.NBitcoinNetwork).ToString()));
}
}
vm.Confirmation = true;
return View(vm);
}
}
private IActionResult ShowAddresses(DerivationSchemeViewModel vm, DerivationStrategy strategy)
{
vm.DerivationScheme = strategy.DerivationStrategyBase.ToString();
if (!string.IsNullOrEmpty(vm.DerivationScheme))
{
var line = strategy.DerivationStrategyBase.GetLineFor(DerivationFeature.Deposit);
for (int i = 0; i < 10; i++)
{
var address = line.Derive((uint)i);
vm.AddressSamples.Add((DerivationStrategyBase.GetKeyPath(DerivationFeature.Deposit).Derive((uint)i).ToString(), address.ScriptPubKey.GetDestinationAddress(strategy.Network.NBitcoinNetwork).ToString()));
}
}
vm.Confirmation = true;
return View(vm);
}
public class GetInfoResult
@ -219,7 +250,8 @@ namespace BTCPayServer.Controllers
}
if (command == "getxpub")
{
var getxpubResult = await hw.GetExtPubKey(network, account); ;
var getxpubResult = await hw.GetExtPubKey(network, account);
;
getxpubResult.CoinType = (int)(getxpubResult.KeyPath.Indexes[1] - 0x80000000);
result = getxpubResult;
}
@ -240,13 +272,13 @@ namespace BTCPayServer.Controllers
if (command == "sendtoaddress")
{
if(!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
throw new Exception($"{network.CryptoCode}: not started or fully synched");
var strategy = GetDirectDerivationStrategy(store, network);
var strategyBase = GetDerivationStrategy(store, network);
var wallet = _WalletProvider.GetWallet(network);
var change = wallet.GetChangeAddressAsync(strategyBase);
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
var changeAddress = await change;
var transaction = await hw.SendToAddress(strategy, unspentCoins, network,

View File

@ -15,6 +15,7 @@ namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet]
[Route("{storeId}/lightning/{cryptoCode}")]
public async Task<IActionResult> AddLightningNode(string storeId, string cryptoCode)

View File

@ -27,10 +27,11 @@ namespace BTCPayServer.Controllers
{
[Route("stores")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[Authorize(Policy = "CanAccessStore")]
[Authorize(Policy = StorePolicies.OwnStore)]
[AutoValidateAntiforgeryToken]
public partial class StoresController : Controller
{
public string CreatedStoreId { get; set; }
public StoresController(
NBXplorerDashboard dashboard,
IServiceProvider serviceProvider,
@ -45,12 +46,14 @@ namespace BTCPayServer.Controllers
BTCPayNetworkProvider networkProvider,
ExplorerClientProvider explorerProvider,
IFeeProviderFactory feeRateProvider,
LanguageService langService,
IHostingEnvironment env)
{
_Dashboard = dashboard;
_Repo = repo;
_TokenRepository = tokenRepo;
_UserManager = userManager;
_LangService = langService;
_TokenController = tokenController;
_WalletProvider = walletProvider;
_Env = env;
@ -75,6 +78,7 @@ namespace BTCPayServer.Controllers
StoreRepository _Repo;
TokenRepository _TokenRepository;
UserManager<ApplicationUser> _UserManager;
private LanguageService _LangService;
IHostingEnvironment _Env;
[TempData]
@ -83,32 +87,6 @@ namespace BTCPayServer.Controllers
get; set;
}
[HttpGet]
[Route("create")]
public IActionResult CreateStore()
{
return View();
}
[HttpPost]
[Route("create")]
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
{
if (!ModelState.IsValid)
{
return View(vm);
}
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
CreatedStoreId = store.Id;
StatusMessage = "Store successfully created";
return RedirectToAction(nameof(ListStores));
}
public string CreatedStoreId
{
get; set;
}
[HttpGet]
[Route("{storeId}/wallet/{cryptoCode}")]
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
@ -128,76 +106,81 @@ namespace BTCPayServer.Controllers
}
[HttpGet]
public async Task<IActionResult> ListStores()
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(string storeId)
{
StoresViewModel result = new StoresViewModel();
result.StatusMessage = StatusMessage;
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
for (int i = 0; i < stores.Length; i++)
{
var store = stores[i];
result.Stores.Add(new StoresViewModel.StoreViewModel()
{
Id = store.Id,
Name = store.StoreName,
WebSite = store.StoreWebsite,
Balances = balances[i].Select(t => t.Result).ToArray()
});
}
return View(result);
StoreUsersViewModel vm = new StoreUsersViewModel();
await FillUsers(storeId, vm);
return View(vm);
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
var users = await _Repo.GetStoreUsers(storeId);
vm.StoreId = storeId;
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
Email = u.Email,
Id = u.Id,
Role = u.Role
}).ToList();
}
[HttpPost]
[Route("{storeId}/users")]
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
{
await FillUsers(storeId, vm);
if (!ModelState.IsValid)
{
return View(vm);
}
var user = await _UserManager.FindByEmailAsync(vm.Email);
if (user == null)
{
ModelState.AddModelError(nameof(vm.Email), "User not found");
return View(vm);
}
if (!StoreRoles.AllRoles.Contains(vm.Role))
{
ModelState.AddModelError(nameof(vm.Role), "Invalid role");
return View(vm);
}
if (!await _Repo.AddStoreUser(storeId, user.Id, vm.Role))
{
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
return View(vm);
}
StatusMessage = "User added successfully";
return RedirectToAction(nameof(StoreUsers));
}
[HttpGet]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId)
[Route("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
StoreUsersViewModel vm = new StoreUsersViewModel();
var store = await _Repo.FindStore(storeId, userId);
if (store == null)
return NotFound();
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Title = $"Remove store user",
Description = $"Are you sure to remove access to remove {store.Role} access to {user.Email}?",
Action = "Delete"
});
}
[HttpPost]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
[Route("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUserPost(string storeId, string userId)
{
var userId = GetUserId();
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
await _Repo.RemoveStore(storeId, userId);
StatusMessage = "Store removed successfully";
return RedirectToAction(nameof(ListStores));
await _Repo.RemoveStoreUser(storeId, userId);
StatusMessage = "User removed successfully";
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
}
[HttpGet]
@ -213,8 +196,10 @@ namespace BTCPayServer.Controllers
vm.Id = store.Id;
vm.StoreName = store.StoreName;
vm.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
vm.StoreWebsite = store.StoreWebsite;
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
vm.LightningMaxValue = storeBlob.LightningMaxValue?.ToString() ?? "";
vm.SpeedPolicy = store.SpeedPolicy;
AddPaymentMethods(store, vm);
vm.StatusMessage = StatusMessage;
@ -226,9 +211,10 @@ namespace BTCPayServer.Controllers
return View(vm);
}
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
{
var derivationByCryptoCode =
var derivationByCryptoCode =
store
.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
@ -263,6 +249,14 @@ namespace BTCPayServer.Controllers
[Route("{storeId}")]
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
{
CurrencyValue currencyValue = null;
if (!string.IsNullOrWhiteSpace(model.LightningMaxValue))
{
if(!CurrencyValue.TryParse(model.LightningMaxValue, out currencyValue))
{
ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid currency value");
}
}
if (!ModelState.IsValid)
{
return View(model);
@ -297,11 +291,14 @@ namespace BTCPayServer.Controllers
store.SetDefaultCrypto(model.DefaultCryptoCurrency);
}
model.SetCryptoCurrencies(_ExplorerProvider, model.DefaultCryptoCurrency);
model.SetLanguages(_LangService, model.DefaultLang);
var blob = store.GetStoreBlob();
blob.NetworkFeeDisabled = !model.NetworkFee;
blob.MonitoringExpiration = model.MonitoringExpiration;
blob.InvoiceExpiration = model.InvoiceExpiration;
blob.DefaultLang = model.DefaultLang;
blob.LightningMaxValue = currencyValue;
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
blob.PreferredExchange = model.PreferredExchange;
@ -339,41 +336,11 @@ namespace BTCPayServer.Controllers
});
}
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, string format, BTCPayNetwork network)
private DerivationStrategy ParseDerivationStrategy(string derivationScheme, Script hint, BTCPayNetwork network)
{
if (format == "Electrum")
{
//Unsupported Electrum
//var p2wsh_p2sh = 0x295b43fU;
//var p2wsh = 0x2aa7ed3U;
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
var standard = 0x0488b21eU;
electrumMapping.Add(standard, new[] { "legacy" });
var p2wpkh_p2sh = 0x049d7cb2U;
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
var p2wpkh = 0x4b24746U;
electrumMapping.Add(p2wpkh, Array.Empty<string>());
var data = Encoders.Base58Check.DecodeData(derivationScheme);
if (data.Length < 4)
throw new FormatException("data.Length < 4");
var prefix = Utils.ToUInt32(data, false);
if (!electrumMapping.TryGetValue(prefix, out string[] labels))
throw new FormatException("!electrumMapping.TryGetValue(prefix, out string[] labels)");
var standardPrefix = Utils.ToBytes(network.NBXplorerNetwork.DefaultSettings.ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
for (int i = 0; i < 4; i++)
data[i] = standardPrefix[i];
derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), network.NBitcoinNetwork).ToString();
foreach (var label in labels)
{
derivationScheme = derivationScheme + $"-[{label}]";
}
}
return new DerivationStrategy(new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(derivationScheme), network);
var parser = new DerivationSchemeParser(network.NBitcoinNetwork, network.DefaultSettings.ChainType);
parser.HintScriptPubKey = hint;
return new DerivationStrategy(parser.Parse(derivationScheme), network);
}
[HttpGet]
@ -403,15 +370,17 @@ namespace BTCPayServer.Controllers
return View(model);
}
model.Label = model.Label ?? String.Empty;
if (storeId == null) // Permissions are not checked by Policy if the storeId is not passed by url
storeId = model.StoreId ?? storeId;
var userId = GetUserId();
if (userId == null)
return Unauthorized();
var store = await _Repo.FindStore(storeId, userId);
if (store == null)
return Unauthorized();
if (store.Role != StoreRoles.Owner)
{
storeId = model.StoreId;
var userId = GetUserId();
if (userId == null)
return Unauthorized();
var store = await _Repo.FindStore(storeId, userId);
if (store == null)
return Unauthorized();
StatusMessage = "Error: You need to be owner of this store to request pairing codes";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
var tokenRequest = new TokenRequest()
@ -491,11 +460,13 @@ namespace BTCPayServer.Controllers
[Route("/api-access-request")]
public async Task<IActionResult> RequestPairing(string pairingCode, string selectedStore = null)
{
if (pairingCode == null)
return NotFound();
var pairing = await _TokenRepository.GetPairingAsync(pairingCode);
if (pairing == null)
{
StatusMessage = "Unknown pairing code";
return RedirectToAction(nameof(ListStores));
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
else
{
@ -517,7 +488,7 @@ namespace BTCPayServer.Controllers
}
[HttpPost]
[Route("api-access-request")]
[Route("/api-access-request")]
public async Task<IActionResult> Pair(string pairingCode, string selectedStore)
{
if (pairingCode == null)
@ -527,6 +498,12 @@ namespace BTCPayServer.Controllers
if (store == null || pairing == null)
return NotFound();
if (store.Role != StoreRoles.Owner)
{
StatusMessage = "Error: You can't approve a pairing without being owner of the store";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
var pairingResult = await _TokenRepository.PairWithStoreAsync(pairingCode, store.Id);
if (pairingResult == PairingResult.Complete || pairingResult == PairingResult.Partial)
{

View File

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Models;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services.Stores;
using BTCPayServer.Services.Wallets;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer.Controllers
{
[Route("stores")]
[Authorize(AuthenticationSchemes = "Identity.Application")]
[AutoValidateAntiforgeryToken]
public partial class UserStoresController : Controller
{
private StoreRepository _Repo;
private BTCPayNetworkProvider _NetworkProvider;
private UserManager<ApplicationUser> _UserManager;
private BTCPayWalletProvider _WalletProvider;
public UserStoresController(
UserManager<ApplicationUser> userManager,
BTCPayNetworkProvider networkProvider,
BTCPayWalletProvider walletProvider,
StoreRepository storeRepository)
{
_Repo = storeRepository;
_NetworkProvider = networkProvider;
_UserManager = userManager;
_WalletProvider = walletProvider;
}
[HttpGet]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStore(string storeId)
{
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Action = "Delete"
});
}
[HttpGet]
[Route("create")]
public IActionResult CreateStore()
{
return View();
}
public string CreatedStoreId
{
get; set;
}
[HttpPost]
[Route("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
var userId = GetUserId();
var store = await _Repo.FindStore(storeId, GetUserId());
if (store == null)
return NotFound();
await _Repo.RemoveStore(storeId, userId);
StatusMessage = "Store removed successfully";
return RedirectToAction(nameof(ListStores));
}
[TempData]
public string StatusMessage { get; set; }
[HttpGet]
public async Task<IActionResult> ListStores()
{
StoresViewModel result = new StoresViewModel();
var stores = await _Repo.GetStoresByUserId(GetUserId());
var balances = stores
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
.OfType<DerivationStrategy>()
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
DerivationStrategy: d.DerivationStrategyBase)))
.Where(_ => _.Wallet != null)
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
.ToArray();
await Task.WhenAll(balances.SelectMany(_ => _));
for (int i = 0; i < stores.Length; i++)
{
var store = stores[i];
result.Stores.Add(new StoresViewModel.StoreViewModel()
{
Id = store.Id,
Name = store.StoreName,
WebSite = store.StoreWebsite,
IsOwner = store.Role == StoreRoles.Owner,
Balances = store.Role == StoreRoles.Owner ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
});
}
return View(result);
}
[HttpPost]
[Route("create")]
public async Task<IActionResult> CreateStore(CreateStoreViewModel vm)
{
if (!ModelState.IsValid)
{
return View(vm);
}
var store = await _Repo.CreateStore(GetUserId(), vm.Name);
CreatedStoreId = store.Id;
StatusMessage = "Store successfully created";
return RedirectToAction(nameof(ListStores));
}
private static async Task<string> GetBalanceString((BTCPayWallet Wallet, DerivationStrategyBase DerivationStrategy) _)
{
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
{
try
{
return (await _.Wallet.GetBalance(_.DerivationStrategy, cts.Token)).ToString();
}
catch
{
return "--";
}
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BTCPayServer.Services.Rates;
namespace BTCPayServer
{
public class CurrencyValue
{
static Regex _Regex = new Regex("^([0-9]+(\\.[0-9]+)?)\\s*([a-zA-Z]+)$");
static CurrencyNameTable _CurrencyTable = new CurrencyNameTable();
public static bool TryParse(string str, out CurrencyValue value)
{
value = null;
var match = _Regex.Match(str);
if (!match.Success ||
!decimal.TryParse(match.Groups[1].Value, out var v))
return false;
var currency = match.Groups.Last().Value.ToUpperInvariant();
var currencyData = _CurrencyTable.GetCurrencyData(currency);
if (currencyData == null)
return false;
v = Math.Round(v, currencyData.Divisibility);
value = new CurrencyValue()
{
Value = v,
Currency = currency
};
return true;
}
public decimal Value { get; set; }
public string Currency { get; set; }
public override string ToString()
{
return Value.ToString(CultureInfo.InvariantCulture) + " " + Currency;
}
}
}

View File

@ -13,6 +13,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using BTCPayServer.Services.Rates;
using BTCPayServer.Payments;
using BTCPayServer.JsonConverters;
namespace BTCPayServer.Data
{
@ -217,6 +218,7 @@ namespace BTCPayServer.Data
{
get; set;
}
public string DefaultLang { get; set; }
[DefaultValue(60)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public int MonitoringExpiration
@ -253,6 +255,9 @@ namespace BTCPayServer.Data
public List<RateRule> RateRules { get; set; } = new List<RateRule>();
public string PreferredExchange { get; set; }
[JsonConverter(typeof(CurrencyValueJsonConverter))]
public CurrencyValue LightningMaxValue { get; set; }
public IRateProvider ApplyRateRules(BTCPayNetwork network, IRateProvider rateProvider)
{
if (!PreferredExchange.IsCoinAverage())

View File

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NBitcoin;
using NBitcoin.DataEncoders;
using NBXplorer;
using NBXplorer.DerivationStrategy;
namespace BTCPayServer
{
public class DerivationSchemeParser
{
public Network Network { get; set; }
public ChainType ChainType { get; set; }
public Script HintScriptPubKey { get; set; }
public DerivationSchemeParser(Network expectedNetwork, ChainType chainType)
{
Network = expectedNetwork;
ChainType = chainType;
}
public DerivationStrategyBase Parse(string str)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
str = str.Trim();
HashSet<string> hintedLabels = new HashSet<string>();
var hintDestination = HintScriptPubKey?.GetDestination();
if (hintDestination != null)
{
if (hintDestination is KeyId)
{
hintedLabels.Add("legacy");
}
if (hintDestination is ScriptId)
{
hintedLabels.Add("p2sh");
}
}
try
{
var result = new DerivationStrategyFactory(Network).Parse(str);
return FindMatch(hintedLabels, result);
}
catch
{
}
Dictionary<uint, string[]> electrumMapping = new Dictionary<uint, string[]>();
//Source https://github.com/spesmilo/electrum/blob/9edffd17542de5773e7284a8c8a2673c766bb3c3/lib/bitcoin.py
var standard = 0x0488b21eU;
electrumMapping.Add(standard, new[] { "legacy" });
var p2wpkh_p2sh = 0x049d7cb2U;
electrumMapping.Add(p2wpkh_p2sh, new string[] { "p2sh" });
var p2wpkh = 0x4b24746U;
electrumMapping.Add(p2wpkh, Array.Empty<string>());
var parts = str.Split('-');
for (int i = 0; i < parts.Length; i++)
{
if (IsLabel(parts[i]))
{
hintedLabels.Add(parts[i].Substring(1, parts[i].Length - 2).ToLowerInvariant());
continue;
}
try
{
var data = Encoders.Base58Check.DecodeData(parts[i]);
if (data.Length < 4)
continue;
var prefix = Utils.ToUInt32(data, false);
var standardPrefix = Utils.ToBytes(ChainType == NBXplorer.ChainType.Main ? 0x0488b21eU : 0x043587cf, false);
for (int ii = 0; ii < 4; ii++)
data[ii] = standardPrefix[ii];
var derivationScheme = new BitcoinExtPubKey(Encoders.Base58Check.EncodeData(data), Network).ToString();
electrumMapping.TryGetValue(prefix, out string[] labels);
if (labels != null)
{
foreach (var label in labels)
{
hintedLabels.Add(label.ToLowerInvariant());
}
}
parts[i] = derivationScheme;
}
catch { continue; }
}
if (hintDestination != null)
{
if (hintDestination is WitKeyId)
{
hintedLabels.Remove("legacy");
hintedLabels.Remove("p2sh");
}
}
str = string.Join('-', parts.Where(p => !IsLabel(p)));
foreach (var label in hintedLabels)
{
str = $"{str}-[{label}]";
}
return FindMatch(hintedLabels, new DerivationStrategyFactory(Network).Parse(str));
}
private DerivationStrategyBase FindMatch(HashSet<string> hintLabels, DerivationStrategyBase result)
{
var facto = new DerivationStrategyFactory(Network);
var firstKeyPath = new KeyPath("0/0");
if (HintScriptPubKey == null)
return result;
if (HintScriptPubKey == result.Derive(firstKeyPath).ScriptPubKey)
return result;
if (result is MultisigDerivationStrategy)
hintLabels.Add("keeporder");
var resultNoLabels = result.ToString();
resultNoLabels = string.Join('-', resultNoLabels.Split('-').Where(p => !IsLabel(p)));
foreach (var labels in ItemCombinations(hintLabels.ToList()))
{
var hinted = facto.Parse(resultNoLabels + '-' + string.Join('-', labels.Select(l=>$"[{l}]").ToArray()));
if (HintScriptPubKey == hinted.Derive(firstKeyPath).ScriptPubKey)
return hinted;
}
throw new FormatException("Could not find any match");
}
private static bool IsLabel(string v)
{
return v.StartsWith('[') && v.EndsWith(']');
}
/// <summary>
/// Method to create lists containing possible combinations of an input list of items. This is
/// basically copied from code by user "jaolho" on this thread:
/// http://stackoverflow.com/questions/7802822/all-possible-combinations-of-a-list-of-values
/// </summary>
/// <typeparam name="T">type of the items on the input list</typeparam>
/// <param name="inputList">list of items</param>
/// <param name="minimumItems">minimum number of items wanted in the generated combinations,
/// if zero the empty combination is included,
/// default is one</param>
/// <param name="maximumItems">maximum number of items wanted in the generated combinations,
/// default is no maximum limit</param>
/// <returns>list of lists for possible combinations of the input items</returns>
public static List<List<T>> ItemCombinations<T>(List<T> inputList, int minimumItems = 1,
int maximumItems = int.MaxValue)
{
int nonEmptyCombinations = (int)Math.Pow(2, inputList.Count) - 1;
List<List<T>> listOfLists = new List<List<T>>(nonEmptyCombinations + 1);
if (minimumItems == 0) // Optimize default case
listOfLists.Add(new List<T>());
for (int i = 1; i <= nonEmptyCombinations; i++)
{
List<T> thisCombination = new List<T>(inputList.Count);
for (int j = 0; j < inputList.Count; j++)
{
if ((i >> j & 1) == 1)
thisCombination.Add(inputList[j]);
}
if (thisCombination.Count >= minimumItems && thisCombination.Count <= maximumItems)
listOfLists.Add(thisCombination);
}
return listOfLists;
}
}
}

View File

@ -134,6 +134,7 @@ namespace BTCPayServer.Hosting
return opts.NetworkProvider;
});
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<BTCPayWalletProvider>();
@ -173,14 +174,14 @@ namespace BTCPayServer.Hosting
services.AddAuthorization(o =>
{
o.AddPolicy("CanAccessStore", builder =>
o.AddPolicy(StorePolicies.CanAccessStores, builder =>
{
builder.AddRequirements(new OwnStoreAuthorizationRequirement());
});
o.AddPolicy("OwnStore", builder =>
o.AddPolicy(StorePolicies.OwnStore, builder =>
{
builder.AddRequirements(new OwnStoreAuthorizationRequirement("Owner"));
builder.AddRequirements(new OwnStoreAuthorizationRequirement(StoreRoles.Owner));
});
});

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Reflection;
using Newtonsoft.Json;
using NBitcoin.JsonConverters;
namespace BTCPayServer.JsonConverters
{
public class CurrencyValueJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(CurrencyValue).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
}
Type longType = typeof(long).GetTypeInfo();
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return reader.TokenType == JsonToken.Null ? null :
CurrencyValue.TryParse((string)reader.Value, out var result) ? result :
throw new JsonObjectException("Invalid Currency value", reader);
}
catch (InvalidCastException)
{
throw new JsonObjectException("Invalid Currency value", reader);
}
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if(value != null)
writer.WriteValue(value.ToString());
}
}
}

View File

@ -13,7 +13,10 @@ namespace BTCPayServer.Models.InvoicingModels
public string CryptoImage { get; set; }
public string Link { get; set; }
}
public string DefaultLang { get; set; }
public List<AvailableCrypto> AvailableCryptos { get; set; } = new List<AvailableCrypto>();
public bool IsLightning { get; set; }
public string CryptoCode { get; set; }
public string ServerUrl { get; set; }
public string InvoiceId { get; set; }

View File

@ -9,20 +9,8 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class DerivationSchemeViewModel
{
class Format
{
public string Name { get; set; }
public string Value { get; set; }
}
public DerivationSchemeViewModel()
{
var btcPay = new Format { Name = "BTCPay", Value = "BTCPay" };
DerivationSchemeFormat = btcPay.Value;
DerivationSchemeFormats = new SelectList(new Format[]
{
btcPay,
new Format { Name = "Electrum", Value = "Electrum" },
}, nameof(btcPay.Value), nameof(btcPay.Name), btcPay);
}
public string DerivationScheme
{
@ -34,18 +22,12 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
} = new List<(string KeyPath, string Address)>();
[Display(Name = "Derivation Scheme format")]
public string DerivationSchemeFormat
{
get;
set;
}
public string CryptoCode { get; set; }
[Display(Name = "Hint address")]
public string HintAddress { get; set; }
public bool Confirmation { get; set; }
public SelectList DerivationSchemeFormats { get; set; }
public string ServerUrl { get; set; }
public string StatusMessage { get; internal set; }
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Models.StoreViewModels
{
public class StoreUsersViewModel
{
public class StoreUserViewModel
{
public string Email { get; set; }
public string Role { get; set; }
public string Id { get; set; }
}
public StoreUsersViewModel()
{
Role = StoreRoles.Guest;
}
[Required]
[EmailAddress]
public string Email { get; set; }
public string StoreId { get; set; }
public string Role { get; set; }
public List<StoreUserViewModel> Users { get; set; }
}
}

View File

@ -1,4 +1,5 @@
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Validations;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
@ -82,6 +83,11 @@ namespace BTCPayServer.Models.StoreViewModels
set;
}
[Display(Name = "Do not propose lightning payment if value of the invoice is above...")]
[MaxLength(20)]
public string LightningMaxValue { get; set; }
[Display(Name = "Consider the invoice confirmed when the payment transaction...")]
public SpeedPolicy SpeedPolicy
{
@ -105,9 +111,12 @@ namespace BTCPayServer.Models.StoreViewModels
get; set;
}
public SelectList CryptoCurrencies { get; set; }
public SelectList Languages { get; set; }
[Display(Name = "Default crypto currency on checkout")]
public string DefaultCryptoCurrency { get; set; }
[Display(Name = "Default language on checkout")]
public string DefaultLang { get; set; }
public class LightningNode
{
@ -122,9 +131,18 @@ namespace BTCPayServer.Models.StoreViewModels
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string defaultCrypto)
{
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Name == defaultCrypto) ?? choices.FirstOrDefault();
var chosen = choices.FirstOrDefault(f => f.Value == defaultCrypto) ?? choices.FirstOrDefault();
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultCryptoCurrency = chosen.Name;
}
public void SetLanguages(LanguageService langService, string defaultLang)
{
defaultLang = defaultLang ?? "en-US";
var choices = langService.GetLanguages().Select(o => new Format() { Name = o.DisplayName, Value = o.Code }).ToArray();
var chosen = choices.FirstOrDefault(f => f.Value == defaultLang) ?? choices.FirstOrDefault();
Languages = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
DefaultLang = chosen.Value;
}
}
}

View File

@ -8,14 +8,11 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class StoresViewModel
{
public string StatusMessage
{
get; set;
}
public List<StoreViewModel> Stores
{
get; set;
} = new List<StoreViewModel>();
public class StoreViewModel
{
public string Name
@ -32,6 +29,11 @@ namespace BTCPayServer.Models.StoreViewModels
{
get; set;
}
public bool IsOwner
{
get;
set;
}
public string[] Balances
{
get; set;

View File

@ -66,6 +66,10 @@ namespace BTCPayServer.Payments.Lightning
{
info = await client.GetInfo(cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested)
{
throw new Exception($"The lightning node did not replied in a timely maner");
}
catch (Exception ex)
{
throw new Exception($"Error while connecting to the API ({ex.Message})");

View File

@ -5,13 +5,13 @@
"launchBrowser": true,
"environmentVariables": {
"BTCPAY_NETWORK": "regtest",
"BTCPAY_BUNDLEJSCSS": "false",
"BTCPAY_LTCEXPLORERURL": "http://127.0.0.1:32838/",
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
"ASPNETCORE_ENVIRONMENT": "Development",
"BTCPAY_CHAINS": "btc,ltc",
"BTCPAY_BTCLIGHTNING": "http://api-token:foiewnccewuify@127.0.0.1:54938/",
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
"BTCPAY_BUNDLEJSCSS": "false"
},
"applicationUrl": "http://127.0.0.1:14142/"
}

View File

@ -242,7 +242,7 @@ namespace BTCPayServer.Services.Invoices
#pragma warning disable CS0618
public List<PaymentEntity> GetPayments()
{
return Payments.ToList();
return Payments?.ToList() ?? new List<PaymentEntity>();
}
public List<PaymentEntity> GetPayments(string cryptoCode)
{
@ -375,7 +375,8 @@ namespace BTCPayServer.Services.Invoices
BIP21 = $"{scheme}:{cryptoInfo.Address}?amount={cryptoInfo.Due}",
};
}
if (info.GetId().PaymentType == PaymentTypes.LightningLike)
var paymentId = info.GetId();
if (paymentId.PaymentType == PaymentTypes.LightningLike)
{
cryptoInfo.PaymentUrls = new NBitpayClient.InvoicePaymentUrls()
{
@ -383,7 +384,7 @@ namespace BTCPayServer.Services.Invoices
};
}
#pragma warning disable CS0618
if (info.CryptoCode == "BTC")
if (info.CryptoCode == "BTC" && paymentId.PaymentType == PaymentTypes.BTCLike)
{
dto.Url = cryptoInfo.Url;
dto.BTCPrice = cryptoInfo.Price;

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Services
{
public class Language
{
public Language(string code, string displayName)
{
DisplayName = displayName;
Code = code;
}
public string Code { get; set; }
public string DisplayName { get; set; }
}
public class LanguageService
{
public Language[] GetLanguages()
{
return new[]
{
new Language("en-US", "English"),
new Language("de-DE", "Deutsch"),
new Language("ja-JP", "日本語"),
new Language("fr-FR", "Français"),
new Language("es-ES", "Spanish"),
new Language("pt-BR", "Portuguese (Brazil)"),
new Language("nl-NL", "Dutch"),
new Language("cs-CZ", "Česky"),
};
}
}
}

View File

@ -41,6 +41,8 @@ namespace BTCPayServer.Services.Rates
private decimal GetRate(Dictionary<string, decimal> rates, string currency)
{
if (currency == "BTC")
return 1.0m;
if (rates.TryGetValue(currency, out decimal result))
return result;
throw new RateUnavailableException(currency);

View File

@ -48,14 +48,72 @@ namespace BTCPayServer.Services.Stores
}
}
public class StoreUser
{
public string Id { get; set; }
public string Email { get; set; }
public string Role { get; set; }
}
public async Task<StoreUser[]> GetStoreUsers(string storeId)
{
if (storeId == null)
throw new ArgumentNullException(nameof(storeId));
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx
.UserStore
.Where(u => u.StoreDataId == storeId)
.Select(u => new StoreUser()
{
Id = u.ApplicationUserId,
Email = u.ApplicationUser.Email,
Role = u.Role
}).ToArrayAsync();
}
}
public async Task<StoreData[]> GetStoresByUserId(string userId)
{
using (var ctx = _ContextFactory.CreateContext())
{
return await ctx.UserStore
return (await ctx.UserStore
.Where(u => u.ApplicationUserId == userId)
.Select(u => u.StoreData)
.ToArrayAsync();
.Select(u => new { u.StoreData, u.Role })
.ToArrayAsync())
.Select(u =>
{
u.StoreData.Role = u.Role;
return u.StoreData;
}).ToArray();
}
}
public async Task<bool> AddStoreUser(string storeId, string userId, string role)
{
using (var ctx = _ContextFactory.CreateContext())
{
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId, Role = role };
ctx.UserStore.Add(userStore);
try
{
await ctx.SaveChangesAsync();
return true;
}
catch (DbUpdateException)
{
return false;
}
}
}
public async Task RemoveStoreUser(string storeId, string userId)
{
using (var ctx = _ContextFactory.CreateContext())
{
var userStore = new UserStore() { StoreDataId = storeId, ApplicationUserId = userId };
ctx.UserStore.Add(userStore);
ctx.Entry<UserStore>(userStore).State = EntityState.Deleted;
await ctx.SaveChangesAsync();
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer
{
public class StorePolicies
{
public const string CanAccessStores = "CanAccessStore";
public const string OwnStore = "OwnStore";
}
public class StoreRoles
{
public const string Owner = "Owner";
public const string Guest = "Guest";
public static IEnumerable<String> AllRoles
{
get
{
yield return Owner;
yield return Guest;
}
}
}
}

View File

@ -0,0 +1,561 @@
@model PaymentModel
<div class="top-header">
<div class="header">
<div class="header__icon">
<img class="header__icon__img" src="~/img/logo-white.png" height="40">
</div>
</div>
<div class="timer-row">
<div class="timer-row__progress-bar" style="width: 0%;"></div>
<div class="timer-row__spinner">
@Html.Partial("Checkout-Spinner")
</div>
<div class="timer-row__message">
<span v-if="srvModel.status === 'expired' || srvModel.status === 'invalid'">
{{$t("Invoice expired")}}
</span>
<span v-else-if="expiringSoon">
{{$t("Invoice expiring soon...")}}
</span>
<span v-else>
{{$t("Awaiting Payment...")}}
</span>
</div>
<div class="timer-row__time-left">@Model.TimeLeft</div>
</div>
</div>
<div class="order-details">
@if (Model.AvailableCryptos.Count > 1)
{
<div class="currency-selection">
<div class="single-item-order__left">
<div style="font-weight: 600;">
{{$t("Pay with")}}
</div>
</div>
<div class="single-item-order__right">
<div class="payment__currencies">
@foreach (var crypto in Model.AvailableCryptos)
{
<a href="@crypto.Link" onclick="return changeCurrency('@crypto.PaymentMethodId');">
<img style="height:32px; margin-left:5px;" alt="@crypto.PaymentMethodId" src="@crypto.CryptoImage" />
</a>
}
</div>
<div class="payment__spinner">
@Html.Partial("Checkout-Spinner")
</div>
</div>
</div>
}
<div class="single-item-order buyerTotalLine">
<div class="single-item-order__left">
<div class="single-item-order__left__name">
{{ srvModel.storeName }}
</div>
<div class="single-item-order__left__description">
{{ srvModel.itemDesc }}
</div>
</div>
<div class="single-item-order__right">
<div class="single-item-order__right__btc-price" id="buyerTotalBtcAmount">
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
</div>
<div class="single-item-order__right__ex-rate">
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
</div>
</div>
<span class="fa fa-angle-double-down"></span>
<span class="fa fa-angle-double-up"></span>
</div>
<line-items>
<div class="line-items">
<div class="line-items__item">
<div class="line-items__item__label">{{$t("Order Amount")}}</div>
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span>{{$t("Network Cost")}}</span>
</div>
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span>{{$t("Already Paid")}}</span>
</div>
<div class="line-items__item__value">-{{srvModel.btcPaid }} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item line-items__item--total">
<div class="line-items__item__label">{{$t("Due")}}</div>
<div class="line-items__item__value">{{srvModel.btcDue}} {{ srvModel.cryptoCode }}</div>
</div>
</div>
</line-items>
<div class="payment-tabs">
<div class="payment-tabs__tab active" id="scan-tab">
<span>{{$t("Scan")}}</span>
</div>
<div class="payment-tabs__tab" id="copy-tab">
<span>{{$t("Copy")}}</span>
</div>
@if (Model.AllowCoinConversion)
{
<div class="payment-tabs__tab" id="altcoins-tab">
<span>{{$t("Conversion")}}</span>
</div>
<div id="tabsSlider" class="payment-tabs__slider three-tabs"></div>
}
else
{
<div id="tabsSlider" class="payment-tabs__slider"></div>
}
</div>
</div>
<div adjust-height="" class="payment-box">
<div class="bp-view payment manual-flow enter-contact-email active" id="emailAddressView">
<form class="manual__step-one refund-address-form contact-email-form" id="emailAddressForm" name="emailAddressForm" novalidate="">
<div class="manual__step-one__header">
<span>{{$t("Contact and Refund Email")}}</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<span>{{$t("Contact_Body")}}</span>
</span>
<span class="submission-error-label">{{$t("Please enter a valid email address")}}</span>
</div>
<div class="input-wrapper">
<input class="bp-input email-input ng-pristine ng-invalid ng-touched" id="emailAddressFormInput" v-bind:placeholder="$t('Your email')" type="email">
<bp-loading-button>
<button type="submit" class="action-button" style="margin-top: 15px;">
<span class="button-text">{{$t("Continue")}}</span>
<div class="loader-wrapper">
@Html.Partial("Checkout-Spinner")
</div>
</button>
</bp-loading-button>
</div>
</form>
</div>
<div class="bp-view payment scan" id="scan">
<div class="payment__scan">
<img v-bind:src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
<qrcode v-bind:val="srvModel.invoiceBitcoinUrlQR" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
</qrcode>
</div>
<div class="payment__details__instruction__open-wallet">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span>{{$t("Open in wallet")}}</span>
<span class="glyphicon glyphicon-new-window"></span>
</a>
</div>
</div>
<div class="bp-view payment manual-flow" id="copy">
<div class="manual__step-two__instructions">
<span i18n="">{{$t("CompletePay_Body", srvModel)}}</span>
</div>
<div class="manual-box flipped" style="margin-bottom: 30px;">
<div class="manual-box__amount">
<div class="manual-box__amount__label label">{{$t("Amount")}}</div>
<div class="manual-box__amount__value copy-cursor" ngxclipboard="">
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
<div class="copied-label">
<span>{{$t("Copied")}}</span>
</div>
</div>
</div>
<div class="manual-box__address">
<div class="flipper flipped-initially">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label">{{$t("Address")}}</div>
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img :src="srvModel.cryptoImage" height="16" />
</div>
<div class="manual-box__address__wrapper__value" style="overflow:hidden;max-width:240px;">{{srvModel.btcAddress}}</div>
</div>
<div class="copied-label" style="top: 5px;">
<span>{{$t("Copied")}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@if (Model.AllowCoinConversion)
{
<div id="altcoins" class="bp-view payment manual-flow">
<div v-if="srvModel.isLightning">
<div class="manual__step-two__instructions">
<span>
{{$t("ConversionTab_BodyTop", srvModel)}}
<br /><br />
{{$t("ConversionTab_BodyDesc", srvModel)}}
</span>
</div>
<center>
<script>function shapeshift_click(a, e) { e.preventDefault(); var link = a.href; var shapeshiftWindow = window.open(link, '1418115287605', 'width=700,height=500,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); shapeshiftWindow.focus(); return false; }</script>
<a onclick="shapeshift_click(this, event);" v-bind:href="srvModel.shapeshiftUrl">
<img src="https://shapeshift.io/images/shifty/xs_light_altcoins.png" class="ss-button">
</a>
@*Changelly doesn't have TO_AMOUNT support so we can't include it
<script type="text/javascript">function open_widget(a, e) { e.preventDefault(); var link = a.href; var changellyWindow = window.open(link, 'Changelly', 'width=600,height=470,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); changellyWindow.focus(); return false; }</script>
<a onclick="open_widget(this, event);" href="https://changelly.com/widget/v1?auth=email&from=DASH&to=BTC&address=&amount=1&merchant_id=&ref_id=">
<img src="https://changelly.com/pay_button_pay_with.png" alt="Changelly" />
</a>*@
</center>
</div>
<div v-else>
<div class="manual__step-two__instructions">
<span>
{{$t("ConversionTab_Lightning")}}
</span>
</div>
</div>
</div>
}
<div class="bp-view pad" id="paid">
<div class="status-block">
<div class="success-block">
<div class="status-icon">
<div class="status-icon__wrapper">
<div class="inner-wrapper">
<div class="status-icon__wrapper__icon">
<img src="~/imlegacy/checkmark.svg">
</div>
<div class="status-icon__wrapper__outline"></div>
</div>
</div>
</div>
<div class="success-message">{{$t("This invoice has been paid")}}</div>
<button class="action-button">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</button>
</div>
</div>
<div class="button-wrapper refund-address-form-container" id="refund-overpayment-button">
</div>
</div>
<div class="bp-view expired" id="archived">
<div class="expired-icon">
<img src="~/imlegacy/archived.svg">
</div>
<div class="archived__message">
<div class="archived__message__header">
<span>{{$t("This invoice has been archived")}}</span>
</div>
<div>
<span>{{$t("Archived_Body")}}</span>
</div>
</div>
</div>
<div class="bp-view expired" id="expired">
<div>
<div class="expired__body">
<div class="expired__header">{{$t("What happened?")}}</div>
<div class="expired__text" i18n="">
{{$t("InvoiceExpired_Body_1", {storeName: srvModel.storeName, maxTimeMinutes: @Model.MaxTimeMinutes})}}
</div>
<div class="expired__text">
{{$t("InvoiceExpired_Body_2")}}
</div>
<div class="expired__text">
{{$t("InvoiceExpired_Body_3")}}
</div>
<div class="expired__text expired__text__smaller">
<span class="expired__text__bullet">{{$t("Invoice ID")}}</span>:
{{srvModel.invoiceId}}
<br />
<span class="expired__text__bullet">{{$t("Order ID")}}</span>:
{{srvModel.orderId}}
</div>
</div>
<a href="/invoices" class="action-button" style="margin-top: 20px;">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</a>
</div>
</div>
@* Obsolete? Start *@
<div class="bp-view" id="link-expired" style="padding-top: 3.6rem;">
<div class="manual__step-one refund-address-form" novalidate="">
<div class="manual__step-one__header" i18n="">Link Expired</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">Sorry, this link has expired. Please try requesting another refund by clicking the button below.</span>
</div>
<div class="input-wrapper">
<bp-loading-button i18n="">
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
@Html.Partial("Checkout-Spinner")
</div>
</button>
</bp-loading-button>
</div>
</div>
</div>
<div class="bp-view confirm-contact-email-view">
<form class="manual__step-one refund-address-form contact-email-form ng-untouched ng-pristine" novalidate="">
<div class="manual__step-one__header" i18n="">Contact &amp; Refund Email</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">If there is an issue with this payment, or a refund needs to be made, we will contact you at this address.</span>
</div>
<div class="input-wrapper">
<input bp-focus="focusEmailInput" class="bp-input email-input ng-untouched ng-pristine" disabled="disabled" name="receiptEmail" placeholder="Your email"
style="opacity: 1;" type="email">
<button type="submit" class="action-button" style="margin-top: 15px;">
<span i18n="">Confirm email address</span>
</button>
</div>
<div class="refund-address-form__link" id="wrong-email-button" style="color: #a9a9a9;">
<span i18n="">Wrong email?</span>
</div>
</form>
</div>
<div class="bp-view wrong-email-view" id="compromised-invoice">
<div class="manual__step-one refund-address-form" novalidate="" style="margin-top: -1rem;">
<div class="manual__step-one__header">
<span i18n="">There seems to be a problem</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">
This invoice was previously opened, and the address <strong class="placeholder-refundEmail">{Entered email address}</strong> was submitted as your contact email. If you entered this email, you can still safely make your payment. <br> <br>
If you did not submit the email address, it's possible a thief falsely
submitted this address to steal refunds. Please contact the merchant
about this security incident, and try your payment again.
</span>
</div>
<div class="input-wrapper">
<a class="action-button" style="margin-top: 15px;" target="_blank" href="mailto:@Model.StoreEmail">
<span i18n="">Contact {{srvModel.storeName}}</span>
</a>
</div>
<div class="refund-address-form__link">
<span i18n="">I understand, continue to payment&nbsp;→</span>
</div>
</div>
</div>
<div class="bp-view confirm-bitcoin-address-view" id="confirm-refund-address">
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-valid" novalidate="" style="padding-top: 1.6rem;">
<div><img src="~/imlegacy/mail.svg"></div>
<div class="manual__step-one__header">
<span i18n="">Please confirm your address</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">You should receive an email from us in a moment at <strong class="placeholder-refundEmail">{enterd refund email}</strong>. To ensure your refund is sent to the correct address, please confirm your bitcoin address by clicking the link in the email. </span>
</div>
<bp-resend-link id="resend-link">
<div class="bp-resend__link">
<span class="link-text">
<span i18n="">Resend email</span>
</span>
<div class="success-text">
<img src="~/imlegacy/circle-check.svg">
<div i18n="">Email resent</div>
</div>
</div>
</bp-resend-link>
</form>
</div>
<div class="bp-view refund-address-view" id="enter-refund-address">
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-invalid" name="refundAddressForm" novalidate="" style="margin-top: 28px; margin-bottom: 4rem;">
<div class="manual__step-one__header">
<span i18n="">Please provide a refund address.</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<span i18n="">
To send your refund of {BTC to refund} BTC,
well need a bitcoin address from your wallet. Please open your bitcoin
wallet, copy a receiving address, and paste it below.
</span>
</span>
<span class="submission-error-label" i18n="" id="invalid-bitcoin-address">Please enter a valid bitcoin address.</span>
</div>
<div class="input-wrapper">
<bp-refund-address name="refundAddress" ngmodel="" class="ng-untouched ng-pristine ng-invalid">
<div class="bp-refund-address">
<div class="bitcoin-logo">
<div><img src="@Model.CryptoImage"></div>
</div>
<input class="bp-input {'not-empty': addressValue.length &gt; 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length &gt; 0}">
</div>
</bp-refund-address>
<bp-loading-button i18n="" id="request-refund-button">
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
@Html.Partial("Checkout-Spinner")
</div>
</button>
</bp-loading-button>
</div>
<div class="refund-address-form__cancel">
<span i18n="">Cancel</span>
</div>
</form>
</div>
<div class="bp-view" id="refund-pending">
<div class="status-block">
<div class="pending-block" style="position: relative; padding-bottom: 1.6rem;">
<img src="~/imlegacy/refund-pending.svg">
<div class="pending-block__header" i18n="">Processing Refund</div>
<span>
<span class="pending-block__message" i18n="">The amount below will be refunded to you within 1-2 business days. </span>
</span>
</div>
</div>
<div class="manual-box" style="margin-bottom: 30px;">
<div class="manual-box__amount amount-only">
<div class="manual-box__amount__label label">
<span class="initial-label">&nbsp;</span>
<span class="final-label" i18n="">Amount To Be Refunded</span>
</div>
<div class="manual-box__amount__value">{BTC Amount} BTC</div>
</div>
<div class="manual-box__address">
<div class="flipper">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Will Be Refunded To</div>
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
</div>
<div class="manual-box__address__wrapper__value">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bp-view expired" id="low-fee">
<div class="expired__body">
<div class="expired__header" i18n="" style="font-weight: 400; font-size: 22px;">Payment Confirming</div>
<div class="expired__text" i18n="">This payment was made with a low <a href="https://bitcoin.org/en/glossary/transaction-fee">bitcoin miner fee</a>, which may prevent it from being accepted by the Bitcoin network.</div>
<div class="expired__text" i18n="">This is an issue with the configuration of your bitcoin wallet.</div>
<div class="expired__text" i18n="">
If the transaction
doesn't confirm, the funds will be spendable again in your wallet.
Depending on the wallet, this may take 48-72 hours.
</div>
<low-fee-timeline>
<div class="timeline">
<div class="timeline__item">
<div class="timeline__item__icon timeline__item__icon--complete">
<img src="~/imlegacy/checkmark-small.svg">
</div>
<div class="timeline__item__name" i18n="">Transaction created</div>
</div>
<div class="timeline__item">
<div class="timeline__item__icon timeline__item__icon--pending">
<img src="~/imlegacy/pending.svg">
</div>
<div class="timeline__item__name">
<span i18n="">Transaction confirming — funds have not yet moved</span>
</div>
</div>
<div class="timeline__item">
<div class="timeline__item__icon"></div>
<div class="timeline__item__name" i18n="">Payment received by {{srvModel.storeName}}</div>
</div>
</div>
</low-fee-timeline>
</div>
<button class="action-button" style="margin-top: .75rem;">
<bp-done-text>
<span i18n="">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</button>
</div>
<div class="bp-view" id="refund-complete">
<div class="status-block">
<div class="success-block" style="opacity: 1;">
<div class="status-icon">
<div class="status-icon__wrapper">
<div class="inner-wrapper">
<div class="status-icon__wrapper__icon">
<img src="~/imlegacy/checkmark.svg">
</div>
<div class="status-icon__wrapper__outline" style="height: 117px; width: 117px;"></div>
</div>
</div>
</div>
<div class="success-message">
<span>
<span i18n="">Refund Complete</span>
</span>
</div>
</div>
</div>
<div class="manual-box">
<div class="manual-box__amount amount-only">
<div class="manual-box__amount__label label">
<span class="initial-label" i18n="">Overpaid By</span>
<span class="final-label">
<span i18n="">Amount Refunded</span>
</span>
</div>
<div class="manual-box__amount__value">{BTC amount} BTC</div>
</div>
<div class="manual-box__address">
<div class="flipper flipped-initially">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Refunded To</div>
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
</div>
<div class="manual-box__address__wrapper__value">
</div>
</div>
</div>
</div>
</div>
</div>
<button class="action-button finished" style="margin-top: 23px;">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</button>
</div>
<div class="footer-button enter-different-address-button">
<bp-done-text>
<span>{{$t("Return to StoreName", srvModel)}}</span>
</bp-done-text>
</div>
@* Obsolete? End *@
</div>

View File

@ -0,0 +1,5 @@
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>

View File

@ -1,8 +1,8 @@
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.Services.LanguageService langService
@model PaymentModel
@{
Layout = null;
ViewData["Title"] = "Payment";
}
<!DOCTYPE html>
<html>
@ -46,632 +46,80 @@
<invoice>
<div class="no-bounce" id="checkoutCtrl">
@*<div class="modal-backdrop fade-in"></div>*@
<!---->
<div class="modal page">
<div class="modal-dialog open opened" role="document">
<div class="modal-dialog open opened enter-purchaser-email" role="document">
<div class="modal-content long">
<div class="content">
<div class="invoice">
<div class="top-header">
<div class="header">
<div class="header__icon">
<img class="header__icon__img" src="~/img/logo-white.png" height="40">
</div>
<!---->
</div>
<div class="timer-row">
<div class="timer-row__progress-bar" style="width: 0%;"></div>
<!---->
<div class="timer-row__spinner">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
<div class="timer-row__message">
<!---->
<span>
<!---->
<span i18n="">Awaiting Payment...</span>
<!---->
</span>
<!---->
</div>
<div class="timer-row__time-left">@Model.TimeLeft</div>
</div>
</div>
<div class="order-details">
@if (Model.AvailableCryptos.Count > 1)
{
<div class="currency-selection">
<div class="single-item-order__left">
<div style="font-weight: 600;">
Pay with
</div>
</div>
<div class="single-item-order__right">
<div class="payment__currencies">
@foreach (var crypto in Model.AvailableCryptos)
{
<a href="@crypto.Link" onclick="return changeCurrency('@crypto.PaymentMethodId');">
<img style="height:32px; margin-left:5px;" alt="@crypto.PaymentMethodId" src="@crypto.CryptoImage" />
</a>
}
</div>
<div class="payment__spinner">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
</div>
</div>
}
<!---->
<div class="single-item-order buyerTotalLine">
<div class="single-item-order__left">
<div class="single-item-order__left__name">
{{ srvModel.storeName }}
</div>
<div class="single-item-order__left__description">
{{ srvModel.itemDesc }}
</div>
</div>
<!---->
<div class="single-item-order__right">
<div class="single-item-order__right__btc-price" id="buyerTotalBtcAmount">
<span>{{ srvModel.btcDue }} {{ srvModel.cryptoCode }}</span>
</div>
<!---->
<div class="single-item-order__right__ex-rate">
1 {{ srvModel.cryptoCode }} = {{ srvModel.rate }}
</div>
<!---->
</div>
<span class="fa fa-angle-double-down"></span>
<span class="fa fa-angle-double-up"></span>
</div>
<!---->
<line-items>
<div class="line-items">
<!---->
<div class="line-items__item">
<div class="line-items__item__label" i18n="">Order Amount</div>
<div class="line-items__item__value">{{srvModel.orderAmount}} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span i18n="">Network Cost</span>
</div>
<div class="line-items__item__value" i18n="">{{srvModel.networkFeeDescription }}</div>
</div>
<div class="line-items__item">
<div class="line-items__item__label">
<span i18n="">Already Paid</span>
</div>
<div class="line-items__item__value" i18n="">-{{srvModel.btcPaid }} {{ srvModel.cryptoCode }}</div>
</div>
<div class="line-items__item line-items__item--total">
<div class="line-items__item__label" i18n="">Due </div>
<div class="line-items__item__value">{{srvModel.btcDue}} {{ srvModel.cryptoCode }}</div>
</div>
<!---->
</div>
</line-items>
<div class="payment-tabs">
<div class="payment-tabs__tab active" id="scan-tab">
<span i18n="">Scan</span>
</div>
<div class="payment-tabs__tab" id="copy-tab">
<span i18n="">Copy</span>
</div>
@if (Model.AllowCoinConversion)
{
<div class="payment-tabs__tab" id="altcoins-tab">
<span i18n="">Conversion</span>
</div>
<div id="tabsSlider" class="payment-tabs__slider three-tabs"></div>
}
else
{
<div id="tabsSlider" class="payment-tabs__slider"></div>
}
</div>
</div>
<div adjust-height="" class="payment-box">
<div class="bp-view payment manual-flow enter-contact-email active" id="emailAddressView">
<form class="manual__step-one refund-address-form contact-email-form" id="emailAddressForm" name="emailAddressForm" novalidate="">
<div class="manual__step-one__header">
<!---->
<span i18n="">Contact &amp; Refund Email</span>
<!---->
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<!---->
<span i18n="">Please provide an email address below. Well contact you at this address if there is an issue with your payment. </span>
<!---->
</span>
<span class="submission-error-label" i18n="">Please enter a valid email address.</span>
</div>
<div class="input-wrapper">
<input class="bp-input email-input ng-pristine ng-invalid ng-touched" id="emailAddressFormInput" placeholder="Your email" type="email">
<bp-loading-button i18n="">
<button class="action-button" style="margin-top: 15px;" type="button">
<span class="button-text" lcl="">Continue</span>
<div class="loader-wrapper">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
</button>
</bp-loading-button>
</div>
</form>
</div>
<div class="bp-view payment scan" id="scan">
<div class="payment__scan">
<img v-bind:src="srvModel.cryptoImage" style="position: absolute; height:64px; width:64px; left:118px; top:96px;" />
<qrcode v-bind:val="srvModel.invoiceBitcoinUrlQR" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
</qrcode>
</div>
<div class="payment__details__instruction__open-wallet">
<a class="payment__details__instruction__open-wallet__btn action-button" v-bind:href="srvModel.invoiceBitcoinUrl">
<span i18n="">Open in wallet</span>
<span class="glyphicon glyphicon-new-window"></span>
</a>
</div>
</div>
<div class="bp-view payment manual-flow" id="copy">
<div class="manual__step-two__instructions">
<span i18n="">To complete your payment, please send {{ srvModel.btcDue }} {{ srvModel.cryptoCode }} to the address below.</span>
</div>
<div class="manual-box flipped" style="margin-bottom: 30px;">
<div class="manual-box__amount">
<div class="manual-box__amount__label label" i18n="">Amount</div>
<!---->
<div class="manual-box__amount__value copy-cursor" ngxclipboard="">
<span>{{srvModel.btcDue}}</span> {{ srvModel.cryptoCode }}
<div class="copied-label">
<span i18n="">Copied</span>
</div>
</div>
<!---->
</div>
<div class="manual-box__address">
<div class="flipper flipped-initially">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Address</div>
<!---->
<div class="manual-box__address__value copy-cursor" ngxclipboard="">
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img :src="srvModel.cryptoImage" height="16" />
</div>
<div class="manual-box__address__wrapper__value" style="overflow:hidden;max-width:240px;">{{srvModel.btcAddress}}</div>
</div>
<div class="copied-label" style="top: 5px;">
<span i18n="">Copied</span>
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
</div>
@if (Model.AllowCoinConversion)
{
<div id="altcoins" class="bp-view payment manual-flow">
<div v-if="srvModel.paymentMethodId != 'BTC_LightningLike'">
<div class="manual__step-two__instructions">
<span>
You can pay {{ srvModel.btcDue }} {{ srvModel.cryptoCode }} using altcoins other than the ones merchant directly supports.
<br /><br />
This service is provided by 3rd party. Please keep in mind that
<span style="font-weight: 900;">we have no control</span> over how providers will forward your funds.
Invoice will only be marked paid once funds are received on {{srvModel.cryptoCode}} Blockchain.
</span>
</div>
<center>
<script>function shapeshift_click(a, e) { e.preventDefault(); var link = a.href; var shapeshiftWindow = window.open(link, '1418115287605', 'width=700,height=500,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); shapeshiftWindow.focus(); return false; }</script>
<a onclick="shapeshift_click(this, event);" v-bind:href="srvModel.shapeshiftUrl">
<img src="https://shapeshift.io/images/shifty/xs_light_altcoins.png" class="ss-button">
</a>
@*Changelly doesn't have TO_AMOUNT support so we can't include it
<script type="text/javascript">function open_widget(a, e) { e.preventDefault(); var link = a.href; var changellyWindow = window.open(link, 'Changelly', 'width=600,height=470,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0'); changellyWindow.focus(); return false; }</script>
<a onclick="open_widget(this, event);" href="https://changelly.com/widget/v1?auth=email&from=DASH&to=BTC&address=&amount=1&merchant_id=&ref_id=">
<img src="https://changelly.com/pay_button_pay_with.png" alt="Changelly" />
</a>*@
</center>
</div>
<div v-else>
<div class="manual__step-two__instructions">
<span>
No conversion providers available for BTC Lightning Network payments.
</span>
</div>
</div>
</div>
}
<div class="bp-view" id="link-expired" style="padding-top: 3.6rem;">
<div class="manual__step-one refund-address-form" novalidate="">
<div class="manual__step-one__header" i18n="">Link Expired</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">Sorry, this link has expired. Please try requesting another refund by clicking the button below.</span>
</div>
<div class="input-wrapper">
<bp-loading-button i18n="">
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
</button>
</bp-loading-button>
</div>
</div>
</div>
<div class="bp-view confirm-contact-email-view">
<form class="manual__step-one refund-address-form contact-email-form ng-untouched ng-pristine" novalidate="">
<div class="manual__step-one__header" i18n="">Contact &amp; Refund Email</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">If there is an issue with this payment, or a refund needs to be made, we will contact you at this address.</span>
</div>
<div class="input-wrapper">
<input bp-focus="focusEmailInput" class="bp-input email-input ng-untouched ng-pristine" disabled="disabled" name="receiptEmail" placeholder="Your email" style="opacity: 1;" type="email">
<button class="action-button" style="margin-top: 15px;">
<span i18n="">Confirm email address</span>
</button>
</div>
<div class="refund-address-form__link" id="wrong-email-button" style="color: #a9a9a9;">
<span i18n="">Wrong email?</span>
</div>
</form>
</div>
<div class="bp-view wrong-email-view" id="compromised-invoice">
<div class="manual__step-one refund-address-form" novalidate="" style="margin-top: -1rem;">
<div class="manual__step-one__header">
<span i18n="">There seems to be a problem</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">
This invoice was previously opened, and the address <strong class="placeholder-refundEmail">{Entered email address}</strong> was submitted as your contact email. If you entered this email, you can still safely make your payment. <br> <br>
If you did not submit the email address, it's possible a thief falsely
submitted this address to steal refunds. Please contact the merchant
about this security incident, and try your payment again.
</span>
</div>
<div class="input-wrapper">
<a class="action-button" style="margin-top: 15px;" target="_blank" href="mailto:@Model.StoreEmail">
<span i18n="">Contact {{srvModel.storeName}}</span>
</a>
</div>
<div class="refund-address-form__link">
<span i18n="">I understand, continue to payment&nbsp;→</span>
</div>
</div>
</div>
<div class="bp-view confirm-bitcoin-address-view" id="confirm-refund-address">
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-valid" novalidate="" style="padding-top: 1.6rem;">
<div><img src="~/imlegacy/mail.svg"></div>
<div class="manual__step-one__header">
<span i18n="">Please confirm your address</span>
</div>
<div class="manual__step-one__instructions">
<span class="initial-label" i18n="">You should receive an email from us in a moment at <strong class="placeholder-refundEmail">{enterd refund email}</strong>. To ensure your refund is sent to the correct address, please confirm your bitcoin address by clicking the link in the email. </span>
</div>
<bp-resend-link id="resend-link">
<div class="bp-resend__link">
<span class="link-text">
<!---->
<span i18n="">Resend email</span>
<!---->
</span>
<div class="success-text">
<!---->
<img src="~/imlegacy/circle-check.svg">
<!---->
<div i18n="">Email resent</div>
<!---->
</div>
</div>
</bp-resend-link>
</form>
</div>
<div class="bp-view refund-address-view" id="enter-refund-address">
<form class="manual__step-one refund-address-form ng-untouched ng-pristine ng-invalid" name="refundAddressForm" novalidate="" style="margin-top: 28px; margin-bottom: 4rem;">
<div class="manual__step-one__header">
<!---->
<span i18n="">Please provide a refund address.</span>
<!---->
</div>
<div class="manual__step-one__instructions">
<span class="initial-label">
<!---->
<span i18n="">
To send your refund of {BTC to refund} BTC,
well need a bitcoin address from your wallet. Please open your bitcoin
wallet, copy a receiving address, and paste it below.
</span>
<!---->
</span>
<span class="submission-error-label" i18n="" id="invalid-bitcoin-address">Please enter a valid bitcoin address.</span>
</div>
<div class="input-wrapper">
<bp-refund-address name="refundAddress" ngmodel="" class="ng-untouched ng-pristine ng-invalid">
<div class="bp-refund-address">
<div class="bitcoin-logo">
<div><img src="@Model.CryptoImage"></div>
</div>
<input class="bp-input {'not-empty': addressValue.length &gt; 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length &gt; 0}">
</div>
</bp-refund-address>
<bp-loading-button i18n="" id="request-refund-button">
<button class="action-button" style="margin-top: 15px;" type="submit">
<span class="button-text" lcl="">Request Refund</span>
<div class="loader-wrapper">
<bp-spinner>
<svg xml:space="preserve" style="enable-background:new 0 0 50 50;" version="1.1" viewBox="0 0 50 50" x="0px" xmlns="http://www.w3.org/2000/svg" y="0px">
<path d="M11.1,29.6c-0.5-1.5-0.8-3-0.8-4.6c0-8.1,6.6-14.7,14.7-14.7S39.7,16.9,39.7,25c0,1.6-0.3,3.2-0.8,4.6l6.1,2c0.7-2.1,1.1-4.3,1.1-6.6c0-11.7-9.5-21.2-21.2-21.2S3.8,13.3,3.8,25c0,2.3,0.4,4.5,1.1,6.6L11.1,29.6z"></path>
</svg>
</bp-spinner>
</div>
</button>
</bp-loading-button>
</div>
<div class="refund-address-form__cancel">
<span i18n="">Cancel</span>
</div>
</form>
</div>
<div class="bp-view pad" id="paid">
<div class="status-block">
<div class="success-block">
<div class="status-icon">
<div class="status-icon__wrapper">
<div class="inner-wrapper">
<div class="status-icon__wrapper__icon">
<img src="~/imlegacy/checkmark.svg">
</div>
<div class="status-icon__wrapper__outline"></div>
</div>
</div>
</div>
<!---->
<div class="success-message" i18n="">This invoice has been paid.</div>
<!---->
<button class="action-button" style="margin-top: 0px;">
<bp-done-text>
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</button>
</div>
</div>
<!---->
<div class="button-wrapper refund-address-form-container" id="refund-overpayment-button">
<!---->
</div>
<!---->
</div>
<div class="bp-view" id="refund-pending">
<div class="status-block">
<div class="pending-block" style="position: relative; padding-bottom: 1.6rem;">
<img src="~/imlegacy/refund-pending.svg">
<div class="pending-block__header" i18n="">Processing Refund</div>
<span>
<!---->
<!---->
<!---->
<span class="pending-block__message" i18n="">The amount below will be refunded to you within 1-2 business days. </span>
</span>
</div>
</div>
<div class="manual-box" style="margin-bottom: 30px;">
<div class="manual-box__amount amount-only">
<div class="manual-box__amount__label label">
<span class="initial-label">&nbsp;</span>
<span class="final-label" i18n="">Amount To Be Refunded</span>
</div>
<div class="manual-box__amount__value">{BTC Amount} BTC</div>
</div>
<div class="manual-box__address">
<div class="flipper">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Will Be Refunded To</div>
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
</div>
<div class="manual-box__address__wrapper__value">
</div>
</div>
</div>
</div>
</div>
</div>
<!---->
</div>
<div class="bp-view expired" id="low-fee">
<div class="expired__body">
<div class="expired__header" i18n="" style="font-weight: 400; font-size: 22px;">Payment Confirming</div>
<div class="expired__text" i18n="">This payment was made with a low <a href="https://bitcoin.org/en/glossary/transaction-fee">bitcoin miner fee</a>, which may prevent it from being accepted by the Bitcoin network.</div>
<div class="expired__text" i18n="">This is an issue with the configuration of your bitcoin wallet.</div>
<div class="expired__text" i18n="">
If the transaction
doesn't confirm, the funds will be spendable again in your wallet.
Depending on the wallet, this may take 48-72 hours.
</div>
<low-fee-timeline>
<div class="timeline">
<div class="timeline__item">
<div class="timeline__item__icon timeline__item__icon--complete">
<img src="~/imlegacy/checkmark-small.svg">
</div>
<div class="timeline__item__name" i18n="">Transaction created</div>
</div>
<div class="timeline__item">
<div class="timeline__item__icon timeline__item__icon--pending">
<img src="~/imlegacy/pending.svg">
</div>
<div class="timeline__item__name">
<span i18n="">Transaction confirming — funds have not yet moved</span>
</div>
</div>
<div class="timeline__item">
<div class="timeline__item__icon"></div>
<div class="timeline__item__name" i18n="">Payment received by {{srvModel.storeName}}</div>
</div>
</div>
</low-fee-timeline>
</div>
<!---->
<!---->
<button class="action-button" style="margin-top: .75rem;">
<bp-done-text>
<!---->
<!---->
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</button>
</div>
<div class="bp-view expired" id="expired">
<!---->
<div>
<div class="expired__body">
<div class="expired__header" i18n="">What happened?</div>
<div class="expired__text" i18n="">This invoice has expired. An invoice is only valid for @Model.MaxTimeMinutes minutes. You can <div class="expired__text__link i18n-return-to-merchant">return to {{srvModel.storeName}}</div> if you would like to submit your payment again.</div>
<div class="expired__text" i18n="">If you tried to send a payment, it has not yet been accepted by the Bitcoin network. We have not yet received your funds.</div>
<div class="expired__text" i18n="">
If the transaction
is not accepted by the Bitcoin network, the funds will be spendable
again in your wallet. Depending on your wallet, this may take 48-72
hours.
</div>
<div class="expired__text">
<span class="expired__text__bullet" i18n="">Invoice ID:</span> {{srvModel.invoiceId}}<br>
<!---->
<span>
<span class="expired__text__bullet" i18n="">Order ID:</span> {{srvModel.orderId}}
</span>
</div>
</div>
<a href="/invoices" class="action-button" style="margin-top: 20px;">
<bp-done-text>
<!---->
<!---->
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</a>
</div>
<!---->
<!---->
</div>
<div class="bp-view expired" id="archived">
<div class="expired-icon">
<img src="~/imlegacy/archived.svg">
</div>
<div class="archived__message">
<div class="archived__message__header">
<span i18n="">This invoice has been archived.</span>
</div>
<div>
<span i18n="">Please contact the store for order information or assistance.</span>
</div>
</div>
</div>
<div class="bp-view" id="refund-complete">
<div class="status-block">
<div class="success-block" style="opacity: 1;">
<div class="status-icon">
<div class="status-icon__wrapper">
<div class="inner-wrapper">
<div class="status-icon__wrapper__icon">
<img src="~/imlegacy/checkmark.svg">
</div>
<div class="status-icon__wrapper__outline" style="height: 117px; width: 117px;"></div>
</div>
</div>
</div>
<div class="success-message">
<!---->
<span>
<span i18n="">Refund Complete</span>
</span>
<!---->
</div>
</div>
</div>
<div class="manual-box">
<div class="manual-box__amount amount-only">
<div class="manual-box__amount__label label">
<span class="initial-label" i18n="">Overpaid By</span>
<!---->
<span class="final-label">
<span i18n="">Amount Refunded</span>
</span>
<!---->
</div>
<div class="manual-box__amount__value">{BTC amount} BTC</div>
</div>
<div class="manual-box__address">
<div class="flipper flipped-initially">
<div class="back"></div>
<div class="front">
<div class="manual-box__address__arrow"></div>
<div class="manual-box__address__label label" i18n="">Refunded To</div>
<div class="manual-box__address__wrapper">
<div class="manual-box__address__wrapper__logo">
<img src="~/imlegacy/bitcoin-symbol.svg">
</div>
<div class="manual-box__address__wrapper__value">
</div>
</div>
</div>
</div>
</div>
</div>
<button class="action-button finished" style="margin-top: 23px;">
<bp-done-text>
<!---->
<!---->
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</button>
</div>
<div class="footer-button enter-different-address-button">
<bp-done-text>
<!---->
<!---->
<span i18n="" class="i18n-return-to-merchant">Return to {{srvModel.storeName}}</span>
</bp-done-text>
</div>
</div>
@Html.Partial("Checkout-Body")
</div>
</div>
</div>
<div style="margin-top: 10px; text-align: right;">
@* Not working because of nsSeparator: false, keySeparator: false,
{{$t("nested.lang")}} >>
*@
<select class="cmblang reverse invisible" onchange="changeLanguage($(this).val())">
@foreach(var lang in langService.GetLanguages())
{
<option value="@lang.Code">@lang.DisplayName</option>
}
</select>
<script>
$(function () {
var storeDefaultLang = '@Model.DefaultLang';
if (urlParams.lang) {
$(".cmblang").val(urlParams.lang);
} else if (storeDefaultLang) {
$(".cmblang").val(storeDefaultLang);
}
$('select').prettyDropdown({
classic: false,
height: 30,
reverse: true,
hoverIntent: 5000
});
});
</script>
</div>
</div>
</div>
</div>
</invoice>
<script type="text/javascript">
var storeDefaultLang = '@Model.DefaultLang';
// initialization
i18next.init({
lng: storeDefaultLang,
fallbackLng: 'en-US',
nsSeparator: false,
keySeparator: false,
resources: {
'en-US': { translation: locales_en },
'de-DE': { translation: locales_de },
'es-ES': { translation: locales_es },
'ja-JP': { translation: locales_ja },
'fr-FR': { translation: locales_fr },
'pt-BR': { translation: locales_pt_br },
'nl': { translation: locales_nl },
'cs-CZ': { translation: locales_cs }
},
});
function changeLanguage(lang) {
i18next.changeLanguage(lang);
}
if (urlParams.lang) {
changeLanguage(urlParams.lang);
}
else if (storeDefaultLang) {
changeLanguage(storeDefaultLang);
}
const i18n = new VueI18next(i18next);
// TODO: Move all logic from core.js to Vue controller
Vue.config.ignoredElements = [
'line-items',
@ -680,12 +128,14 @@
/^bp-/
];
var checkoutCtrl = new Vue({
i18n: i18n,
el: '#checkoutCtrl',
components: {
qrcode: VueQr
},
data: {
srvModel: srvModel
srvModel: srvModel,
expiringSoon: false
}
});
</script>

View File

@ -54,7 +54,7 @@
{
<li class="nav-item"><a asp-area="" asp-controller="Server" asp-action="ListUsers" class="nav-link js-scroll-trigger">Server settings</a></li>
}
<li class="nav-item"><a asp-area="" asp-controller="Stores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>

View File

@ -5,6 +5,7 @@
ViewData.AddActivePage(BTCPayServer.Views.Stores.StoreNavPages.Index);
}
@Html.Partial("_StatusMessage", Model.StatusMessage)
<h4>@ViewData["Title"]</h4>
<div class="row">
@ -32,17 +33,13 @@
<div id="ledger-info" class="form-text text-muted" style="display: none;">
<span>A ledger wallet is detected, which account do you want to use?</span>
<ul>
@for(int i = 0; i < 4; i++)
{
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (49'/<span class="ledger-info-cointype">0</span>'/@i')</a></li>
}
@for(int i = 0; i < 4; i++)
{
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (49'/<span class="ledger-info-cointype">0</span>'/@i')</a></li>
}
</ul>
</div>
</div>
<div class="form-group">
<label asp-for="DerivationSchemeFormat"></label>
<select asp-for="DerivationSchemeFormat" asp-items="Model.DerivationSchemeFormats" class="form-control"></select>
</div>
<div class="form-group">
<span>BTCPay format memo</span>
<table class="table">
@ -90,7 +87,6 @@
</div>
<input type="hidden" asp-for="Confirmation" />
<input type="hidden" asp-for="DerivationScheme" />
<input type="hidden" asp-for="DerivationSchemeFormat" />
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
@ -110,6 +106,16 @@
</tbody>
</table>
</div>
<div class="form-group">
<h5>Wrong addresses?</h5>
<span>Help us to find the correct settings by telling us the first address of your wallet</span>
</div>
<div class="form-group">
<label asp-for="HintAddress"></label>
<input asp-for="HintAddress" class="form-control" />
<span asp-validation-for="HintAddress" class="text-danger"></span>
</div>
<button name="command" type="submit" class="btn btn-success">Confirm</button>
}
</form>

View File

@ -13,31 +13,39 @@
</div>
</div>
<div class="row">
<table class="table">
<tr>
<th>Label</th>
<td>@Model.Label</td>
</tr>
<tr>
<th>Facade</th>
<td>@Model.Facade</td>
</tr>
<tr>
<th>SIN</th>
<td>@Model.SIN</td>
</tr>
</table>
<div class="col-md-4"></div>
<div class="col-md-4">
<table class="table">
<tr>
<th>Label</th>
<td style="text-align:right;">@Model.Label</td>
</tr>
<tr>
<th>Facade</th>
<td style="text-align:right;">@Model.Facade</td>
</tr>
<tr>
<th>SIN</th>
<td style="text-align:right;">@Model.SIN</td>
</tr>
</table>
</div>
<div class="col-md-4"></div>
</div>
<div class="row">
<form asp-action="Pair" method="post">
<div class="form-group">
<label asp-for="SelectedStore"></label>
<select asp-for="SelectedStore" asp-items="@(new SelectList(Model.Stores,"Id","Name"))" class="form-control"></select>
<span asp-validation-for="SelectedStore" class="text-danger"></span>
</div>
<input type="hidden" name="pairingCode" value="@Model.Id" />
<button type="submit" class="btn btn-info" title="Approve this pairing demand">Approve</button>
</form>
<div class="col-md-4"></div>
<div class="col-md-4">
<form asp-action="Pair" method="post">
<div class="form-group">
<label asp-for="SelectedStore"></label>
<select asp-for="SelectedStore" asp-items="@(new SelectList(Model.Stores,"Id","Name"))" class="form-control"></select>
<span asp-validation-for="SelectedStore" class="text-danger"></span>
</div>
<input type="hidden" name="pairingCode" value="@Model.Id" />
<button type="submit" class="btn btn-info" title="Approve this pairing demand">Approve</button>
</form>
</div>
<div class="col-md-4"></div>
</div>
</div>
</section>

View File

@ -14,7 +14,8 @@ namespace BTCPayServer.Views.Stores
public static string Tokens => "Tokens";
public static string Users => "Users";
public static string UsersNavClass(ViewContext viewContext) => PageNavClass(viewContext, Users);
public static string TokensNavClass(ViewContext viewContext) => PageNavClass(viewContext, Tokens);
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);

View File

@ -0,0 +1,57 @@
@model StoreUsersViewModel
@{
Layout = "../Shared/_NavLayout.cshtml";
ViewData["Title"] = "Manage users";
ViewData.AddActivePage(StoreNavPages.Users);
}
<h4>@ViewData["Title"]</h4>
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<h5>Users</h5>
<span>Add access to your store to other users (Guest will not be able to see and modify the store settings)<br />
Note that the user must have a registered account on this BTCPay Server.</span>
</div>
<div class="form-inline">
<form method="post">
<input asp-for="Email" type="text" class="form-control" placeholder="user@example.com">
<select asp-for="Role" class="form-control">
<option value="@StoreRoles.Owner">Owner</option>
<option value="@StoreRoles.Guest">Guest</option>
</select>
<button type="submit" role="button" class="form-control btn btn-success"><span class="glyphicon glyphicon-plus"></span>Add user</button>
</form>
</div>
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
<tr>
<th>Email</th>
<th>Role</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
<tbody>
@foreach(var user in Model.Users)
{
<tr>
<td>@user.Email</td>
<td>@user.Role</td>
<td style="text-align:right">
<a asp-action="DeleteStoreUser" asp-route-storeId="@Model.StoreId" asp-route-userId="@user.Id">Remove</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>

View File

@ -34,6 +34,10 @@
<label asp-for="DefaultCryptoCurrency"></label>
<select asp-for="DefaultCryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="DefaultLang"></label>
<select asp-for="DefaultLang" asp-items="Model.Languages" class="form-control"></select>
</div>
<div class="form-group">
<label asp-for="NetworkFee"></label>
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
@ -89,20 +93,20 @@
</tr>
</thead>
<tbody>
@foreach (var scheme in Model.DerivationSchemes)
{
<tr>
<td>@scheme.Crypto</td>
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
<td style="text-align:right">
@if(!string.IsNullOrWhiteSpace(scheme.Value))
{
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
}
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
</td>
</tr>
}
@foreach(var scheme in Model.DerivationSchemes)
{
<tr>
<td>@scheme.Crypto</td>
<td style="max-width:300px;overflow:hidden;">@scheme.Value</td>
<td style="text-align:right">
@if(!string.IsNullOrWhiteSpace(scheme.Value))
{
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
}
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
</td>
</tr>
}
</tbody>
</table>
</div>
@ -112,9 +116,15 @@
<h5>Lightning nodes (Experimental)</h5>
<p>
<span>A connection to a lightning charge node is required to generate lignting network enabled invoices.<br /></span>
<span>This is experimental and not advised for production so keep in mind:</span>
<span>This is experimental and not advised for production.</span>
</p>
</div>
<div class="form-group">
<label asp-for="LightningMaxValue"></label>
<input asp-for="LightningMaxValue" class="form-control" />
<span asp-validation-for="LightningMaxValue" class="text-danger"></span>
<p class="form-text text-muted">Example: 5.50 USD</p>
</div>
<div class="form-group">
<table class="table">
<thead class="thead-inverse">
@ -125,14 +135,14 @@
</tr>
</thead>
<tbody>
@foreach (var scheme in Model.LightningNodes)
{
<tr>
<td>@scheme.CryptoCode</td>
<td>@scheme.Address</td>
<td style="text-align:right"><a asp-action="AddLightningNode" asp-route-cryptoCode="@scheme.CryptoCode">Modify</a></td>
</tr>
}
@foreach(var scheme in Model.LightningNodes)
{
<tr>
<td>@scheme.CryptoCode</td>
<td>@scheme.Address</td>
<td style="text-align:right"><a asp-action="AddLightningNode" asp-route-cryptoCode="@scheme.CryptoCode">Modify</a></td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -29,42 +29,44 @@
<p id="check-success" style="display:none;"><span class="glyphicon glyphicon-ok-sign" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
</div>
</div>
<div class="col-md-6">
<form id="sendform" style="display:none;">
<input type="hidden" id="cryptoCode" asp-for="CryptoCurrency" />
<div class="form-group">
<label>Destination</label>
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
<span id="Destination-Error" class="text-danger"></span>
</div>
<div class="form-group">
<label>Amount</label>
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
<span id="Amount-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
</p>
</div>
<div class="form-group">
<label>Fee rate (satoshi per byte)</label>
<input id="fee-textbox" name="FeeRate" class="form-control" type="text" />
<span id="FeeRate-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
The recommended value is <a id="crypto-fee-link" href="#"><span id="crypto-fee"></span></a> satoshi per byte.
</p>
</div>
<div class="form-group">
<label>Subtract fees from amount</label>
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
</div>
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
</form>
<div class="row">
<div class="col-md-6">
<form id="sendform" style="display:none;">
<input type="hidden" id="cryptoCode" asp-for="CryptoCurrency" />
<div class="form-group">
<label>Destination</label>
<input id="destination-textbox" name="Destination" class="form-control" type="text" />
<span id="Destination-Error" class="text-danger"></span>
</div>
<div class="form-group">
<label>Amount</label>
<input id="amount-textbox" name="Amount" class="form-control" type="text" />
<span id="Amount-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
Your current balance is <a id="crypto-balance-link" href="#"><span id="crypto-balance"></span></a> <span id="crypto-code"></span>.
</p>
</div>
<div class="form-group">
<label>Fee rate (satoshi per byte)</label>
<input id="fee-textbox" name="FeeRate" class="form-control" type="text" />
<span id="FeeRate-Error" class="text-danger"></span>
<p class="form-text text-muted crypto-info" style="display: none;">
The recommended value is <a id="crypto-fee-link" href="#"><span id="crypto-fee"></span></a> satoshi per byte.
</p>
</div>
<div class="form-group">
<label>Subtract fees from amount</label>
<input id="substract-checkbox" name="SubstractFees" class="form-check" type="checkbox" />
</div>
<button id="confirm-button" name="command" type="submit" class="btn btn-success">Confirm</button>
</form>
</div>
</div>
@section Scripts
{
<script type="text/javascript">
@section Scripts
{
<script type="text/javascript">
@Model.ToJSVariableModel("srvModel")
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
}
</script>
<script src="~/js/ledgerwebsocket.js" type="text/javascript" defer="defer"></script>
<script src="~/js/StoreWallet.js" type="text/javascript" defer="defer"></script>
}

View File

@ -4,5 +4,6 @@
<ul class="nav nav-pills nav-stacked">
<li class="@StoreNavPages.IndexNavClass(ViewContext)"><a asp-action="UpdateStore">Information</a></li>
<li class="@StoreNavPages.TokensNavClass(ViewContext)"><a asp-action="ListTokens">Access Tokens</a></li>
<li class="@StoreNavPages.UsersNavClass(ViewContext)"><a asp-action="StoreUsers">Users</a></li>
</ul>

View File

@ -8,7 +8,7 @@
<div class="row">
<div class="col-lg-12 text-center">
@Html.Partial("_StatusMessage", Model.StatusMessage)
@Html.Partial("_StatusMessage", TempData["TempDataProperty-StatusMessage"])
</div>
</div>
@ -27,7 +27,7 @@
<tr>
<th>Name</th>
<th>Website</th>
<th>Balances</th>
<th>On-Chain balances</th>
<th style="text-align:right">Actions</th>
</tr>
</thead>
@ -52,7 +52,13 @@
}
}
</td>
<td style="text-align:right"><a asp-action="UpdateStore" asp-route-storeId="@store.Id">Settings</a> - <a asp-action="DeleteStore" asp-route-storeId="@store.Id">Remove</a></td>
<td style="text-align:right">
@if(store.IsOwner)
{
<a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@store.Id">Settings</a><span> - </span>
}
<a asp-action="DeleteStore" asp-route-storeId="@store.Id">Remove</a>
</td>
</tr>
}
</tbody>

View File

@ -33,7 +33,8 @@
"inputFiles": [
"wwwroot/vendor/font-awesome/css/font-awesome.css",
"wwwroot/css/css.css",
"wwwroot/css/normalizer.css"
"wwwroot/css/normalizer.css",
"wwwroot/vendor/jquery-prettydropdowns/prettydropdowns.css"
]
},
{
@ -41,9 +42,12 @@
"inputFiles": [
"wwwroot/vendor/clipboard.js/clipboard.js",
"wwwroot/vendor/jquery/jquery.js",
"wwwroot/js/vue.min.js",
"wwwroot/js/vue-qrcode.js",
"wwwroot/js/core.js"
"wwwroot/vendor/vuejs/vue.min.js",
"wwwroot/vendor/vuejs/vue-qrcode.js",
"wwwroot/vendor/i18next/i18next.js",
"wwwroot/vendor/i18next/vue-i18next.js",
"wwwroot/vendor/jquery-prettydropdowns/jquery.prettydropdowns.js",
"wwwroot/js/checkout/**/*.js"
]
}
]

View File

@ -9550,8 +9550,8 @@ strong {
}
.expired__body {
padding: 14px 10px;
padding-top: 8px;
padding: 0px 8px 0px;
margin-top: -10px;
}
.expired__header {
@ -9562,10 +9562,14 @@ strong {
.expired__text {
margin-top: 20px;
font-weight: 100;
font-size: 14.5px;
font-size: 14px;
opacity: .8;
}
.expired__text .expired__text__smaller {
font-size: 11px;
}
.expired__text__bullet {
font-weight: 500;
}

View File

@ -1,3 +1,5 @@
// TODO: Refactor... switch from jQuery to Vue.js
// public methods
function resetTabsSlider() {
$("#tabsSlider").removeClass("slide-copy");
$("#tabsSlider").removeClass("slide-altcoins");
@ -16,8 +18,11 @@ function resetTabsSlider() {
$("#altcoins").removeClass("active");
}
// public methods
function onDataCallback(jsonData) {
// extender properties used
jsonData.shapeshiftUrl = "https://shapeshift.io/shifty.html?destination=" + jsonData.btcAddress + "&output=" + jsonData.paymentMethodId + "&amount=" + jsonData.btcDue;
//
var newStatus = jsonData.status;
if (newStatus === "complete" ||
@ -44,7 +49,6 @@ function onDataCallback(jsonData) {
if (newStatus === "expired" || newStatus === "invalid") { //TODO: different state if the invoice is invalid (failed to confirm after timeout)
$(".timer-row").removeClass("expiring-soon");
$(".timer-row__message span").html("Invoice expired.");
$(".timer-row__spinner").html("");
$("#emailAddressView").removeClass("active");
$(".modal-dialog").addClass("expired");
@ -63,7 +67,6 @@ function onDataCallback(jsonData) {
$(".payment__spinner").hide();
}
jsonData.shapeshiftUrl = "https://shapeshift.io/shifty.html?destination=" + jsonData.btcAddress + "&output=" + jsonData.paymentMethodId + "&amount=" + jsonData.btcDue;
// updating ui
checkoutCtrl.srvModel = jsonData;
}
@ -125,7 +128,6 @@ $(document).ready(function () {
function hideEmailForm() {
$("[role=document]").removeClass("enter-purchaser-email");
$("#emailAddressView").removeClass("active");
$("placeholder-refundEmail").html(srvModel.customerEmail);
@ -163,6 +165,8 @@ $(document).ready(function () {
} else {
$("#emailAddressForm").addClass("ng-touched ng-dirty ng-submitted ng-invalid");
}
return false;
});
}
@ -282,23 +286,23 @@ $(document).ready(function () {
}
function animateUpdate() {
var now = new Date();
var timeDiff = end.getTime() - now.getTime();
var perc = 100 - Math.round(timeDiff / timerMax * 100);
var status = checkoutCtrl.srvModel.status;
if (perc === 75 && (status === "paidPartial" || status === "new")) {
$(".timer-row").addClass("expiring-soon");
$(".timer-row__message span").html("Invoice expiring soon ...");
checkoutCtrl.expiringSoon = true;
updateProgress(perc);
}
if (perc <= 100) {
updateProgress(perc);
setTimeout(animateUpdate, timeoutVal);
}
if (perc >= 100 && status === "expired") {
onDataCallback(status);
}
//if (perc >= 100 && status === "expired") {
// onDataCallback(status);
//}
}
}

View File

@ -0,0 +1,48 @@
const locales_cs = {
nested: {
lang: 'Jazyk'
},
"Awaiting Payment...": "Očekávám platbu...",
"Pay with": "Zaplatit pomocí",
"Contact and Refund Email": "Kontaktní email",
"Contact_Body": "Prosímte poskytněte vaši emailovou adresu. Kontaktujeme vás v případě, že se objeví problému s vaší platbou.",
"Your email": "Váš email",
"Continue": "Pokračovat",
"Please enter a valid email address": "Prosíme vložte platnou emailovou adresu",
"Order Amount": "Cena objednávky",
"Network Cost": "Síťové náklady",
"Already Paid": "Již zaplaceno",
"Due": "Do kdy",
// Tabs
"Scan": "Skenovat",
"Copy": "Kopírovat",
"Conversion": "Konverze",
// Scan tab
"Open in wallet": "Otevřít v peněžence",
// Copy tab
"CompletePay_Body": "K dokončení platby, prosíme pošlete {{btcDue}} {{cryptoCode}} na adresu níže.",
"Amount": "Částka",
"Address": "Adresa",
"Copied": "Zkopírováno",
// Conversion tab
"ConversionTab_BodyTop": "Můžete zaplatit {{btcDue}} {{cryptoCode}} i pomocí altcoinů které přímo nepodporuje obchodník.",
"ConversionTab_BodyDesc": "Tato služba je poskytována třetí stranou. Prosíme mějte na paměti, že nemáme žádnou kontrolu nad tím, jak poskytovatelé budou nakládat s vašimi prostředky. Faktura bude označena jako zaplacena, pouze když jsou prostředky obdrženy v {{cryptoCode}} Blockchainu.",
"Shapeshift_Button_Text": "Zaplatit pomocí Altcoinů",
"ConversionTab_Lightning": "Pro platby Lightning Network nejsou dostupní žádní poskytovatelé konverzí.",
// Invoice expired
"Invoice expiring soon...": "Faktura brzy vyprší...",
"Invoice expired": "Faktura vypršela",
"What happened?": "Co se stalo?",
"InvoiceExpired_Body_1": "Tato faktura již vypršela. Faktura je platná pouze {{maxTimeMinutes}} minut. \
Můžete se vrátit do {{storeName}}, pokud chcete svojí objednávku založit znovu.",
"InvoiceExpired_Body_2": "Pokud jste se pokoušeli poslat platbu, nebyla zatím zaznamenána v Bitcoinové síti. Zatím jsme neobdrželi vaše prostředky.",
"InvoiceExpired_Body_3": "Pokud nebude transakce přijata Bitcoinovou sítí, vaše prostředky bude opět použitelné ve vaší peněžence. V závislosti na vaší peněžence toto může trvat 48-72 hodin.",
"Invoice ID": "ID Faktury",
"Order ID": "ID Objednávky",
"Return to StoreName": "Vrátit se na {{storeName}}",
// Invoice paid
"This invoice has been paid": "Faktura byla zaplacena",
// Invoice archived
"This invoice has been archived": "Tato faktura byla archivována",
"Archived_Body": "Prosíme kontaktujte prodejce pro informace o objednávce a případnou pomoc"
};

View File

@ -0,0 +1,48 @@
const locales_de = {
nested: {
lang: 'Sprache'
},
"Awaiting Payment...": "Warten auf Zahlung...",
"Pay with": "Bezahlen mit",
"Contact and Refund Email": "Kontakt und Rückerstattungs Email",
"Contact_Body": "Bitte geben Sie unten eine E-Mail-Adresse an. Wir werden Sie unter dieser Adresse kontaktieren, wenn ein Problem mit Ihrer Zahlung vorliegt.",
"Your email": "Deine Email",
"Continue": "Fortsetzen",
"Please enter a valid email address": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"Order Amount": "Bestellbetrag",
"Network Cost": "Netzwerkkosten",
"Already Paid": "Bereits bezahlt",
"Due": "Fällig",
// Tabs
"Scan": "Scan",
"Copy": "Kopieren",
"Conversion": "Umwandlung",
// Scan tab
"Open in wallet": "In der Brieftasche öffnen",
// Copy tab
"CompletePay_Body": "Um Ihre Zahlung abzuschließen, senden Sie bitte {{btcDue}} {{cryptoCode}} an die unten angegebene Adresse.",
"Amount": "Menge",
"Address": "Adresse",
"Copied": "Kopiert",
// Conversion tab
"ConversionTab_BodyTop": "Sie können {{btcDue}} {{cryptoCode}} mit altcoins bezahlen, die nicht direkt vom Händler unterstützt werden.",
"ConversionTab_BodyDesc": "Dieser Service wird von Drittanbietern bereitgestellt. Bitte beachten Sie, dass wir keine Kontrolle darüber haben, wie die Anbieter Ihre Gelder weiterleiten. Die Rechnung wird erst bezahlt, wenn das Geld in {{cryptoCode}} Blockchain eingegangen ist.",
"Shapeshift_Button_Text": "Bezahlen mit Altcoins",
"ConversionTab_Lightning": "Für Lightning Network-Zahlungen sind keine Conversion-Anbieter verfügbar.",
// Invoice expired
"Invoice expiring soon...": "Die Rechnung läuft bald ab...",
"Invoice expired": "Die Rechnung ist abgelaufen",
"What happened?": "Was ist passiert?",
"InvoiceExpired_Body_1": "Diese Rechnung ist abgelaufen. Eine Rechnung ist nur für {{maxTimeMinutes}} Minuten gültig. \
Sie können zu {{storeName}} zurückkehren, wenn Sie Ihre Zahlung erneut senden möchten.",
"InvoiceExpired_Body_2": "Wenn Sie versucht haben, eine Zahlung zu senden, wurde sie vom Bitcoin-Netzwerk noch nicht akzeptiert. Wir haben Ihre Gelder noch nicht erhalten.",
"InvoiceExpired_Body_3": "Wenn die Transaktion vom Bitcoin-Netzwerk nicht akzeptiert wird, ist das Geld wieder in Ihrer Brieftasche verfügbar. Abhängig von Ihrem Geldbeutel, kann dies 48-72 Stunden dauern.",
"Invoice ID": "Rechnungs ID",
"Order ID": "Auftrag ID",
"Return to StoreName": "Zurück zu {{storeName}}",
// Invoice paid
"This invoice has been paid": "Diese Rechnung wurde bezahlt",
// Invoice archived
"This invoice has been archived": "Diese Rechnung wurde archiviert",
"Archived_Body": "Bitte kontaktieren Sie den Shop für Bestellinformationen oder Hilfe"
};

View File

@ -0,0 +1,48 @@
const locales_en = {
nested: {
lang: 'Language'
},
"Awaiting Payment...": "Awaiting Payment...",
"Pay with": "Pay with",
"Contact and Refund Email": "Contact & Refund Email",
"Contact_Body": "Please provide an email address below. Well contact you at this address if there is an issue with your payment.",
"Your email": "Your email",
"Continue": "Continue",
"Please enter a valid email address": "Please enter a valid email address",
"Order Amount": "Order Amount",
"Network Cost": "Network Cost",
"Already Paid": "Already Paid",
"Due": "Due",
// Tabs
"Scan": "Scan",
"Copy": "Copy",
"Conversion": "Conversion",
// Scan tab
"Open in wallet": "Open in wallet",
// Copy tab
"CompletePay_Body": "To complete your payment, please send {{btcDue}} {{cryptoCode}} to the address below.",
"Amount": "Amount",
"Address": "Address",
"Copied": "Copied",
// Conversion tab
"ConversionTab_BodyTop": "You can pay {{btcDue}} {{cryptoCode}} using altcoins other than the ones merchant directly supports.",
"ConversionTab_BodyDesc": "This service is provided by 3rd party. Please keep in mind that we have no control over how providers will forward your funds. Invoice will only be marked paid once funds are received on {{cryptoCode}} Blockchain.",
"Shapeshift_Button_Text": "Pay with Altcoins",
"ConversionTab_Lightning": "No conversion providers available for Lightning Network payments.",
// Invoice expired
"Invoice expiring soon...": "Invoice expiring soon...",
"Invoice expired": "Invoice expired",
"What happened?": "What happened?",
"InvoiceExpired_Body_1": "This invoice has expired. An invoice is only valid for {{maxTimeMinutes}} minutes. \
You can return to {{storeName}} if you would like to submit your payment again.",
"InvoiceExpired_Body_2": "If you tried to send a payment, it has not yet been accepted by the Bitcoin network. We have not yet received your funds.",
"InvoiceExpired_Body_3": "If the transaction is not accepted by the Bitcoin network, the funds will be spendable again in your wallet. Depending on your wallet, this may take 48-72 hours.",
"Invoice ID": "Invoice ID",
"Order ID": "Order ID",
"Return to StoreName": "Return to {{storeName}}",
// Invoice paid
"This invoice has been paid": "This invoice has been paid",
// Invoice archived
"This invoice has been archived": "This invoice has been archived",
"Archived_Body": "Please contact the store for order information or assistance"
};

View File

@ -0,0 +1,47 @@
const locales_es = {
nested: {
lang: 'Lenguaje'
},
"Awaiting Payment...": "En espera de pago...",
"Pay with": "Pagar con",
"Contact and Refund Email": "Contacto y correo electrónico de reembolso",
"Contact_Body": "Por favor provea una dirección de correo electrónico a continuación. Nos pondremos en contacto con usted en esta dirección si hay un problema con su pago.",
"Your email": "Tu correo electrónico",
"Continue": "Continuar",
"Please enter a valid email address": "Por favor entre un correo electrónico valido",
"Order Amount": "Total de el pedido",
"Network Cost": "Costo de la red",
"Already Paid": "Ya pagado",
"Due": "Debido",
// Tabs
"Scan": "Escaniar",
"Copy": "Copiar",
"Conversion": "Conversión",
// Scan tab
"Open in wallet": "Abrir en billetera",
// Copy tab
"CompletePay_Body": "Para completar su pago, envíe {{btcDue}} {{cryptoCode}} a la dirección siguiente.",
"Amount": "Cantidad",
"Address": "Direccón",
"Copied": "Copiado",
// Conversion tab
"ConversionTab_BodyTop": "Puede pagar {{btcDue}} {{cryptoCode}} usando altcoins que no sean los que el comerciante soporta directamente.",
"ConversionTab_BodyDesc": "Este servicio es proveído por terceros. Tenga en cuenta que no tenemos control sobre cómo los proveedores enviarán sus fondos. La factura solo se marcará como abonada una vez que se reciban los fondos en el bloque de cadenas de {{cryptoCode}} .",
"Shapeshift_Button_Text": "Pagar con Altcoins",
"ConversionTab_Lightning": "No hay proveedores de conversión disponibles para los pagos de Lightning Network.",
// Invoice expired
"Invoice expiring soon...": "La factura expira pronto...",
"Invoice expired": "La factura expiro",
"What happened?": "¿Qué sucedió?",
"InvoiceExpired_Body_1": "Esta factura ha expirado. Una factura solo es válida por {{maxTimeMinutes}} minutos. \ Puede volver a {{storeName}} si desea volver a enviar su pago.",
"InvoiceExpired_Body_2": "Si intentó enviar un pago, aún no ha sido aceptado por la red de Bitcoin. Todavía no hemos recibido sus fondos.",
"InvoiceExpired_Body_3": "Si la transacción no es aceptada por la red de Bitcoin, los fondos se podrán gastar nuevamente en su billetera. Dependiendo de su billetera, esto puede tomar 48-72 horas.",
"Invoice ID": "ID de factura",
"Order ID": "ID de pedido",
"Return to StoreName": "Regresar a {{storeName}}",
// Invoice paid
"This invoice has been paid": "Esta factura ha sido pagada",
// Invoice archived
"This invoice has been archived": "Esta factura ha sido archivada",
"Archived_Body": "Por favor, comuníquese con la tienda para obtener información de su pedido o asistencia"
};

View File

@ -0,0 +1,48 @@
const locales_fr = {
nested: {
lang: 'Langue'
},
"Awaiting Payment...": "En attente du paiement...",
"Pay with": "Payer avec",
"Contact and Refund Email": "Adresse de contact et de remboursement",
"Contact_Body": "Merci de renseigner l'adresse email ci-dessous. Nous vous contacterons à cette adresse si il y a un problème avec votre paiement.",
"Your email": "Votre email",
"Continue": "Continuer",
"Please enter a valid email address": "Merci de rentrer une addrese email valide",
"Order Amount": "Montant de la commande",
"Network Cost": "Coût réseau",
"Already Paid": "Déjà payé",
"Due": "Dûe",
// Tabs
"Scan": "Scanner",
"Copy": "Copier",
"Conversion": "Convertir",
// Scan tab
"Open in wallet": "Ouvrir le portefeuille",
// Copy tab
"CompletePay_Body": "Pour terminer le paiement, merci d'envoyer {{btcDue}} {{cryptoCode}} à l'adresse ci-dessous.",
"Amount": "Montant",
"Address": "Adresse",
"Copied": "Copié",
// Conversion tab
"ConversionTab_BodyTop": "Vous pouvez payer {{btcDue}} {{cryptoCode}} en utilisant d'autre crypto-monnaies alternatives non supportées directement par le marchant.",
"ConversionTab_BodyDesc": "Ce service est fournis par un tiers partie. Cependant, nous n'avons aucun controle la façon dont sera traité vos fonds. La facture sera considérée payée seulement quand les fonds seront reçus sur la blockchain {{ cryptoCode }}.",
"Shapeshift_Button_Text": "Payer avec une crypto-monnaie alternative",
"ConversionTab_Lightning": "Pas de fournisseur disponible pour les paiements sur le Lightning Network.",
// Invoice expired
"Invoice expiring soon...": "La facture va bientôt expirer...",
"Invoice expired": "Facture expiré",
"What happened?": "Que s'est t'il passé?",
"InvoiceExpired_Body_1": "La facture a expirée. Une facture est seulement valide pour {{maxTimeMinutes}} minutes. \
Vous pouvez revenir sur {{storeName}} si vous voulez resoumettre votre paiement.",
"InvoiceExpired_Body_2": "Si vous avez essayé d'envoyer un paiement, il n'a pas encore été accepté par la blockchain. Nous n'avons pas encore reçu vos fonds.",
"InvoiceExpired_Body_3": "Si votre transaction n'a pas été accepté par la blockchain, vos fonds reviendront dans votre portefueille. Selon votre portefueille, cela peut prendre entre 48 et 72 heures.",
"Invoice ID": "Numéro de facture",
"Order ID": "Numéro de commande",
"Return to StoreName": "Retourner sur {{storeName}}",
// Invoice paid
"This invoice has been paid": "Cette facture a été payée",
// Invoice archived
"This invoice has been archived": "Cette facture a été archivée",
"Archived_Body": "Merci de contacter le marchand pour plus d'assistance ou d'information sur cette commande."
};

View File

@ -0,0 +1,48 @@
const locales_ja = {
nested: {
lang: 'Language'
},
"Awaiting Payment...": "お支払いをお待ちしております…",
"Pay with": "お支払い方法",
"Contact and Refund Email": "問題発生時の連絡先",
"Contact_Body": "決済においては、何か問題が発生したらこちらのメールアドレスに対してご連絡差し上げることもございますので、ご記入ください",
"Your email": "ご自分のメールアドレス",
"Continue": "続ける",
"Please enter a valid email address": "正常なメールアドレスをご記入ください",
"Order Amount": "注文金額",
"Network Cost": "ネットワーク手数料",
"Already Paid": "支払い済み金額",
"Due": "未払い金額",
// Tabs
"Scan": "スキャン",
"Copy": "コピー",
"Conversion": "変換",
// Scan tab
"Open in wallet": "ウォレットで開く",
// Copy tab
"CompletePay_Body": "決済をするために、下記のアドレスに {{btcDue}} {{cryptoCode}} をお送りください",
"Amount": "金額",
"Address": "アドレス",
"Copied": "コピーしました",
// Conversion tab
"ConversionTab_BodyTop": "代わりに、お店が受け付けていなくても {{btcDue}} {{cryptoCode}} での支払いもできます。",
"ConversionTab_BodyDesc": "ただし、この変換は第三者サービスによるものですので、お店が受け付けている通貨で着金するまでの間の処理に関しては何の保証もいたしません。変換後に受付中の通貨 ({{cryptoCode}}) がお店に着金してから支払い済みとなりますのでご了承ください。",
"Shapeshift_Button_Text": "他の仮想通貨で支払う",
"ConversionTab_Lightning": "ライトニングのペイメントでは現在変換サービスが存在しないためご利用いただけません。ご了承ください。",
// Invoice expired
"Invoice expiring soon...": "お支払いの期限が迫っています...",
"Invoice expired": "お支払いの期限が切れました",
"What happened?": "え!?ナニコレ!?",
"InvoiceExpired_Body_1": "当件のお支払いの有効期限が過ぎてしまいました。最大 {{maxTimeMinutes}} 分以内に支払うことが義務付けられています。 \
まだお支払いのご希望の場合 {{storeName}} に一旦戻っていただき、もう一度お支払いの手続きを最初からやり直してみてください。",
"InvoiceExpired_Body_2": "送金手続きを完了したつもりでも、ネットワークにて取り込まれて処理されるまでは決済となりません。現時点ではまだ着金しておりません。",
"InvoiceExpired_Body_3": "ネットワークにて取り込まれなかった送金はいずれ送金元のウォレットに戻りますが、ウォレットソフトによっては2〜3日かかる場合もございますのでご了承ください。",
"Invoice ID": "お支払い ID",
"Order ID": "ご注文 ID",
"Return to StoreName": "{{storeName}} に戻る",
// Invoice paid
"This invoice has been paid": "お支払いが完了しました",
// Invoice archived
"This invoice has been archived": "お支払いをアーカイブしました",
"Archived_Body": "ご注文に関わる詳細などでお困りの場合はお店の担当窓口へお問い合わせください。"
};

View File

@ -0,0 +1,48 @@
const locales_nl = {
nested: {
lang: 'Taal'
},
"Awaiting Payment...": "Wachtende op de betaling...",
"Pay with": "Betalen met",
"Contact and Refund Email": "Email adres voor opvolging en terugbetaling",
"Contact_Body": "Bedankt om je email adres in te vullen voor een mogelijke opvolging. We contacteren je indien er een probleem optreedt.",
"Your email": "Je email adres",
"Continue": "Verdergaan",
"Please enter a valid email address": "Bedankt om een geldig email adres in te vullen",
"Order Amount": "Bedrag van je bestelling",
"Network Cost": "Netwerk kosten",
"Already Paid": "Reeds betaald",
"Due": "Verschuldigd",
// Tabs
"Scan": "Scannen",
"Copy": "Kopiëren",
"Conversion": "Omzetting",
// Scan tab
"Open in wallet": "Wallet openen",
// Copy tab
"CompletePay_Body": "Om de betaling te vervoledigen, bedankt om {{btcDue}} {{cryptoCode}} naar het hieronder vemelde adres op te sturen.",
"Amount": "Bedrag",
"Address": "Adres",
"Copied": "Gekopieerd",
// Conversion tab
"ConversionTab_BodyTop": "Je kan alternatieve cryptocurrencies gebruiken die niet ondersteund zijn door de verkoper, om {{btcDue}} {{cryptoCode}} te betalen.",
"ConversionTab_BodyDesc": "Deze dienst wordt door een externe partij geleverd. Bijgevolg, hebben we geen zicht over jouw fondsen. De factuur wordt pas als betaald beschouwd, wanneer de fondsen door de blockchain aanvaard zijn {{ cryptoCode }}.",
"Shapeshift_Button_Text": "Betalen met een alternatieve cryptocurrency",
"ConversionTab_Lightning": "Geen leverancier beschikbaar voor de betalingen op het Lightning Network",
// Invoice expired
"Invoice expiring soon...": "De factuur zal weldra vervallen...",
"Invoice expired": "Vervallen factuur",
"What happened?": "Wat gebeurde er?",
"InvoiceExpired_Body_1": "De factuur is vervallen. Een factuur is geldig voor {{maxTimeMinutes}} minuten. \
Je kan terug komen naar {{storeName}} indien je nog eens je betaling wilt proberen uit te voeren.",
"InvoiceExpired_Body_2": "Indien je een betaling uitvoerde, werd deze nog niet aanvaard door de blockchain. We hebben je fondsen nog niet ontvangen.",
"InvoiceExpired_Body_3": "Indien je transactie niet door de blockchain werd aanvaard, zullen je fondsen terug in wallet verschijnen. Volgens de wallet, kan dit 48 to 72 uren duren.",
"Invoice ID": "Factuurnummer",
"Order ID": "Bestllingsnummer",
"Return to StoreName": "Terug naar {{storeName}}",
// Invoice paid
"This invoice has been paid": "Deze factuur werd betaald",
// Invoice archived
"This invoice has been archived": "Deze factuur werd geactiveerd",
"Archived_Body": "Bedankt om de winkel te contacteren voor bijstand met of informatie over deze bestelling."
};

View File

@ -0,0 +1,48 @@
const locales_pt_br = {
nested: {
lang: 'Idioma'
},
"Awaiting Payment...": "Esperando o pagamento...",
"Pay with": "Pague com",
"Contact and Refund Email": "Email de contato e reembolso",
"Contact_Body": "Por favor, forneça um email abaixo. Nós iremos contactar você se algum problema ocorrer com o seu pagamento.",
"Your email": "Seu email",
"Continue": "Continue",
"Please enter a valid email address": "Por favor, entre com um email válido",
"Order Amount": "Valor do pedido",
"Network Cost": "Custo da rede",
"Already Paid": "Já foi pago",
"Due": "Devido",
// Tabs
"Scan": "Escaneie",
"Copy": "Copie",
"Conversion": "Conversão",
// Scan tab
"Open in wallet": "Abra na carteira",
// Copy tab
"CompletePay_Body": "Para completar seu pagamento, por favor envie {{btcDue}} {{cryptoCode}} para o endereço abaixo.",
"Amount": "Quantia",
"Address": "Endereço",
"Copied": "Copiado",
// Conversion tab
"ConversionTab_BodyTop": "Você pode pagar {{btcDue}} {{cryptoCode}} utilizando outras altcoins além das que a loja aceita diretamente.",
"ConversionTab_BodyDesc": "Esse serviço é oferecido por terceiros. Por favor, tenha em mente que não temos nenhum controle sobre como seus fundos serão utilizados. A fatura apenas será marcada como paga quando os fundos forem recebidos na Blockchain {{cryptoCode}}.",
"Shapeshift_Button_Text": "Pague com Altcoins",
"ConversionTab_Lightning": "Não há provedores de conversão disponíveis para pagamentos via Lightning Network.",
// Invoice expired
"Invoice expiring soon...": "A fatura está vencendo...",
"Invoice expired": "Fatura vencida",
"What happened?": "O que aconteceu?",
"InvoiceExpired_Body_1": "Essa fatura vence. Uma fatura é válida apenas por {{maxTimeMinutes}} minutos. \
Você pode retornar à {{storeName}} se desejar enviar seu pagamento novamente.",
"InvoiceExpired_Body_2": "Se você tentou enviar um pagamento, o mesmo não foi aceito pela rede Bitcoin. Nós não recebemos ainda o valor enviado.",
"InvoiceExpired_Body_3": "Se a transação não for aceita pela rede Bitcoin, o valor retornará à sua carteira. Dependendo da sua carteira, isso pode demorar de 48 a 72 horas.",
"Invoice ID": "Nº da Fatura",
"Order ID": "Nº do Pedido",
"Return to StoreName": "Retornar à {{storeName}}",
// Invoice paid
"This invoice has been paid": "Essa fatura foi paga",
// Invoice archived
"This invoice has been archived": "Essa fatura foi arquivada",
"Archived_Body": "Por favor, contate o estabelecimento para informações e suporte"
};

View File

@ -0,0 +1,12 @@
var urlParams;
(window.onpopstate = function () {
var match,
pl = /\+/g, // Regex for replacing addition symbol with a space
search = /([^&=]+)=?([^&]*)/g,
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
query = window.location.search.substring(1);
urlParams = {};
while (match = search.exec(query))
urlParams[decode(match[1])] = decode(match[2]);
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("VueI18next",[],t):"object"==typeof exports?exports.VueI18next=t():e.VueI18next=t()}(this,function(){return function(e){function t(i){if(n[i])return n[i].exports;var o=n[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/dist/",t(t.s=2)}([function(e,t,n){"use strict";function i(e){i.installed||(i.installed=!0,t.Vue=u=e,u.mixin({computed:{$t:function(){var e=this;return function(t,n){return e.$i18n.t(t,n,e.$i18n.i18nLoadedAt)}}},beforeCreate:function(){var e=this.$options;e.i18n?this.$i18n=e.i18n:e.parent&&e.parent.$i18n&&(this.$i18n=e.parent.$i18n)}}),u.component(r.default.name,r.default))}Object.defineProperty(t,"__esModule",{value:!0}),t.Vue=void 0,t.install=i;var o=n(1),r=function(e){return e&&e.__esModule?e:{default:e}}(o),u=t.Vue=void 0},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={name:"i18next",functional:!0,props:{tag:{type:String,default:"span"},path:{type:String,required:!0}},render:function(e,t){var n=t.props,i=t.data,o=t.children,r=t.parent,u=r.$i18n;if(!u)return o;var a=n.path,s=u.i18next.services.interpolator.regexp,f=u.t(a,{interpolation:{prefix:"#$?",suffix:"?$#"}}),d=[],c={};return o.forEach(function(e){e.data&&e.data.attrs&&e.data.attrs.tkey&&(c[e.data.attrs.tkey]=e)}),f.split(s).reduce(function(e,t,n){var i=void 0;if(n%2==0){if(0===t.length)return e;i=t}else i=o[parseInt(t,10)];return e.push(i),e},d),e(n.tag,i,d)}},e.exports=t.default},function(e,t,n){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},r=function(){function e(e,t){for(var n=0;n<t.length;n++){var i=t[n];i.enumerable=i.enumerable||!1,i.configurable=!0,"value"in i&&(i.writable=!0),Object.defineProperty(e,i.key,i)}}return function(t,n,i){return n&&e(t.prototype,n),i&&e(t,i),t}}(),u=n(0),a=function(){function e(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};i(this,e);var o=n.bindI18n,r=void 0===o?"languageChanged loaded":o,u=n.bindStore,a=void 0===u?"added removed":u;this._vm=null,this.i18next=t,this.onI18nChanged=this.onI18nChanged.bind(this),r&&this.i18next.on(r,this.onI18nChanged),a&&this.i18next.store&&this.i18next.store.on(a,this.onI18nChanged),this.resetVM({i18nLoadedAt:new Date})}return r(e,[{key:"resetVM",value:function(e){var t=this._vm,n=u.Vue.config.silent;u.Vue.config.silent=!0,this._vm=new u.Vue({data:e}),u.Vue.config.silent=n,t&&u.Vue.nextTick(function(){return t.$destroy()})}},{key:"t",value:function(e,t){return this.i18next.t(e,t)}},{key:"onI18nChanged",value:function(){this.i18nLoadedAt=new Date}},{key:"i18nLoadedAt",get:function(){return this._vm.$data.i18nLoadedAt},set:function(e){this._vm.$set(this._vm,"i18nLoadedAt",e)}}]),e}();t.default=a,a.install=u.install,a.version="0.4.0",("undefined"==typeof window?"undefined":o(window))&&window.Vue&&window.Vue.use(a),e.exports=t.default}])});
//# sourceMappingURL=vue-i18next.js.map

View File

@ -0,0 +1,490 @@
/*!
* jQuery Pretty Dropdowns Plugin v4.11.0 by T. H. Doan (http://thdoan.github.io/pretty-dropdowns/)
*
* jQuery Pretty Dropdowns by T. H. Doan is licensed under the MIT License.
* Read a copy of the license in the LICENSE file or at
* http://choosealicense.com/licenses/mit
*/
(function($) {
$.fn.prettyDropdown = function(oOptions) {
// Default options
oOptions = $.extend({
classic: false,
customClass: 'arrow',
height: 50,
hoverIntent: 200,
multiDelimiter: '; ',
multiVerbosity: 99,
selectedMarker: '&#10003;',
reverse: false,
afterLoad: function(){}
}, oOptions);
oOptions.selectedMarker = '<span aria-hidden="true" class="checked"> ' + oOptions.selectedMarker + '</span>';
// Validate options
if (isNaN(oOptions.height) || oOptions.height<8) oOptions.height = 8;
if (isNaN(oOptions.hoverIntent) || oOptions.hoverIntent<0) oOptions.hoverIntent = 200;
if (isNaN(oOptions.multiVerbosity)) oOptions.multiVerbosity = 99;
// Translatable strings
var MULTI_NONE = 'None selected',
MULTI_PREFIX = 'Selected: ',
MULTI_POSTFIX = ' selected';
// Globals
var $current,
aKeys = [
'0','1','2','3','4','5','6','7','8','9',,,,,,,,
'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'
],
nCount,
nHoverIndex,
nLastIndex,
nTimer,
nTimestamp,
// Initiate pretty drop-downs
init = function(elSel) {
var $select = $(elSel),
nSize = elSel.size,
sId = elSel.name || elSel.id || '',
sLabelId;
// Exit if widget has already been initiated
if ($select.data('loaded')) return;
// Remove 'size' attribute to it doesn't affect vertical alignment
$select.data('size', nSize).removeAttr('size');
// Set <select> height to reserve space for <div> container
$select.css('visibility', 'hidden').outerHeight(oOptions.height);
nTimestamp = +new Date();
// Test whether to add 'aria-labelledby'
if (elSel.id) {
// Look for <label>
var $label = $('label[for=' + elSel.id + ']');
if ($label.length) {
// Add 'id' to <label> if necessary
if ($label.attr('id') && !/^menu\d{13,}$/.test($label.attr('id'))) sLabelId = $label.attr('id');
else $label.attr('id', (sLabelId = 'menu' + nTimestamp));
}
}
nCount = 0;
var $items = $('optgroup, option', $select),
$selected = $items.filter(':selected'),
bMultiple = elSel.multiple,
// Height - 2px for borders
sHtml = '<ul' + (elSel.disabled ? '' : ' tabindex="0"') + ' role="listbox"'
+ (elSel.title ? ' title="' + elSel.title + '" aria-label="' + elSel.title + '"' : '')
+ (sLabelId ? ' aria-labelledby="' + sLabelId + '"' : '')
+ ' aria-activedescendant="item' + nTimestamp + '-1" aria-expanded="false"'
+ ' style="max-height:' + (oOptions.height-2) + 'px;margin:'
// NOTE: $select.css('margin') returns an empty string in Firefox, so we have to get
// each margin individually. See https://github.com/jquery/jquery/issues/3383
+ $select.css('margin-top') + ' '
+ $select.css('margin-right') + ' '
+ $select.css('margin-bottom') + ' '
+ $select.css('margin-left') + ';">';
if (bMultiple) {
sHtml += renderItem(null, 'selected');
$items.each(function() {
if (this.selected) {
sHtml += renderItem(this, '', true)
} else {
sHtml += renderItem(this);
}
});
} else {
if (oOptions.classic) {
$items.each(function() {
sHtml += renderItem(this);
});
} else {
sHtml += renderItem($selected[0], 'selected');
$items.filter(':not(:selected)').each(function() {
sHtml += renderItem(this);
});
}
}
sHtml += '</ul>';
$select.wrap('<div ' + (sId ? 'id="prettydropdown-' + sId + '" ' : '')
+ 'class="prettydropdown '
+ (oOptions.classic ? 'classic ' : '')
+ (elSel.disabled ? 'disabled ' : '')
+ (bMultiple ? 'multiple ' : '')
+ oOptions.customClass + ' loading"'
// NOTE: For some reason, the container height is larger by 1px if the <select> has the
// 'multiple' attribute or 'size' attribute with a value larger than 1. To fix this, we
// have to inline the height.
+ ((bMultiple || nSize>1) ? ' style="height:' + oOptions.height + 'px;"' : '')
+'></div>').before(sHtml).data('loaded', true);
var $dropdown = $select.parent().children('ul'),
nWidth = $dropdown.outerWidth(true),
nOuterWidth;
$items = $dropdown.children();
// Update default selected values for multi-select menu
if (bMultiple) updateSelected($dropdown);
else if (oOptions.classic) $('[data-value="' + $selected.val() + '"]', $dropdown).addClass('selected').append(oOptions.selectedMarker);
// Calculate width if initially hidden
if ($dropdown.width()<=0) {
var $clone = $dropdown.parent().clone().css({
position: 'absolute',
top: '-100%'
});
$('body').append($clone);
nWidth = $clone.children('ul').outerWidth(true);
$('li', $clone).width(nWidth);
nOuterWidth = $clone.children('ul').outerWidth(true);
$clone.remove();
}
// Set dropdown width and event handler
// NOTE: Setting width using width(), then css() because width() only can return a float,
// which can result in a missing right border when there is a scrollbar.
$items.width(nWidth).css('width', $items.css('width')).click(function() {
var $li = $(this),
$selected = $dropdown.children('.selected');
// Ignore disabled menu
if ($dropdown.parent().hasClass('disabled')) return;
// Only update if not disabled, not a label, and a different value selected
if ($dropdown.hasClass('active') && !$li.hasClass('disabled') && !$li.hasClass('label') && $li.data('value')!==$selected.data('value')) {
// Select highlighted item
if (bMultiple) {
if ($li.children('span.checked').length) $li.children('span.checked').remove();
else $li.append(oOptions.selectedMarker);
// Sync <select> element
$dropdown.children(':not(.selected)').each(function(nIndex) {
$('optgroup, option', $select).eq(nIndex).prop('selected', $(this).children('span.checked').length>0);
});
// Update selected values for multi-select menu
updateSelected($dropdown);
} else {
$selected.removeClass('selected').children('span.checked').remove();
$li.addClass('selected').append(oOptions.selectedMarker);
if (!oOptions.classic) $dropdown.prepend($li);
$dropdown.removeClass('reverse').attr('aria-activedescendant', $li.attr('id'));
if ($selected.data('group') && !oOptions.classic) $dropdown.children('.label').filter(function() {
return $(this).text()===$selected.data('group');
}).after($selected);
// Sync <select> element
$('optgroup, option', $select).filter(function() {
// NOTE: .data('value') can return numeric, so using == comparison instead.
return this.value==$li.data('value') || this.text===$li.contents().filter(function() {
// Filter out selected marker
return this.nodeType===3;
}).text();
}).prop('selected', true);
}
$select.trigger('change');
}
if ($li.hasClass('selected') || !bMultiple) {
$dropdown.toggleClass('active');
$dropdown.attr('aria-expanded', $dropdown.hasClass('active'));
}
// Try to keep drop-down menu within viewport
if ($dropdown.hasClass('active')) {
// Close any other open menus
if ($('.prettydropdown > ul.active').length>1) resetDropdown($('.prettydropdown > ul.active').not($dropdown)[0]);
var nWinHeight = window.innerHeight,
nMaxHeight,
nOffsetTop = $dropdown.offset().top,
nScrollTop = document.body.scrollTop,
nDropdownHeight = $dropdown.outerHeight();
if (nSize) {
nMaxHeight = nSize*(oOptions.height-2);
if (nMaxHeight<nDropdownHeight-2) nDropdownHeight = nMaxHeight+2;
}
var nDropdownBottom = nOffsetTop-nScrollTop+nDropdownHeight;
if (nDropdownBottom > nWinHeight ||
oOptions.reverse) {
// Expand to direction that has the most space
if (nOffsetTop - nScrollTop > nWinHeight - (nOffsetTop - nScrollTop + oOptions.height) ||
oOptions.reverse) {
$dropdown.addClass('reverse');
if (!oOptions.classic) $dropdown.append($selected);
if (nOffsetTop-nScrollTop+oOptions.height<nDropdownHeight) {
$dropdown.outerHeight(nOffsetTop-nScrollTop+oOptions.height);
// Ensure the selected item is in view
$dropdown.scrollTop(nDropdownHeight);
}
} else {
$dropdown.height($dropdown.height()-(nDropdownBottom-nWinHeight));
}
}
if (nMaxHeight && nMaxHeight<$dropdown.height()) $dropdown.css('height', nMaxHeight + 'px');
// Ensure the selected item is in view
if (oOptions.classic) $dropdown.scrollTop($selected.index()*(oOptions.height-2));
} else {
$dropdown.data('clicked', true);
resetDropdown($dropdown[0]);
}
});
$dropdown.on({
focusin: function() {
// Unregister any existing handlers first to prevent duplicate firings
$(window).off('keydown', handleKeypress).on('keydown', handleKeypress);
},
focusout: function() {
$(window).off('keydown', handleKeypress);
},
mouseenter: function() {
$dropdown.data('hover', true);
},
mouseleave: resetDropdown,
mousemove: hoverDropdownItem
});
// Put focus on menu when user clicks on label
if (sLabelId) $('#' + sLabelId).off('click', handleFocus).click(handleFocus);
// Done with everything!
$dropdown.parent().width(nOuterWidth||$dropdown.outerWidth(true)).removeClass('loading');
oOptions.afterLoad();
},
// Manage widget focusing
handleFocus = function(e) {
$('ul[aria-labelledby=' + e.target.id + ']').focus();
},
// Manage keyboard navigation
handleKeypress = function(e) {
var $dropdown = $('.prettydropdown > ul.active, .prettydropdown > ul:focus');
if (!$dropdown.length) return;
if (e.which===9) { // Tab
resetDropdown($dropdown[0]);
return;
} else {
// Intercept non-Tab keys only
e.preventDefault();
e.stopPropagation();
}
var $items = $dropdown.children(),
bOpen = $dropdown.hasClass('active'),
nItemsHeight = $dropdown.height()/(oOptions.height-2),
nItemsPerPage = nItemsHeight%1<0.5 ? Math.floor(nItemsHeight) : Math.ceil(nItemsHeight),
sKey;
nHoverIndex = Math.max(0, $dropdown.children('.hover').index());
nLastIndex = $items.length-1;
$current = $items.eq(nHoverIndex);
$dropdown.data('lastKeypress', +new Date());
switch (e.which) {
case 13: // Enter
if (!bOpen) {
$current = $items.filter('.selected');
toggleHover($current, 1);
}
$current.click();
break;
case 27: // Esc
if (bOpen) resetDropdown($dropdown[0]);
break;
case 32: // Space
if (bOpen) {
sKey = ' ';
} else {
$current = $items.filter('.selected');
toggleHover($current, 1);
$current.click();
}
break;
case 33: // Page Up
if (bOpen) {
toggleHover($current, 0);
toggleHover($items.eq(Math.max(nHoverIndex-nItemsPerPage-1, 0)), 1);
}
break;
case 34: // Page Down
if (bOpen) {
toggleHover($current, 0);
toggleHover($items.eq(Math.min(nHoverIndex+nItemsPerPage-1, nLastIndex)), 1);
}
break;
case 35: // End
if (bOpen) {
toggleHover($current, 0);
toggleHover($items.eq(nLastIndex), 1);
}
break;
case 36: // Home
if (bOpen) {
toggleHover($current, 0);
toggleHover($items.eq(0), 1);
}
break;
case 38: // Up
if (bOpen) {
toggleHover($current, 0);
// If not already key-navigated or first item is selected, cycle to the last item; or
// else select the previous item
toggleHover(nHoverIndex ? $items.eq(nHoverIndex-1) : $items.eq(nLastIndex), 1);
}
break;
case 40: // Down
if (bOpen) {
toggleHover($current, 0);
// If last item is selected, cycle to the first item; or else select the next item
toggleHover(nHoverIndex===nLastIndex ? $items.eq(0) : $items.eq(nHoverIndex+1), 1);
}
break;
default:
if (bOpen) sKey = aKeys[e.which-48];
}
if (sKey) { // Alphanumeric key pressed
clearTimeout(nTimer);
$dropdown.data('keysPressed', $dropdown.data('keysPressed')===undefined ? sKey : $dropdown.data('keysPressed') + sKey);
nTimer = setTimeout(function() {
$dropdown.removeData('keysPressed');
// NOTE: Windows keyboard repeat delay is 250-1000 ms. See
// https://technet.microsoft.com/en-us/library/cc978658.aspx
}, 300);
// Build index of matches
var aMatches = [],
nCurrentIndex = $current.index();
$items.each(function(nIndex) {
if ($(this).text().toLowerCase().indexOf($dropdown.data('keysPressed'))===0) aMatches.push(nIndex);
});
if (aMatches.length) {
// Cycle through items matching key(s) pressed
for (var i=0; i<aMatches.length; ++i) {
if (aMatches[i]>nCurrentIndex) {
toggleHover($items, 0);
toggleHover($items.eq(aMatches[i]), 1);
break;
}
if (i===aMatches.length-1) {
toggleHover($items, 0);
toggleHover($items.eq(aMatches[0]), 1);
}
}
}
}
},
// Highlight menu item
hoverDropdownItem = function(e) {
var $dropdown = $(e.currentTarget);
if (e.target.nodeName!=='LI' || !$dropdown.hasClass('active') || new Date()-$dropdown.data('lastKeypress')<200) return;
toggleHover($dropdown.children(), 0, 1);
toggleHover($(e.target), 1, 1);
},
// Construct menu item
// elOpt is null for first item in multi-select menus
renderItem = function(elOpt, sClass, bSelected) {
var sGroup = '',
sText = '',
sTitle;
sClass = sClass || '';
if (elOpt) {
switch (elOpt.nodeName) {
case 'OPTION':
if (elOpt.parentNode.nodeName==='OPTGROUP') sGroup = elOpt.parentNode.getAttribute('label');
sText = (elOpt.getAttribute('data-prefix') || '') + elOpt.text + (elOpt.getAttribute('data-suffix') || '');
break;
case 'OPTGROUP':
sClass += ' label';
sText = elOpt.getAttribute('label');
break;
}
if (elOpt.disabled || (sGroup && elOpt.parentNode.disabled)) sClass += ' disabled';
sTitle = elOpt.title;
if (sGroup && !sTitle) sTitle = elOpt.parentNode.title;
}
++nCount;
return '<li id="item' + nTimestamp + '-' + nCount + '"'
+ (sGroup ? ' data-group="' + sGroup + '"' : '')
+ (elOpt && elOpt.value ? ' data-value="' + elOpt.value + '"' : '')
+ (elOpt && elOpt.nodeName==='OPTION' ? ' role="option"' : '')
+ (sTitle ? ' title="' + sTitle + '" aria-label="' + sTitle + '"' : '')
+ (sClass ? ' class="' + $.trim(sClass) + '"' : '')
+ ((oOptions.height!==50) ? ' style="height:' + (oOptions.height-2)
+ 'px;line-height:' + (oOptions.height-4) + 'px;"' : '') + '>' + sText
+ ((bSelected || sClass==='selected') ? oOptions.selectedMarker : '') + '</li>';
},
// Reset menu state
// @param o Event or Element object
resetDropdown = function(o) {
var $dropdown = $(o.currentTarget||o);
// NOTE: Sometimes it's possible for $dropdown to point to the wrong element when you
// quickly hover over another menu. To prevent this, we need to check for .active as a
// backup and manually reassign $dropdown. This also requires that it's not clicked on
// because in rare cases the reassignment fails and the reverse menu will not get reset.
if (o.type==='mouseleave' && !$dropdown.hasClass('active') && !$dropdown.data('clicked')) $dropdown = $('.prettydropdown > ul.active');
$dropdown.data('hover', false);
clearTimeout(nTimer);
nTimer = setTimeout(function() {
if ($dropdown.data('hover')) return;
if ($dropdown.hasClass('reverse') && !oOptions.classic) $dropdown.prepend($dropdown.children(':last-child'));
$dropdown.removeClass('active reverse').removeData('clicked').attr('aria-expanded', 'false').css('height', '');
$dropdown.children().removeClass('hover nohover');
}, (o.type==='mouseleave' && !$dropdown.data('clicked')) ? oOptions.hoverIntent : 0);
},
// Set menu item hover state
// bNoScroll set on hoverDropdownItem()
toggleHover = function($li, bOn, bNoScroll) {
if (bOn) {
$li.removeClass('nohover').addClass('hover');
if ($li.length===1 && $current && !bNoScroll) {
// Ensure items are always in view
var $dropdown = $li.parent(),
nDropdownHeight = $dropdown.outerHeight(),
nItemOffset = $li.offset().top-$dropdown.offset().top-1; // -1px for top border
if ($li.index()===0) {
$dropdown.scrollTop(0);
} else if ($li.index()===nLastIndex) {
$dropdown.scrollTop($dropdown.children().length*oOptions.height);
} else {
if (nItemOffset+oOptions.height>nDropdownHeight) $dropdown.scrollTop($dropdown.scrollTop()+oOptions.height+nItemOffset-nDropdownHeight);
else if (nItemOffset<0) $dropdown.scrollTop($dropdown.scrollTop()+nItemOffset);
}
}
} else {
$li.removeClass('hover').addClass('nohover');
}
},
// Update selected values for multi-select menu
updateSelected = function($dropdown) {
var $select = $dropdown.parent().children('select'),
aSelected = $('option', $select).map(function() {
if (this.selected) return this.text;
}).get(),
sSelected;
if (oOptions.multiVerbosity>=aSelected.length) sSelected = aSelected.join(oOptions.multiDelimiter) || MULTI_NONE;
else sSelected = aSelected.length + '/' + $('option', $select).length + MULTI_POSTFIX;
if (sSelected) {
var sTitle = ($select.attr('title') ? $select.attr('title') : '') + (aSelected.length ? '\n' + MULTI_PREFIX + aSelected.join(oOptions.multiDelimiter) : '');
$dropdown.children('.selected').text(sSelected);
$dropdown.attr({
'title': sTitle,
'aria-label': sTitle
});
} else {
$dropdown.children('.selected').empty();
$dropdown.attr({
'title': $select.attr('title'),
'aria-label': $select.attr('title')
});
}
};
/**
* Public Functions
*/
// Resync the menu with <select> to reflect state changes
this.refresh = function(oOptions) {
return this.each(function() {
var $select = $(this);
$select.prevAll('ul').remove();
$select.unwrap().data('loaded', false);
this.size = $select.data('size');
init(this);
});
};
return this.each(function() {
init(this);
});
};
}(jQuery));

View File

@ -0,0 +1,206 @@
.prettydropdown {
position: relative;
min-width: 72px; /* 70px + borders */
display: inline-block;
}
.prettydropdown.loading {
min-width: 0;
}
.prettydropdown > ul {
border-radius: 5px;
position: absolute;
top: 0;
left: 0;
background: #fff;
border: 1px solid #a9a9a9;
box-sizing: content-box;
color: #000;
cursor: pointer;
font: normal 18px Calibri, sans-serif;
list-style-type: none;
margin: 0;
padding: 0;
text-align: left;
-webkit-user-select: none; /* Chrome all / Safari all */
-moz-user-select: none; /* Firefox all */
-ms-user-select: none; /* IE 10+ */
user-select: none; /* Likely future */
z-index: 1;
}
.prettydropdown.loading > ul {
visibility: hidden;
white-space: nowrap;
}
.prettydropdown > ul:focus, .prettydropdown:not(.disabled) > ul:hover {
border-color: #7f7f7f;
}
.prettydropdown:not(.disabled) > ul.active {
width: auto;
max-height: 400px !important;
border-color: #1e90ff;
overflow-x: hidden;
overflow-y: auto;
z-index: 99;
}
.prettydropdown > ul.active:focus {
outline: none;
}
.prettydropdown > ul.active.reverse {
top: auto;
bottom: 0;
}
.prettydropdown > ul > li {
font-size: 14px;
position: relative;
min-width: 70px;
height: 48px; /* 50px - borders */
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
box-sizing: border-box;
display: none;
line-height: 46px; /* 48px - borders */
margin: 0;
padding-left: 0.8rem;
}
.prettydropdown.loading > ul > li {
min-width: 0;
display: block;
padding-right: 0.8rem;
}
.prettydropdown > ul:not(.active) > li:not(.selected):first-child {
color: transparent; /* Prevent FOUC */
}
.prettydropdown > ul > li:first-child, .prettydropdown > ul.active > li {
display: block;
}
.prettydropdown > ul.active > li:not(.label):hover, .prettydropdown > ul.active > li.hover:not(.label), .prettydropdown > ul.active > li:first-child:hover:after {
background: #1e90ff;
color: #fff;
}
.prettydropdown > ul.active > li.nohover {
background: inherit !important;
color: inherit !important;
}
.prettydropdown > ul.active > li.hover:before, .prettydropdown > ul.active > li.nohover:after {
border-top-color: #fff !important;
}
.prettydropdown > ul.active > li.hover:after, .prettydropdown > ul.active > li.nohover:before {
border-top-color: #1e90ff !important;
}
.prettydropdown.arrow > ul > li.selected:before, .prettydropdown.arrow > ul > li.selected:after {
position: absolute;
top: 8px;
bottom: 0;
right: 8px;
height: 16px;
border: 8px solid transparent; /* Arrow size */
box-sizing: border-box;
content: '';
display: block;
margin: auto;
}
.prettydropdown.arrow.small > ul > li.selected:before, .prettydropdown.arrow.small > ul > li.selected:after {
top: 4px;
height: 8px;
border-width: 4px;
}
.prettydropdown.arrow > ul > li.selected:before {
border-top-color: #a9a9a9; /* Arrow color */
}
.prettydropdown.arrow > ul > li.selected:after {
top: 4px; /* Chevron thickness */
border-top-color: #fff; /* Match background colour */
}
.prettydropdown.arrow.small > ul > li.selected:after {
top: 2px; /* Chevron thickness */
}
.prettydropdown.arrow.triangle > ul > li.selected:after {
content: none;
}
.prettydropdown > ul:hover > li.selected:before {
border-top-color: #7f7f7f;
}
.prettydropdown > ul.active > li.selected:before,
.prettydropdown > ul.active > li.selected:after {
border: none;
}
.prettydropdown > ul:not(.active) > li > span.checked {
display: none;
}
/* Multi-Select */
.prettydropdown.multiple > ul > li.selected {
overflow: hidden;
padding-right: 2rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.prettydropdown > ul > li > span.checked {
clear: both;
float: right;
font-weight: bold;
margin-right: 0.8rem;
}
/* Option Groups */
.prettydropdown > ul > li.label {
cursor: default;
font-weight: bold;
}
.prettydropdown > ul > li.label:first-child,
.prettydropdown.classic > ul > li.label ~ li.selected {
border-top: none;
}
.prettydropdown > ul > li.label ~ li:not(.label):not(.selected),
.prettydropdown.classic > ul.active > li.label ~ li:not(.label) {
padding-left: 1.6rem;
}
/* Classic Behavior */
.prettydropdown.classic > ul:not(.active) > li.selected:not(:first-child) {
position: absolute;
top: 0;
display: block;
}
/* Disabled */
.prettydropdown.disabled, .prettydropdown > ul > li.disabled {
opacity: 0.3;
}
.prettydropdown.disabled > ul > li, .prettydropdown > ul > li.disabled {
cursor: not-allowed;
}
/* Divider Lines */
.prettydropdown.multiple > ul > li.selected + li, .prettydropdown.multiple > ul.reverse > li.selected,
.prettydropdown > ul > li.label, .prettydropdown > ul > li.label ~ li.selected {
border-top-color: #dedede;
}

View File

@ -1,3 +1,6 @@
$ver = [regex]::Match((Get-Content BTCPayServer\BTCPayServer.csproj), '<Version>([^<]+)<').Groups[1].Value
git tag -a "v$ver" -m "$ver"
git push --tags
git push --tags
git tag -d "stable"
git tag -a "stable" -m "stable"
git push --tags --force