Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
9bf0c20198 | |||
6b7ac0e000 | |||
188c0a9a86 | |||
c49479c8ad | |||
2072b6e136 | |||
08d82390b0 | |||
b845a545e2 | |||
db958b2401 | |||
7266420eec | |||
f36fbe7a76 | |||
8e279b110c | |||
d626870e46 | |||
df49b094d5 | |||
7d17bf7f2a | |||
e51f3dd1ae | |||
b810b88c6c | |||
39b34ff4ed | |||
f72fd63113 | |||
97eedc2c9f | |||
db222c53e3 | |||
61e919b88d | |||
d14040c142 | |||
13a3a581d8 | |||
f6dbae1cef | |||
ccbcda86ac | |||
b74e8cf756 | |||
8f8266f15d | |||
ab8d3f5813 | |||
08220dbea5 | |||
3b2cf2f1de | |||
c3beca27be | |||
28b820241f | |||
e985224092 | |||
f1c467aa7d | |||
ae7cfe90ab | |||
718a36ddd0 | |||
c0b903d79c | |||
48eaf906b0 | |||
59afebaa57 | |||
3e06e45054 | |||
fe55acb268 | |||
e656813844 |
@ -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; }
|
||||
|
@ -151,7 +151,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
litecoind:
|
||||
image: nicolasdorier/docker-litecoin:0.14.2
|
||||
image: nicolasdorier/docker-litecoin:0.15.1
|
||||
environment:
|
||||
BITCOIN_EXTRA_ARGS: |
|
||||
rpcuser=ceiwHEbqWI83
|
||||
|
@ -2,18 +2,22 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<Version>1.0.1.54</Version>
|
||||
<Version>1.0.1.61</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" />
|
||||
|
@ -201,7 +201,7 @@ 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()
|
||||
@ -211,6 +211,7 @@ namespace BTCPayServer.Controllers
|
||||
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 +234,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 +418,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 +435,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
|
||||
|
@ -43,6 +43,50 @@ namespace BTCPayServer.Controllers
|
||||
return View(users);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
public new async Task<IActionResult> User(string userId)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var userVM = new UserViewModel();
|
||||
userVM.Id = user.Id;
|
||||
userVM.IsAdmin = IsAdmin(roles);
|
||||
return View(userVM);
|
||||
}
|
||||
|
||||
private static bool IsAdmin(IList<string> roles)
|
||||
{
|
||||
return roles.Contains(Roles.ServerAdmin, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
[Route("server/users/{userId}")]
|
||||
[HttpPost]
|
||||
public new async Task<IActionResult> User(string userId, UserViewModel viewModel)
|
||||
{
|
||||
var user = await _UserManager.FindByIdAsync(userId);
|
||||
if (user == null)
|
||||
return NotFound();
|
||||
var roles = await _UserManager.GetRolesAsync(user);
|
||||
var isAdmin = IsAdmin(roles);
|
||||
bool updated = false;
|
||||
|
||||
if(isAdmin != viewModel.IsAdmin)
|
||||
{
|
||||
if (viewModel.IsAdmin)
|
||||
await _UserManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
else
|
||||
await _UserManager.RemoveFromRoleAsync(user, Roles.ServerAdmin);
|
||||
updated = true;
|
||||
}
|
||||
if(updated)
|
||||
{
|
||||
viewModel.StatusMessage = "User successfully updated";
|
||||
}
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
|
||||
[Route("server/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteUser(string userId)
|
||||
|
@ -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)
|
||||
@ -125,14 +126,14 @@ namespace BTCPayServer.Controllers
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
await handler.Test(paymentMethod, network);
|
||||
var info = await handler.Test(paymentMethod, network);
|
||||
vm.StatusMessage = $"Connection to the lightning node succeed ({info})";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
vm.StatusMessage = $"Error: {ex.Message}";
|
||||
return View(vm);
|
||||
}
|
||||
vm.StatusMessage = "Connection to the lightning node succeed";
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
@ -84,120 +88,99 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[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")]
|
||||
public async Task<IActionResult> Wallet(string storeId)
|
||||
[Route("{storeId}/wallet/{cryptoCode}")]
|
||||
public async Task<IActionResult> Wallet(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = await _Repo.FindStore(storeId, GetUserId());
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
WalletModel model = new WalletModel();
|
||||
model.ServerUrl = GetStoreUrl(storeId);
|
||||
model.SetCryptoCurrencies(_ExplorerProvider, store.GetDefaultCrypto());
|
||||
model.CryptoCurrency = cryptoCode;
|
||||
return View(model);
|
||||
}
|
||||
|
||||
private string GetStoreUrl(string storeId)
|
||||
{
|
||||
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers(string storeId)
|
||||
{
|
||||
StoreUsersViewModel vm = new StoreUsersViewModel();
|
||||
await FillUsers(storeId, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task FillUsers(string storeId, StoreUsersViewModel vm)
|
||||
{
|
||||
var users = await _Repo.GetStoreUsers(storeId);
|
||||
vm.StoreId = storeId;
|
||||
vm.Users = users.Select(u => new StoreUsersViewModel.StoreUserViewModel()
|
||||
{
|
||||
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]
|
||||
public async Task<IActionResult> ListStores()
|
||||
[Route("{storeId}/users/{userId}/delete")]
|
||||
public async Task<IActionResult> DeleteStoreUser(string storeId, string userId)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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 "--";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{storeId}/delete")]
|
||||
public async Task<IActionResult> DeleteStore(string storeId)
|
||||
{
|
||||
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,6 +196,7 @@ 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.SpeedPolicy = store.SpeedPolicy;
|
||||
@ -225,6 +209,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.AllowCoinConversion = storeBlob.AllowCoinConversion;
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
private void AddPaymentMethods(StoreData store, StoreViewModel vm)
|
||||
{
|
||||
@ -263,6 +248,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, StoreViewModel model)
|
||||
{
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
@ -297,11 +283,13 @@ 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;
|
||||
|
||||
bool newExchange = blob.PreferredExchange != model.PreferredExchange;
|
||||
blob.PreferredExchange = model.PreferredExchange;
|
||||
@ -403,15 +391,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 +481,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 +509,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 +519,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)
|
||||
{
|
||||
|
147
BTCPayServer/Controllers/UserStoresController.cs
Normal file
147
BTCPayServer/Controllers/UserStoresController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -217,6 +217,7 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
public string DefaultLang { get; set; }
|
||||
[DefaultValue(60)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public int MonitoringExpiration
|
||||
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -13,6 +13,8 @@ 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 string CryptoCode { get; set; }
|
||||
public string ServerUrl { get; set; }
|
||||
|
17
BTCPayServer/Models/ServerViewModels/UserViewModel.cs
Normal file
17
BTCPayServer/Models/ServerViewModels/UserViewModel.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class UserViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
[Display(Name = "Is admin")]
|
||||
public bool IsAdmin { get; set; }
|
||||
public string StatusMessage { get; set; }
|
||||
}
|
||||
}
|
28
BTCPayServer/Models/StoreViewModels/StoreUsersViewModel.cs
Normal file
28
BTCPayServer/Models/StoreViewModels/StoreUsersViewModel.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -105,9 +106,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 +126,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -10,25 +10,10 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public class WalletModel
|
||||
{
|
||||
public string ServerUrl { get; set; }
|
||||
public SelectList CryptoCurrencies { get; set; }
|
||||
[Display(Name = "Crypto currency")]
|
||||
public string CryptoCurrency
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
class Format
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
public void SetCryptoCurrencies(ExplorerClientProvider explorerProvider, string selectedScheme)
|
||||
{
|
||||
var choices = explorerProvider.GetAll().Select(o => new Format() { Name = o.Item1.CryptoCode, Value = o.Item1.CryptoCode }).ToArray();
|
||||
var chosen = choices.FirstOrDefault(f => f.Name == selectedScheme) ?? choices.FirstOrDefault();
|
||||
CryptoCurrencies = new SelectList(choices, nameof(chosen.Value), nameof(chosen.Name), chosen);
|
||||
CryptoCurrency = chosen.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -216,6 +216,7 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
var port = info.Port;
|
||||
return new LightningNodeInformation()
|
||||
{
|
||||
NodeId = info.Id,
|
||||
P2PPort = port,
|
||||
Address = address,
|
||||
BlockHeight = info.BlockHeight
|
||||
|
@ -20,5 +20,10 @@ namespace BTCPayServer.Payments.Lightning.CLightning
|
||||
public string NodeId { get; private set; }
|
||||
public string Host { get; private set; }
|
||||
public int Port { get; private set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{NodeId}@{Host}:{Port}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -163,6 +163,7 @@ namespace BTCPayServer.Payments.Lightning.Charge
|
||||
var port = info.Port;
|
||||
return new LightningNodeInformation()
|
||||
{
|
||||
NodeId = info.Id,
|
||||
P2PPort = port,
|
||||
Address = address,
|
||||
BlockHeight = info.BlockHeight
|
||||
|
@ -21,6 +21,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
|
||||
public class LightningNodeInformation
|
||||
{
|
||||
public string NodeId { get; set; }
|
||||
public string Address { get; internal set; }
|
||||
public int P2PPort { get; internal set; }
|
||||
public int BlockHeight { get; set; }
|
||||
|
@ -54,7 +54,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
/// </summary>
|
||||
public bool SkipP2PTest { get; set; }
|
||||
|
||||
public async Task Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new Exception($"Full node not available");
|
||||
@ -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})");
|
||||
@ -91,6 +95,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
throw new Exception($"Error while connecting to the lightning node via {info.Address}:{info.P2PPort} ({ex.Message})");
|
||||
}
|
||||
return new NodeInfo(info.NodeId, info.Address, info.P2PPort);
|
||||
}
|
||||
|
||||
private async Task<bool> TestConnection(string addressStr, int port, CancellationToken cancellation)
|
||||
|
@ -5,15 +5,15 @@
|
||||
"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://localhost:14142/"
|
||||
"applicationUrl": "http://127.0.0.1:14142/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
34
BTCPayServer/Services/LanguageService.cs
Normal file
34
BTCPayServer/Services/LanguageService.cs
Normal file
@ -0,0 +1,34 @@
|
||||
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"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
26
BTCPayServer/StorePolicies.cs
Normal file
26
BTCPayServer/StorePolicies.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
561
BTCPayServer/Views/Invoice/Checkout-Body.cshtml
Normal file
561
BTCPayServer/Views/Invoice/Checkout-Body.cshtml
Normal 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.paymentMethodId != 'BTC_LightningLike'">
|
||||
<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 & 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 →</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,
|
||||
we’ll 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 > 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length > 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"> </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>
|
5
BTCPayServer/Views/Invoice/Checkout-Spinner.cshtml
Normal file
5
BTCPayServer/Views/Invoice/Checkout-Spinner.cshtml
Normal 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>
|
@ -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,79 @@
|
||||
|
||||
<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 & Refund Email</span>
|
||||
<!---->
|
||||
</div>
|
||||
<div class="manual__step-one__instructions">
|
||||
<span class="initial-label">
|
||||
<!---->
|
||||
<span i18n="">Please provide an email address below. We’ll 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 & 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 →</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,
|
||||
we’ll 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 > 0} ng-untouched ng-pristine ng-valid" id="refund-address-input" name="refundAddress" ngclass="{'not-empty': addressValue.length > 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"> </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 }
|
||||
},
|
||||
});
|
||||
|
||||
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 +127,14 @@
|
||||
/^bp-/
|
||||
];
|
||||
var checkoutCtrl = new Vue({
|
||||
i18n: i18n,
|
||||
el: '#checkoutCtrl',
|
||||
components: {
|
||||
qrcode: VueQr
|
||||
},
|
||||
data: {
|
||||
srvModel: srvModel
|
||||
srvModel: srvModel,
|
||||
expiringSoon: false
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -23,7 +23,8 @@
|
||||
<tr>
|
||||
<td>@user.Name</td>
|
||||
<td>@user.Email</td>
|
||||
<td><a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a></td>
|
||||
</tr>}
|
||||
<td><a asp-action="User" asp-route-userId="@user.Id">Modify</a> <span> - </span> <a asp-action="DeleteUser" asp-route-userId="@user.Id">Remove</a></td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
22
BTCPayServer/Views/Server/User.cshtml
Normal file
22
BTCPayServer/Views/Server/User.cshtml
Normal file
@ -0,0 +1,22 @@
|
||||
@model UserViewModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Email;
|
||||
ViewData.AddActivePage(ServerNavPages.Users);
|
||||
}
|
||||
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@Html.Partial("_StatusMessage", Model.StatusMessage)
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label asp-for="IsAdmin"></label>
|
||||
<input asp-for="IsAdmin" type="checkbox" class="form-check" />
|
||||
</div>
|
||||
<button name="command" type="submit" class="btn btn-success" value="Save">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -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>
|
||||
|
@ -16,14 +16,14 @@
|
||||
<div class="alert alert-warning alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<p>
|
||||
<span>A connection to a lightning charge node is required to generate lignting network enabled invoices. <br /></span>
|
||||
<span>A connection to a lightning charge node or clightning unix socket is required to generate lignting network enabled invoices. <br /></span>
|
||||
<span>This is experimental and not advised for production so keep in mind:</span>
|
||||
</p>
|
||||
<ul>
|
||||
<li>You might lose your money</li>
|
||||
<li>The devs of BTCPay Server don't know what they are doing and won't be able to help you if shit hit the fan</li>
|
||||
<li>You approve being #reckless and being the sole responsible party for your loss</li>
|
||||
<li>BTCPay Server relies on a <a href="https://github.com/ElementsProject/lightning-charge">Lightning Charge</a> node</li>
|
||||
<li>BTCPay Server relies on a <a href="https://github.com/ElementsProject/lightning-charge">Lightning Charge</a> node or CLightning unix socket</li>
|
||||
<li>If you have no idea what above mean, search by yourself</li>
|
||||
<li>If you still have no idea how to use lightning, give up for now, we'll make it easier later</li>
|
||||
</ul>
|
||||
|
@ -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>
|
||||
|
@ -14,10 +14,9 @@ namespace BTCPayServer.Views.Stores
|
||||
|
||||
|
||||
public static string Tokens => "Tokens";
|
||||
public static string Wallet => "Wallet";
|
||||
|
||||
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 WalletNavClass(ViewContext viewContext) => PageNavClass(viewContext, Wallet);
|
||||
|
||||
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
|
||||
|
||||
|
56
BTCPayServer/Views/Stores/StoreUsers.cshtml
Normal file
56
BTCPayServer/Views/Stores/StoreUsers.cshtml
Normal file
@ -0,0 +1,56 @@
|
||||
@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)</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>
|
@ -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,14 +93,20 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var scheme in Model.DerivationSchemes)
|
||||
{
|
||||
<tr>
|
||||
<td>@scheme.Crypto</td>
|
||||
<td style="max-width:400px;overflow:hidden;">@scheme.Value</td>
|
||||
<td style="text-align:right"><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>
|
||||
@ -106,7 +116,7 @@
|
||||
<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">
|
||||
@ -119,14 +129,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>
|
||||
|
@ -2,7 +2,6 @@
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData["Title"] = "Manage wallet";
|
||||
ViewData.AddActivePage(StoreNavPages.Wallet);
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
@ -30,45 +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;">
|
||||
<div class="form-group">
|
||||
<label asp-for="CryptoCurrency"></label>
|
||||
<select id="cryptoCurrencies" asp-for="CryptoCurrency" asp-items="Model.CryptoCurrencies" class="form-control"></select>
|
||||
</div>
|
||||
<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>
|
||||
}
|
||||
|
@ -4,6 +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.WalletNavClass(ViewContext)"><a asp-action="Wallet">Wallet</a></li>
|
||||
<li class="@StoreNavPages.UsersNavClass(ViewContext)"><a asp-action="StoreUsers">Users</a></li>
|
||||
</ul>
|
||||
|
||||
|
@ -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>
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
|
||||
var recommendedFees = "";
|
||||
var recommendedBalance = "";
|
||||
var cryptoCode = $("#cryptoCode").val();
|
||||
|
||||
function WriteAlert(type, message) {
|
||||
$(".alert").removeClass("alert-danger");
|
||||
@ -37,7 +38,7 @@
|
||||
}
|
||||
|
||||
var args = "";
|
||||
args += "cryptoCode=" + $("#cryptoCurrencies").val();
|
||||
args += "cryptoCode=" + cryptoCode;
|
||||
args += "&destination=" + $("#destination-textbox").val();
|
||||
args += "&amount=" + $("#amount-textbox").val();
|
||||
args += "&feeRate=" + $("#fee-textbox").val();
|
||||
@ -64,6 +65,10 @@
|
||||
WriteAlert("danger", result.error);
|
||||
} else {
|
||||
WriteAlert("success", 'Transaction broadcasted (' + result.transactionId + ')');
|
||||
$("#fee-textbox").val("");
|
||||
$("#amount-textbox").val("");
|
||||
$("#destination-textbox").val("");
|
||||
$("#substract-checkbox").prop("checked", false);
|
||||
updateInfo();
|
||||
}
|
||||
});
|
||||
@ -85,15 +90,10 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
$("#cryptoCurrencies").on("change", function (elem) {
|
||||
updateInfo();
|
||||
});
|
||||
|
||||
var updateInfo = function () {
|
||||
if (!ledgerDetected)
|
||||
return false;
|
||||
$(".crypto-info").css("display", "none");
|
||||
var cryptoCode = $("#cryptoCurrencies").val();
|
||||
bridge.sendCommand("getinfo", "cryptoCode=" + cryptoCode)
|
||||
.catch(function (reason) { Write('check', 'error', reason); })
|
||||
.then(function (result) {
|
||||
|
@ -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);
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
48
BTCPayServer/wwwroot/js/checkout/langs/de.js
Normal file
48
BTCPayServer/wwwroot/js/checkout/langs/de.js
Normal 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 BTC 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"
|
||||
};
|
48
BTCPayServer/wwwroot/js/checkout/langs/en.js
Normal file
48
BTCPayServer/wwwroot/js/checkout/langs/en.js
Normal 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. We’ll 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 BTC 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"
|
||||
};
|
2
BTCPayServer/wwwroot/js/checkout/langs/es.js
Normal file
2
BTCPayServer/wwwroot/js/checkout/langs/es.js
Normal file
@ -0,0 +1,2 @@
|
||||
const locales_es = {
|
||||
};
|
48
BTCPayServer/wwwroot/js/checkout/langs/fr.js
Normal file
48
BTCPayServer/wwwroot/js/checkout/langs/fr.js
Normal 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 expirée...",
|
||||
"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 et 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."
|
||||
};
|
2
BTCPayServer/wwwroot/js/checkout/langs/ja.js
Normal file
2
BTCPayServer/wwwroot/js/checkout/langs/ja.js
Normal file
@ -0,0 +1,2 @@
|
||||
const locales_ja = {
|
||||
};
|
48
BTCPayServer/wwwroot/js/checkout/langs/nl.js
Normal file
48
BTCPayServer/wwwroot/js/checkout/langs/nl.js
Normal 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."
|
||||
};
|
48
BTCPayServer/wwwroot/js/checkout/langs/pt_BR.js
Normal file
48
BTCPayServer/wwwroot/js/checkout/langs/pt_BR.js
Normal 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 de BTC.",
|
||||
// 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"
|
||||
};
|
12
BTCPayServer/wwwroot/js/checkout/querystring.js
Normal file
12
BTCPayServer/wwwroot/js/checkout/querystring.js
Normal 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]);
|
||||
})();
|
2122
BTCPayServer/wwwroot/vendor/i18next/i18next.js
vendored
Normal file
2122
BTCPayServer/wwwroot/vendor/i18next/i18next.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2
BTCPayServer/wwwroot/vendor/i18next/vue-i18next.js
vendored
Normal file
2
BTCPayServer/wwwroot/vendor/i18next/vue-i18next.js
vendored
Normal 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
|
490
BTCPayServer/wwwroot/vendor/jquery-prettydropdowns/jquery.prettydropdowns.js
vendored
Normal file
490
BTCPayServer/wwwroot/vendor/jquery-prettydropdowns/jquery.prettydropdowns.js
vendored
Normal 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: '✓',
|
||||
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));
|
206
BTCPayServer/wwwroot/vendor/jquery-prettydropdowns/prettydropdowns.css
vendored
Normal file
206
BTCPayServer/wwwroot/vendor/jquery-prettydropdowns/prettydropdowns.css
vendored
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user