mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-16 23:24:25 +01:00
add bitcoin switch plugin
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<method v="2">
|
||||
<option name="Build Solution" enabled="true" />
|
||||
<option name="RunConfigurationTask" enabled="true" run_configuration_name="ConfigBuilder" run_configuration_type="DotNetProject" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -61,6 +61,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.MicroN
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Subscriptions", "Plugins\BTCPayServer.Plugins.Subscriptions\BTCPayServer.Plugins.Subscriptions.csproj", "{994E5D32-849B-4276-82A9-2A18DBC98D39}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.BitcoinSwitch", "Plugins\BTCPayServer.Plugins.BitcoinSwitch\BTCPayServer.Plugins.BitcoinSwitch.csproj", "{B4688F11-33F9-41AB-846E-09CE9ECF3E08}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -293,6 +295,14 @@ Global
|
||||
{994E5D32-849B-4276-82A9-2A18DBC98D39}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{994E5D32-849B-4276-82A9-2A18DBC98D39}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{994E5D32-849B-4276-82A9-2A18DBC98D39}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B4688F11-33F9-41AB-846E-09CE9ECF3E08}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
<!-- -->
|
||||
<!-- Plugin specific properties -->
|
||||
<PropertyGroup>
|
||||
<Product>Bitcoin Switch</Product>
|
||||
<Description>Control harwdare using the POS as a switch</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
<!-- Plugin development properties -->
|
||||
<PropertyGroup>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
<PreserveCompilationContext>false</PreserveCompilationContext>
|
||||
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
|
||||
<ItemDefinitionGroup>
|
||||
<ProjectReference>
|
||||
<Properties>StaticWebAssetsEnabled=false</Properties>
|
||||
<Private>false</Private>
|
||||
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
|
||||
</ProjectReference>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources\**" />
|
||||
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources\js\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Views\Shared\BitcoinSwitch\FileSellerTemplateEditorItemDetail.cshtml" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,67 @@
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Plugins.FileSeller;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Plugins.BitcoinSwitch;
|
||||
|
||||
[AllowAnonymous]
|
||||
public class BitcoinSwitchController:ControllerBase
|
||||
{
|
||||
private readonly BitcoinSwitchService _service;
|
||||
|
||||
public BitcoinSwitchController(BitcoinSwitchService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
[Route("~/apps/{id}/pos/bitcoinswitch")]
|
||||
public async Task Connect(string id)
|
||||
{
|
||||
if (HttpContext.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
await Echo(id, webSocket);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Echo(string id, WebSocket webSocket)
|
||||
{
|
||||
try
|
||||
{
|
||||
_service.AppToSockets.Add(id, webSocket);
|
||||
var buffer = new byte[1024 * 4];
|
||||
var receiveResult = await webSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
|
||||
while (!receiveResult.CloseStatus.HasValue && webSocket.State == WebSocketState.Open)
|
||||
{
|
||||
|
||||
receiveResult = await webSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
}
|
||||
|
||||
await webSocket.CloseAsync(
|
||||
receiveResult.CloseStatus.Value,
|
||||
receiveResult.CloseStatusDescription,
|
||||
CancellationToken.None);
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
_service.AppToSockets.Remove(id, webSocket);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Plugins.FileSeller;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Plugins.BitcoinSwitch;
|
||||
|
||||
public class BitcoinSwitchPlugin : BaseBTCPayServerPlugin
|
||||
{
|
||||
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
|
||||
{
|
||||
new() {Identifier = nameof(BTCPayServer), Condition = ">=2.0.0"}
|
||||
};
|
||||
|
||||
public override void Execute(IServiceCollection applicationBuilder)
|
||||
{
|
||||
applicationBuilder.AddSingleton<BitcoinSwitchService>();
|
||||
applicationBuilder.AddHostedService<BitcoinSwitchService>(provider => provider.GetRequiredService<BitcoinSwitchService>());
|
||||
applicationBuilder.AddUIExtension("app-template-editor-item-detail", "BitcoinSwitch/BitcoinSwitchPluginTemplateEditorItemDetail");
|
||||
|
||||
base.Execute(applicationBuilder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Storage.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Plugins.FileSeller
|
||||
{
|
||||
|
||||
|
||||
|
||||
public class BitcoinSwitchEvent
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public string Message { get; set; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class BitcoinSwitchService : EventHostedServiceBase
|
||||
{
|
||||
private readonly AppService _appService;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
public BitcoinSwitchService(EventAggregator eventAggregator,
|
||||
ILogger<BitcoinSwitchService> logger,
|
||||
AppService appService,
|
||||
InvoiceRepository invoiceRepository) : base(eventAggregator, logger)
|
||||
{
|
||||
_appService = appService;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
public ConcurrentMultiDictionary<string, WebSocket> AppToSockets { get; } = new();
|
||||
|
||||
|
||||
protected override void SubscribeToEvents()
|
||||
{
|
||||
Subscribe<InvoiceEvent>();
|
||||
Subscribe<BitcoinSwitchEvent>();
|
||||
base.SubscribeToEvents();
|
||||
}
|
||||
|
||||
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is BitcoinSwitchEvent bitcoinSwitchEvent)
|
||||
{
|
||||
if (AppToSockets.TryGetValues(bitcoinSwitchEvent.AppId, out var sockets))
|
||||
{
|
||||
foreach (var socket in sockets)
|
||||
{
|
||||
try
|
||||
{
|
||||
await socket.SendAsync(
|
||||
new ArraySegment<byte>(System.Text.Encoding.UTF8.GetBytes(bitcoinSwitchEvent.Message)),
|
||||
WebSocketMessageType.Text, true, cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (evt is not InvoiceEvent invoiceEvent) return;
|
||||
List<AppCartItem> cartItems = null;
|
||||
if (invoiceEvent.Name is not (InvoiceEvent.Completed or InvoiceEvent.MarkedCompleted
|
||||
or InvoiceEvent.Confirmed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice);
|
||||
|
||||
if (!appIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (invoiceEvent.Invoice.Metadata.AdditionalData.TryGetValue("bitcoinswitchactivated", out var activated))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
|
||||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems)))
|
||||
{
|
||||
var items = cartItems ?? new List<AppCartItem>();
|
||||
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) &&
|
||||
!items.Exists(cartItem => cartItem.Id == invoiceEvent.Invoice.Metadata.ItemCode))
|
||||
{
|
||||
items.Add(new AppCartItem()
|
||||
{
|
||||
Id = invoiceEvent.Invoice.Metadata.ItemCode,
|
||||
Count = 1,
|
||||
Price = invoiceEvent.Invoice.Price
|
||||
});
|
||||
}
|
||||
|
||||
var apps = (await _appService.GetApps(appIds)).Select(data =>
|
||||
{
|
||||
switch (data.AppType)
|
||||
{
|
||||
case PointOfSaleAppType.AppType:
|
||||
var possettings = data.GetSettings<PointOfSaleSettings>();
|
||||
return (Data: data, Settings: (object) possettings,
|
||||
Items: AppService.Parse(possettings.Template));
|
||||
case CrowdfundAppType.AppType:
|
||||
var cfsettings = data.GetSettings<CrowdfundSettings>();
|
||||
return (Data: data, Settings: cfsettings,
|
||||
Items: AppService.Parse(cfsettings.PerksTemplate));
|
||||
default:
|
||||
return (null, null, null);
|
||||
}
|
||||
}).Where(tuple => tuple.Data != null && tuple.Items.Any(item =>
|
||||
item.AdditionalData?.ContainsKey("bitcoinswitch_gpio") is true &&
|
||||
items.Exists(cartItem => cartItem.Id == item.Id)));
|
||||
|
||||
|
||||
foreach (var valueTuple in apps)
|
||||
{
|
||||
foreach (var item1 in valueTuple.Items.Where(item =>
|
||||
item.AdditionalData?.ContainsKey("bitcoinswitch_gpio") is true &&
|
||||
items.Exists(cartItem => cartItem.Id == item.Id)))
|
||||
{
|
||||
var appId = valueTuple.Data.Id;
|
||||
var gpio = item1.AdditionalData["bitcoinswitch_gpio"].Value<string>();
|
||||
var duration = item1.AdditionalData.TryGetValue("bitcoinswitch_duration", out var durationObj) &&
|
||||
durationObj.Type == JTokenType.Integer
|
||||
? durationObj.Value<string>()
|
||||
: "5000";
|
||||
|
||||
PushEvent(new BitcoinSwitchEvent()
|
||||
{
|
||||
AppId = appId,
|
||||
Message = $"{gpio}-{duration}.0"
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
invoiceEvent.Invoice.Metadata.SetAdditionalData("bitcoinswitchactivated", "true");
|
||||
await _invoiceRepository.UpdateInvoiceMetadata(invoiceEvent.InvoiceId, invoiceEvent.Invoice.StoreId,
|
||||
invoiceEvent.Invoice.Metadata.ToJObject());
|
||||
|
||||
|
||||
}
|
||||
|
||||
await base.ProcessEvent(evt, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
/// <summary>
|
||||
/// https://stackoverflow.com/a/60719233/275504
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
public class ConcurrentMultiDictionary<TKey, TValue>
|
||||
: IEnumerable<KeyValuePair<TKey, TValue[]>>
|
||||
{
|
||||
private class Bag : HashSet<TValue>
|
||||
{
|
||||
public bool IsDiscarded { get; set; }
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<TKey, Bag> _dictionary;
|
||||
|
||||
public ConcurrentMultiDictionary()
|
||||
{
|
||||
_dictionary = new ConcurrentDictionary<TKey, Bag>();
|
||||
}
|
||||
|
||||
public int Count => _dictionary.Count;
|
||||
|
||||
public bool Add(TKey key, TValue value)
|
||||
{
|
||||
var spinWait = new SpinWait();
|
||||
while (true)
|
||||
{
|
||||
var bag = _dictionary.GetOrAdd(key, _ => new Bag());
|
||||
lock (bag)
|
||||
{
|
||||
if (!bag.IsDiscarded) return bag.Add(value);
|
||||
}
|
||||
spinWait.SpinOnce();
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddOrReplace(TKey key, TValue value)
|
||||
{
|
||||
Remove(key, value);
|
||||
return Add(key, value);
|
||||
}
|
||||
|
||||
public bool Remove(TKey key)
|
||||
{
|
||||
return _dictionary.TryRemove(key, out _);
|
||||
}
|
||||
public bool Remove(TKey key, out TValue[]? items)
|
||||
{
|
||||
if(_dictionary.TryRemove(key, out var x))
|
||||
{
|
||||
items = x.ToArray();
|
||||
return true;
|
||||
|
||||
}
|
||||
items = null;
|
||||
return false;
|
||||
}
|
||||
public bool Remove(TKey key, TValue value)
|
||||
{
|
||||
var spinWait = new SpinWait();
|
||||
while (true)
|
||||
{
|
||||
if (!_dictionary.TryGetValue(key, out var bag)) return false;
|
||||
bool spinAndRetry = false;
|
||||
lock (bag)
|
||||
{
|
||||
if (bag.IsDiscarded)
|
||||
{
|
||||
spinAndRetry = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool valueRemoved = bag.Remove(value);
|
||||
if (!valueRemoved) return false;
|
||||
if (bag.Count != 0) return true;
|
||||
bag.IsDiscarded = true;
|
||||
}
|
||||
}
|
||||
if (spinAndRetry) { spinWait.SpinOnce(); continue; }
|
||||
bool keyRemoved = _dictionary.TryRemove(key, out var currentBag);
|
||||
Debug.Assert(keyRemoved, $"Key {key} was not removed");
|
||||
Debug.Assert(bag == currentBag, $"Removed wrong bag");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetValues(TKey key, out TValue[] values)
|
||||
{
|
||||
if (!_dictionary.TryGetValue(key, out var bag)) { values = null; return false; }
|
||||
bool isDiscarded;
|
||||
lock (bag) { isDiscarded = bag.IsDiscarded; values = bag.ToArray(); }
|
||||
if (isDiscarded) { values = null; return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Contains(TKey key, TValue value)
|
||||
{
|
||||
if (!_dictionary.TryGetValue(key, out var bag)) return false;
|
||||
lock (bag) return !bag.IsDiscarded && bag.Contains(value);
|
||||
}
|
||||
public bool Contains(TKey key, IEnumerable<TValue> value)
|
||||
{
|
||||
if (!_dictionary.TryGetValue(key, out var bag)) return false;
|
||||
lock (bag) return !bag.IsDiscarded && value.Any(bag.Contains);
|
||||
}
|
||||
|
||||
public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key);
|
||||
|
||||
public ICollection<TKey> Keys => _dictionary.Keys;
|
||||
|
||||
public IEnumerator<KeyValuePair<TKey, TValue[]>> GetEnumerator()
|
||||
{
|
||||
foreach (var key in _dictionary.Keys)
|
||||
{
|
||||
if (this.TryGetValues(key, out var values))
|
||||
{
|
||||
yield return new KeyValuePair<TKey, TValue[]>(key, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public bool ContainsValue(TValue value)
|
||||
{
|
||||
return _dictionary.Keys.Any(key => Contains(key, value));
|
||||
}
|
||||
public bool ContainsValue(IEnumerable<TValue> value)
|
||||
{
|
||||
return _dictionary.Keys.Any(key => Contains(key, value));
|
||||
}
|
||||
public IEnumerable<TKey> GetKeysContainingValue(IEnumerable<TValue> value)
|
||||
{
|
||||
return _dictionary.Keys.Where(key => Contains(key, value));
|
||||
}
|
||||
public IEnumerable<TKey> GetKeysContainingValue(TValue value)
|
||||
{
|
||||
return _dictionary.Keys.Where(key => Contains(key, value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Relay
|
||||
{
|
||||
public static class MultiValueDictionaryExtensions
|
||||
{
|
||||
public static ConcurrentMultiDictionary<TKey, TValue> ToMultiValueDictionary<TInput, TKey, TValue>(this IEnumerable<TInput> collection, Func<TInput, TKey> keySelector, Func<TInput, TValue> valueSelector)
|
||||
{
|
||||
var dictionary = new ConcurrentMultiDictionary<TKey, TValue>();
|
||||
foreach (var item in collection)
|
||||
{
|
||||
dictionary.Add(keySelector(item), valueSelector(item));
|
||||
}
|
||||
return dictionary;
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Plugins/BTCPayServer.Plugins.BitcoinSwitch/README.md
Normal file
17
Plugins/BTCPayServer.Plugins.BitcoinSwitch/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Biotcoin Switch Plugin
|
||||
|
||||
This plugin allows you to connect your BTCPay Server to the Bitcoin Switch hardware, developed by the amazing LNURL team.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Go to the "Plugins" page of your BTCPay Server
|
||||
2. Click on install under the "Bitcoin Switch" plugin listing
|
||||
3. Restart BTCPay Server
|
||||
4. Go to your point of sale or crowdfund app settings
|
||||
5. Click on "Edit" on the item/perk you'd like to enable Bitcoin Switch for.
|
||||
6. Specify the hardware GPIO pin and duration of activation
|
||||
7. Close the editor
|
||||
8. Choose Print Display in Point of Sale style (if you want to be able to print LNURL QR codes for each item specifically).
|
||||
9. Save the app
|
||||
10. Your websocket url is the point of sale url, appended with "/bitcoinswitch" and the scheme set to wss:// (e.g. wss://mybtcpay.com/apps/A9xD2nxuWzQTh33E9U6YvyyXrvA/pos/bitcoinswitch)
|
||||
11. Upon purchase (invoice marked as settled), any open websockets will receive the message to activate (io-duration)
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
<template v-if="editingItem">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Bitcoin Switch GPIO</label>
|
||||
<input type="number" inputmode="numeric" min="0" step="1" class="form-control mb-2" :value="editingItem['bitcoinswitch_gpio'] || ''" v-on:change="if(event.target.value) Vue.set(editingItem, 'bitcoinswitch_gpio', event.target.value); else Vue.delete(editingItem, 'bitcoinswitch_gpio');"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="editingItem['bitcoinswitch_gpio']">
|
||||
<label class="form-label">Bitcoin Switch Duration</label>
|
||||
<input type="number" inputmode="numeric" min="1" step="1000" class="form-control mb-2" :value="editingItem['bitcoinswitch_duration'] || ''" asp-items="files" class="form-select w-auto" v-on:change="if(event.target.value) Vue.set(editingItem, 'bitcoinswitch_duration', event.target.value); else Vue.delete(editingItem, 'bitcoinswitch_duration');"/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
@using BTCPayServer.Abstractions.Services
|
||||
@inject Safe Safe
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, BTCPayServer
|
||||
@addTagHelper *, BTCPayServer.Abstractions
|
||||
Reference in New Issue
Block a user