Compare commits
19 Commits
fix-store-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
395495fef5 | ||
|
|
aee8c5f032 | ||
|
|
0785a1ab39 | ||
|
|
ae06fd4404 | ||
|
|
e2d9b4e87c | ||
|
|
621adc1804 | ||
|
|
831f1e2c5b | ||
|
|
9f56451b41 | ||
|
|
427ba10f5e | ||
|
|
409822d1a2 | ||
|
|
1e84841af1 | ||
|
|
29136fe4c4 | ||
|
|
1b462e81da | ||
|
|
e75d011eb4 | ||
|
|
ef534215f9 | ||
|
|
b147ddba3f | ||
|
|
fdd3e0fc33 | ||
|
|
797e7a33c7 | ||
|
|
f4f988b113 |
9
.github/workflows/build-test.yml
vendored
9
.github/workflows/build-test.yml
vendored
@ -7,7 +7,7 @@ on:
|
||||
- '**/*.md'
|
||||
- '**/*.gitignore'
|
||||
- '**/*.gitattributes'
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
|
||||
@ -36,7 +36,9 @@ jobs:
|
||||
run: dotnet build --configuration Release BTCPayApp.Server
|
||||
# Setup infrastructure
|
||||
- name: Start containers
|
||||
run: docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
|
||||
run: |
|
||||
docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" build
|
||||
docker compose -f "submodules/btcpayserver/BTCPayServer.Tests/docker-compose.yml" up -d dev
|
||||
- name: Start BTCPay
|
||||
run: |
|
||||
./setup.sh
|
||||
@ -65,6 +67,9 @@ jobs:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Install workloads
|
||||
run: dotnet workload install maui
|
||||
- name: Clean before build
|
||||
run: |
|
||||
dotnet clean BTCPayApp.Maui/BTCPayApp.Maui.csproj
|
||||
- name: Build
|
||||
# TODO: Use proper keystore once we switch to real releases
|
||||
# https://learn.microsoft.com/en-us/dotnet/maui/android/deployment/publish-cli?view=net-maui-8.0#code-try-4
|
||||
|
||||
@ -70,16 +70,20 @@ public class BTCPayAppClient(string baseUri, string? apiKey = null, HttpClient?
|
||||
return await SendHttpRequest<JObject?>(path, payload, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<JObject?> CreatePosInvoice(CreatePosInvoiceRequest req, CancellationToken cancellation = default)
|
||||
public async Task<JObject?> CreatePosInvoice(Models.CreatePosInvoiceRequest req, CancellationToken cancellation = default)
|
||||
{
|
||||
var query = new Dictionary<string, object>();
|
||||
if (req.Total != null) query.Add("amount", req.Total.Value.ToString(CultureInfo.InvariantCulture));
|
||||
if (req.DiscountPercent != null) query.Add("discount", req.DiscountPercent.Value.ToString(CultureInfo.InvariantCulture));
|
||||
if (req.Tip != null) query.Add("tip", req.Tip.Value.ToString(CultureInfo.InvariantCulture));
|
||||
if (req.PosData != null) query.Add("posData", req.PosData);
|
||||
return await SendHttpRequest<JObject?>($"apps/{req.AppId}/pos/light", query, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public async Task<string> SubmitLNURLWithdrawForInvoice(SubmitLnUrlRequest req, CancellationToken cancellation = default)
|
||||
{
|
||||
return await SendHttpRequest<string>($"plugins/NFC", req, HttpMethod.Post, cancellation);
|
||||
}
|
||||
|
||||
public virtual async Task<T> UploadFileRequest<T>(string apiPath, StreamContent fileContent, string fileName, string mimeType, CancellationToken token = default)
|
||||
{
|
||||
using MultipartFormDataContent multipartContent = new();
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
|
||||
namespace BTCPayApp.Core.BTCPayServer;
|
||||
public interface INfcService
|
||||
{
|
||||
event EventHandler<string> OnNfcDataReceived;
|
||||
void StartNfc();
|
||||
void Dispose();
|
||||
}
|
||||
15
BTCPayApp.Core/Contracts/INfcInterface.cs
Normal file
15
BTCPayApp.Core/Contracts/INfcInterface.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using BTCPayApp.Core.Models;
|
||||
|
||||
namespace BTCPayApp.Core.Contracts;
|
||||
public interface INfcService: IDisposable
|
||||
{
|
||||
event EventHandler<NfcCardData> OnNfcDataReceived;
|
||||
void StartNfc();
|
||||
void EndNfc();
|
||||
}
|
||||
|
||||
public class NfcCardData
|
||||
{
|
||||
public string Message { get; set; }
|
||||
public byte[] Payload { get; set; }
|
||||
}
|
||||
17
BTCPayApp.Core/Models/CreatePosInvoiceRequest.cs
Normal file
17
BTCPayApp.Core/Models/CreatePosInvoiceRequest.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayApp.Core.Models;
|
||||
|
||||
public class CreatePosInvoiceRequest
|
||||
{
|
||||
public string? AppId { get; set; }
|
||||
public List<AppCartItem>? Cart { get; set; }
|
||||
public int? DiscountPercent { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? DiscountAmount { get; set; }
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal? Tip { get; set; }
|
||||
public string? PosData { get; set; }
|
||||
}
|
||||
28
BTCPayApp.Core/Models/NfcLnUrlRecord.cs
Normal file
28
BTCPayApp.Core/Models/NfcLnUrlRecord.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayApp.Core.Models
|
||||
{
|
||||
public class NfcLnUrlRecord
|
||||
{
|
||||
public byte[]? Payload { get; set; }
|
||||
|
||||
//
|
||||
// Summary:
|
||||
// LnUrl
|
||||
public string? LnUrl { get; set; }
|
||||
|
||||
//
|
||||
// Summary:
|
||||
// String formatted payload
|
||||
public string? Message { get; set; }
|
||||
|
||||
//
|
||||
// Summary:
|
||||
// Two letters ISO 639-1 Language Code (ex: en, fr, de...)
|
||||
public string? LanguageCode { get; set; }
|
||||
}
|
||||
}
|
||||
15
BTCPayApp.Core/Models/SubmitLnUrlRequest.cs
Normal file
15
BTCPayApp.Core/Models/SubmitLnUrlRequest.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayApp.Core.Models
|
||||
{
|
||||
public class SubmitLnUrlRequest
|
||||
{
|
||||
public string? Lnurl { get; set; }
|
||||
public string? InvoiceId { get; set; }
|
||||
public long? Amount { get; set; }
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
using BTCPayApp.Core.Extensions;
|
||||
using BTCPayApp.Maui.Services;
|
||||
using BTCPayApp.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Maui.LifecycleEvents;
|
||||
using Plugin.Fingerprint;
|
||||
using BTCPayApp.Core.BTCPayServer;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
|
||||
|
||||
|
||||
|
||||
#if ANDROID
|
||||
@ -60,7 +61,7 @@ public static class MauiProgram
|
||||
|
||||
var context = Android.App.Application.Context;
|
||||
var intent = new Intent(context, typeof(HubConnectionForegroundService));
|
||||
context.StartForegroundService(intent); // App is in background — start service
|
||||
//context.StartForegroundService(intent); // App is in background — start service
|
||||
})
|
||||
.OnDestroy(activity =>
|
||||
{
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:supportsRtl="true">
|
||||
<service android:name=".HubConnectionForegroundService" android:enabled="true" android:exported="false"/>
|
||||
</application>
|
||||
<service android:name=".HubConnectionForegroundService" android:enabled="true" android:exported="false"/>
|
||||
</application>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
<data android:scheme="mailto" />
|
||||
</intent>
|
||||
</queries>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using Android.App;
|
||||
using Android.Content;
|
||||
using Android.Content.PM;
|
||||
using Android.Nfc;
|
||||
using Android.OS;
|
||||
using Plugin.NFC;
|
||||
|
||||
@ -16,6 +15,15 @@ public class MainActivity : MauiAppCompatActivity
|
||||
{
|
||||
CrossNFC.Init(this);
|
||||
base.OnCreate(savedInstanceState);
|
||||
|
||||
#if DEBUG
|
||||
// Enable WebView debugging
|
||||
if (Build.VERSION.SdkInt >= BuildVersionCodes.Kitkat)
|
||||
{
|
||||
Android.Webkit.WebView.SetWebContentsDebuggingEnabled(true); // Fully qualify the WebView class
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
|
||||
@ -1,98 +1,48 @@
|
||||
using BTCPayApp.Core.BTCPayServer;
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Models;
|
||||
using Plugin.NFC;
|
||||
|
||||
public class NfcService : INfcService
|
||||
public class NfcService : INfcService, IDisposable
|
||||
{
|
||||
public event EventHandler<string> OnNfcDataReceived = delegate { };
|
||||
public event EventHandler<NfcCardData> OnNfcDataReceived = delegate { };
|
||||
|
||||
public void StartNfc()
|
||||
{
|
||||
if (!CrossNFC.IsSupported)
|
||||
{
|
||||
OnNfcDataReceived?.Invoke(this, "NFC not supported on this device");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CrossNFC.Current.IsEnabled)
|
||||
{
|
||||
OnNfcDataReceived?.Invoke(this, "NFC is disabled. Please enable it.");
|
||||
return;
|
||||
}
|
||||
|
||||
CrossNFC.Current.OnMessageReceived += Current_OnMessageReceived;
|
||||
CrossNFC.Current.StartListening();
|
||||
}
|
||||
public void EndNfc()
|
||||
{
|
||||
CrossNFC.Current.StopListening();
|
||||
Dispose();
|
||||
}
|
||||
|
||||
private void Current_OnMessageReceived(ITagInfo tagInfo)
|
||||
{
|
||||
if (tagInfo == null || tagInfo.Records == null || !tagInfo.Records.Any())
|
||||
{
|
||||
OnNfcDataReceived?.Invoke(this, "No NDEF data found on the tag");
|
||||
//throw new ArgumentException("No NFC records found in the tag info.");
|
||||
return;
|
||||
}
|
||||
|
||||
var record = tagInfo.Records[0];
|
||||
|
||||
foreach (var record in tagInfo.Records)
|
||||
// Pass the raw tag info up - let the consumer decide what to do with it
|
||||
OnNfcDataReceived?.Invoke(this, new NfcCardData
|
||||
{
|
||||
string data = string.Empty;
|
||||
|
||||
// Handle URI records
|
||||
if (record.TypeFormat == NFCNdefTypeFormat.Uri)
|
||||
{
|
||||
data = record.Message; // e.g., "lightning:lnbc..."
|
||||
data = $"URI: {data}";
|
||||
}
|
||||
// Handle Well-Known Text records
|
||||
else if (record.TypeFormat == NFCNdefTypeFormat.WellKnown && record.TypeFormat == NFCNdefTypeFormat.WellKnown && record.Payload != null)
|
||||
{
|
||||
// Decode text payload (Well-Known Text records have a specific format)
|
||||
data = DecodeTextRecord(record);
|
||||
data = $"Text: {data}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle other types (e.g., Unknown, Mime)
|
||||
data = $"Unsupported record type: {record.TypeFormat}";
|
||||
}
|
||||
|
||||
// Check for Lightning-specific data
|
||||
if (data.Contains("lightning:") || data.Contains("lnbc"))
|
||||
{
|
||||
OnNfcDataReceived?.Invoke(this, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnNfcDataReceived?.Invoke(this, $"Unsupported NFC data: {data}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to decode Well-Known Text records
|
||||
private string DecodeTextRecord(NFCNdefRecord record)
|
||||
{
|
||||
if (record.Payload == null || record.Payload.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
// Well-Known Text record format: [Status Byte][Language Code][Text]
|
||||
byte statusByte = record.Payload[0];
|
||||
int languageCodeLength = statusByte & 0x3F; // Lower 6 bits indicate language code length
|
||||
int textStartIndex = 1 + languageCodeLength;
|
||||
|
||||
if (textStartIndex >= record.Payload.Length)
|
||||
return string.Empty;
|
||||
|
||||
// Extract text (UTF-8 encoded)
|
||||
string text = System.Text.Encoding.UTF8.GetString(
|
||||
record.Payload,
|
||||
textStartIndex,
|
||||
record.Payload.Length - textStartIndex);
|
||||
return text;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Error decoding text record";
|
||||
}
|
||||
Message = record.Message,
|
||||
Payload = record.Payload
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Photino.Blazor;
|
||||
using Photino.NET;
|
||||
using Size = System.Drawing.Size;
|
||||
|
||||
namespace BTCPayApp.Photino;
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Extensions;
|
||||
using BTCPayApp.Desktop;
|
||||
using BTCPayApp.Server.Services;
|
||||
using BTCPayApp.UI;
|
||||
using Serilog;
|
||||
|
||||
@ -17,6 +19,8 @@ builder.Services.ConfigureBTCPayAppCore();
|
||||
builder.Services.AddDangerousSSLSettingsForDev();
|
||||
#endif
|
||||
|
||||
builder.Services.AddSingleton<INfcService, NfcService>();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
var app = builder.Build();
|
||||
if (!app.Environment.IsDevelopment())
|
||||
|
||||
23
BTCPayApp.Server/Services/NfcService.cs
Normal file
23
BTCPayApp.Server/Services/NfcService.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using BTCPayApp.Core.Contracts;
|
||||
using BTCPayApp.Core.Models;
|
||||
|
||||
namespace BTCPayApp.Server.Services
|
||||
{
|
||||
public class NfcService : INfcService
|
||||
{
|
||||
public event EventHandler<NfcCardData> OnNfcDataReceived = delegate { };
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void EndNfc()
|
||||
{
|
||||
}
|
||||
|
||||
public void StartNfc()
|
||||
{
|
||||
// NFC for web is supported within the btcpayserver iframe, so we dont need to implement NFC support here.
|
||||
}
|
||||
}
|
||||
}
|
||||
60
BTCPayApp.UI/Components/BTCPaySupporters.razor
Normal file
60
BTCPayApp.UI/Components/BTCPaySupporters.razor
Normal file
File diff suppressed because one or more lines are too long
@ -1,4 +1,8 @@
|
||||
@using BTCPayApp.Core.Auth
|
||||
@using BTCPayApp.UI.Features
|
||||
@using BTCPayServer.Client.Models
|
||||
@inject IAccountManager AccountManager
|
||||
@inject IDispatcher Dispatcher
|
||||
<div @attributes="InputAttributes" class="@CssClass">
|
||||
@if (Invoices is not null)
|
||||
{
|
||||
@ -44,4 +48,21 @@
|
||||
public Dictionary<string, object>? InputAttributes { get; set; }
|
||||
|
||||
private string CssClass => $"invoice-list {(InputAttributes?.ContainsKey("class") is true ? InputAttributes["class"] : "")}".Trim();
|
||||
|
||||
private string? StoreId => AccountManager.CurrentStore?.Id;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(StoreId))
|
||||
{
|
||||
Dispatcher.Dispatch(new StoreState.FetchInvoices(StoreId));
|
||||
}
|
||||
}
|
||||
}
|
||||
private async void OnStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
@using System.Globalization
|
||||
@using System.Text.Json
|
||||
@using System.Text.Json.Nodes
|
||||
@using BTCPayApp.Core.Models
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Client.Models
|
||||
@inject IJSRuntime JS
|
||||
@ -97,18 +98,21 @@
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary mx-3" type="submit" disabled="@(IsSubmitting || amount == 0)" id="pay-button" @onclick="HandleSubmit">
|
||||
@if (IsSubmitting)
|
||||
{
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Charge</span>
|
||||
}
|
||||
</button>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<button class="btn btn-lg btn-primary mx-3" type="submit" disabled="@(IsSubmitting || amount == 0)" id="pay-button" @onclick="HandleSubmit">
|
||||
@if (IsSubmitting)
|
||||
{
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Charge</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (CanAccessRecentTransactions)
|
||||
{
|
||||
@ -248,7 +252,7 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
#nullable enable
|
||||
#nullable enable
|
||||
[Parameter, EditorRequired]
|
||||
public string StoreId { get; set; } = null!;
|
||||
[Parameter, EditorRequired]
|
||||
@ -274,7 +278,7 @@
|
||||
[Parameter]
|
||||
public EventCallback LoadRecentTransactions { get; set; }
|
||||
[Parameter]
|
||||
public EventCallback<CreatePosInvoiceRequest> CreateInvoice { get; set; }
|
||||
public EventCallback<Core.Models.CreatePosInvoiceRequest> CreateInvoice { get; set; }
|
||||
[Parameter]
|
||||
public IEnumerable<RecentTransaction>? RecentTransactions { get; set; }
|
||||
[Parameter]
|
||||
@ -436,8 +440,6 @@
|
||||
{
|
||||
var data = new JsonObject
|
||||
{
|
||||
["subTotal"] = GetAmount(),
|
||||
["total"] = GetTotal(),
|
||||
["cart"] = JsonValue.Create(Model.Cart)
|
||||
};
|
||||
|
||||
@ -667,16 +669,12 @@
|
||||
StateHasChanged();
|
||||
if (CreateInvoice.HasDelegate)
|
||||
{
|
||||
var req = new CreatePosInvoiceRequest
|
||||
var req = new Core.Models.CreatePosInvoiceRequest
|
||||
{
|
||||
Cart = Model.Cart,
|
||||
DiscountPercent = Model.DiscountPercent,
|
||||
TipPercent = Model.TipPercent,
|
||||
Tip = GetTip(),
|
||||
Amounts = GetAmounts(),
|
||||
DiscountAmount = GetDiscount(),
|
||||
Subtotal = GetAmount(),
|
||||
Total = GetTotal(),
|
||||
PosData = GetData()
|
||||
};
|
||||
await CreateInvoice.InvokeAsync(req);
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
@attribute [Route(Routes.Checkout)]
|
||||
@using BTCPayApp.Core
|
||||
@using BTCPayApp.Core.Auth
|
||||
@using BTCPayApp.Core.BTCPayServer
|
||||
@using BTCPayApp.Core.Contracts
|
||||
@using BTCPayApp.Core.Models
|
||||
@inject IJSRuntime JS
|
||||
@inject IAccountManager AccountManager
|
||||
@inject INfcService NfcService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||
|
||||
<PageTitle>Checkout</PageTitle>
|
||||
|
||||
<iframe id="AppCheckout" name="checkout" allowfullscreen src="@CheckoutUrl" @onload="OnIframeLoad"></iframe>
|
||||
|
||||
@if (!_iframeLoaded)
|
||||
{
|
||||
<section class="loading-container">
|
||||
@ -15,13 +22,66 @@
|
||||
<div class="fs-4">Loading</div>
|
||||
</section>
|
||||
}
|
||||
<iframe id="AppCheckout" name="checkout" allow="clipboard-read;clipboard-write;nfc" allowfullscreen src="@CheckoutUrl" @onload="OnIframeLoad"></iframe>
|
||||
|
||||
@if (_toast.IsVisible)
|
||||
{
|
||||
<div class="toast-container @(_toast.IsError ? "toast-error" : "toast-success")">
|
||||
<div class="toast-message">@_toast.Message</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
max-width: 90%;
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
animation: slideUp 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: 1px solid #1e7e34;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: 1px solid #c82333;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string? InvoiceId { get; set; }
|
||||
[Parameter, EditorRequired] public string? InvoiceId { get; set; }
|
||||
|
||||
private bool _iframeLoaded;
|
||||
private bool _scanInProgress;
|
||||
private Toast _toast = new();
|
||||
|
||||
private string BaseUri => AccountManager.Account!.BaseUri;
|
||||
private string? CheckoutUrl => string.IsNullOrEmpty(InvoiceId) ? null : $"{BaseUri}i/{InvoiceId}";
|
||||
@ -33,20 +93,63 @@
|
||||
|
||||
NfcService.OnNfcDataReceived += OnNfcDataReceived;
|
||||
NfcService.StartNfc();
|
||||
_toast.OnUpdated = StateHasChanged;
|
||||
}
|
||||
|
||||
private async void OnNfcDataReceived(object? sender, string data)
|
||||
private async void OnNfcDataReceived(object? sender, NfcCardData record)
|
||||
{
|
||||
if (_scanInProgress)
|
||||
return;
|
||||
|
||||
_scanInProgress = true;
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("Interop.sendNfcDataToIframe", "AppCheckout", data);
|
||||
if (record == null || record.Message == null)
|
||||
{
|
||||
_toast.ShowError("No NFC data found");
|
||||
return;
|
||||
}
|
||||
|
||||
var message = record.Message;
|
||||
if (string.IsNullOrEmpty(message))
|
||||
{
|
||||
_toast.ShowError("Empty NFC message");
|
||||
return;
|
||||
}
|
||||
|
||||
var btcPayClient = new BTCPayAppClient(BaseUri);
|
||||
var req = new SubmitLnUrlRequest
|
||||
{
|
||||
InvoiceId = InvoiceId,
|
||||
Lnurl = message
|
||||
};
|
||||
|
||||
_toast.ShowSuccess("Submitting NFC data to Server", 5000);
|
||||
|
||||
var result = await btcPayClient.SubmitLNURLWithdrawForInvoice(req);
|
||||
if (result == null)
|
||||
{
|
||||
_toast.Hide();
|
||||
NfcService.EndNfc();
|
||||
NfcService.OnNfcDataReceived -= OnNfcDataReceived;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await JS.InvokeVoidAsync("Interop.sendNfcErrorToIframe", "AppCheckout", ex.Message);
|
||||
try
|
||||
{
|
||||
var errorFromServer = ex.Message.Split("\"")[1].Trim('"');
|
||||
_toast.ShowError("ERROR: " + errorFromServer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_toast.ShowError("NFC call to server ERROR");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_scanInProgress = false;
|
||||
}
|
||||
|
||||
//StateHasChanged();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@ -54,4 +157,59 @@
|
||||
NfcService.OnNfcDataReceived -= OnNfcDataReceived;
|
||||
}
|
||||
|
||||
public class Toast
|
||||
{
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public bool IsVisible { get; private set; }
|
||||
public bool IsError { get; private set; }
|
||||
public string Message { get; private set; } = string.Empty;
|
||||
|
||||
public Action? OnUpdated { get; set; }
|
||||
|
||||
public void ShowSuccess(string message, int delayTime = 3000)
|
||||
{
|
||||
Show(message, false);
|
||||
}
|
||||
|
||||
public void ShowError(string message, int delayTime = 3000)
|
||||
{
|
||||
Show(message, true);
|
||||
}
|
||||
|
||||
private void Show(string message, bool isError, int delayTime = 3000)
|
||||
{
|
||||
_cts?.Cancel();
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
Message = message;
|
||||
IsError = isError;
|
||||
IsVisible = true;
|
||||
OnUpdated?.Invoke();
|
||||
|
||||
var token = _cts.Token;
|
||||
_ = HideAfterDelayAsync(token);
|
||||
}
|
||||
|
||||
private async Task HideAfterDelayAsync(CancellationToken token, int delayTime = 3000)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(delayTime, token);
|
||||
if (!token.IsCancellationRequested)
|
||||
{
|
||||
IsVisible = false;
|
||||
OnUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
_cts?.Cancel();
|
||||
IsVisible = false;
|
||||
OnUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,28 @@
|
||||
.container {
|
||||
max-width: 560px;
|
||||
padding-top: var(--btcpay-space-l);
|
||||
padding-bottom: var(--btcpay-space-l);
|
||||
max-width: 560px;
|
||||
padding-top: var(--btcpay-space-l);
|
||||
padding-bottom: var(--btcpay-space-l);
|
||||
}
|
||||
|
||||
iframe {
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%; /* fallback for older webviews */
|
||||
height: calc(100% - var(--navbar-bottom-height));
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%; /* fallback for older webviews */
|
||||
height: calc(100% - var(--navbar-bottom-height));
|
||||
}
|
||||
|
||||
|
||||
.section-complete ::deep .checkout-complete {
|
||||
width: 110px; /* Increase size */
|
||||
height: 100px;
|
||||
fill: var(--btcpay-primary) !important; /* Change fill color */
|
||||
stroke: var(--btcpay-primary) !important; /* Change stroke color */
|
||||
/*fill: var(--btcpay-primary);*/
|
||||
}
|
||||
|
||||
.section-complete {
|
||||
height: 80vh;
|
||||
}
|
||||
@ -36,6 +36,10 @@
|
||||
{
|
||||
<Alert Type="danger">@Error</Alert>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(_successMessage))
|
||||
{
|
||||
<Alert Type="success">@_successMessage</Alert>
|
||||
}
|
||||
@if (Invoice is not null)
|
||||
{
|
||||
@if (CanCheckout)
|
||||
@ -56,45 +60,64 @@
|
||||
<InvoiceStatusDisplay Invoice="@Invoice"/>
|
||||
</div>
|
||||
|
||||
<div class="invoice-actions mt-3 mb-3">
|
||||
<div class="row g-2">
|
||||
@if (!string.IsNullOrEmpty(ReceiptUrl))
|
||||
{
|
||||
<div class="col-4">
|
||||
<a class="btn btn-outline-secondary btn-sm w-100" href="@ReceiptUrl" rel="noreferrer noopener" target="_blank">Receipt</a>
|
||||
</div>
|
||||
}
|
||||
@if (!Invoice.Archived)
|
||||
{
|
||||
<div class="col-4">
|
||||
<button class="btn btn-outline-secondary btn-sm w-100" @onclick="ToggleArchive">
|
||||
Archive
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">General Information</h4>
|
||||
<div class="box">
|
||||
<table class="table my-0">
|
||||
<tbody>
|
||||
@if (Invoice.Metadata.TryGetValue("orderId", out var orderId))
|
||||
{
|
||||
@if (Invoice.Metadata.TryGetValue("orderId", out var orderId))
|
||||
{
|
||||
<tr>
|
||||
<th>Order Id</th>
|
||||
<td>
|
||||
@if (Invoice.Metadata.TryGetValue("orderUrl", out var orderUrl))
|
||||
{
|
||||
<a href="@orderUrl" rel="noreferrer noopener" target="_blank">@orderId</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@orderId</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Invoice.Metadata.TryGetValue("paymentRequestId", out var paymentRequestId))
|
||||
{
|
||||
<tr>
|
||||
<th>Payment Request Id</th>
|
||||
<td>@paymentRequestId</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<th>Order Id</th>
|
||||
<th>Created</th>
|
||||
<td>
|
||||
@if (Invoice.Metadata.TryGetValue("orderUrl", out var orderUrl))
|
||||
{
|
||||
<a href="@orderUrl" rel="noreferrer noopener" target="_blank">@orderId</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@orderId</span>
|
||||
}
|
||||
<DateDisplay DateTimeOffset="@Invoice.CreatedTime"/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Invoice.Metadata.TryGetValue("paymentRequestId", out var paymentRequestId))
|
||||
{
|
||||
<tr>
|
||||
<th>Payment Request Id</th>
|
||||
<td>@paymentRequestId</td>
|
||||
<th>Expired</th>
|
||||
<td>
|
||||
<DateDisplay DateTimeOffset="@Invoice.ExpirationTime"/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<th>Created</th>
|
||||
<td>
|
||||
<DateDisplay DateTimeOffset="@Invoice.CreatedTime"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Expired</th>
|
||||
<td>
|
||||
<DateDisplay DateTimeOffset="@Invoice.ExpirationTime"/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -111,27 +134,27 @@
|
||||
<div class="box">
|
||||
<table class="table my-0">
|
||||
<tbody>
|
||||
@if (!string.IsNullOrEmpty(itemCode?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Item code</th>
|
||||
<td>@itemCode</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(itemDesc?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Item Description</th>
|
||||
<td>@itemDesc</td>
|
||||
</tr>
|
||||
}
|
||||
@if (taxIncluded is not null)
|
||||
{
|
||||
<tr>
|
||||
<th>Tax Included</th>
|
||||
<td>@taxIncluded</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(itemCode?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Item code</th>
|
||||
<td>@itemCode</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(itemDesc?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Item Description</th>
|
||||
<td>@itemDesc</td>
|
||||
</tr>
|
||||
}
|
||||
@if (taxIncluded is not null)
|
||||
{
|
||||
<tr>
|
||||
<th>Tax Included</th>
|
||||
<td>@taxIncluded</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -150,80 +173,80 @@
|
||||
Invoice.Metadata.TryGetValue("buyerZip", out var buyerZip);
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerName?.ToString()) || !string.IsNullOrEmpty(buyerEmail?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerPhone?.ToString()) || !string.IsNullOrEmpty(buyerAddress1?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerAddress2?.ToString()) || !string.IsNullOrEmpty(buyerCity?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerState?.ToString()) || !string.IsNullOrEmpty(buyerCountry?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerZip?.ToString()))
|
||||
!string.IsNullOrEmpty(buyerPhone?.ToString()) || !string.IsNullOrEmpty(buyerAddress1?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerAddress2?.ToString()) || !string.IsNullOrEmpty(buyerCity?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerState?.ToString()) || !string.IsNullOrEmpty(buyerCountry?.ToString()) ||
|
||||
!string.IsNullOrEmpty(buyerZip?.ToString()))
|
||||
{
|
||||
<h4 class="mt-4">Buyer Information</h4>
|
||||
<div class="box">
|
||||
<table class="table my-0">
|
||||
<tbody>
|
||||
@if (!string.IsNullOrEmpty(buyerName?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>@buyerName</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerEmail?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td>
|
||||
<a href="mailto:@buyerEmail">@buyerEmail</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerPhone?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Phone</th>
|
||||
<td>@buyerPhone</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerAddress1?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Address 1</th>
|
||||
<td>@buyerAddress1</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerAddress2?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Address 2</th>
|
||||
<td>@buyerAddress2</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerCity?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>City</th>
|
||||
<td>@buyerCity</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerState?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<td>@buyerState</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerCountry?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<td>@buyerCountry</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerZip?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Zip</th>
|
||||
<td>@buyerZip</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerName?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td>@buyerName</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerEmail?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td>
|
||||
<a href="mailto:@buyerEmail">@buyerEmail</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerPhone?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Phone</th>
|
||||
<td>@buyerPhone</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerAddress1?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Address 1</th>
|
||||
<td>@buyerAddress1</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerAddress2?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Address 2</th>
|
||||
<td>@buyerAddress2</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerCity?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>City</th>
|
||||
<td>@buyerCity</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerState?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<td>@buyerState</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerCountry?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<td>@buyerCountry</td>
|
||||
</tr>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(buyerZip?.ToString()))
|
||||
{
|
||||
<tr>
|
||||
<th>Zip</th>
|
||||
<td>@buyerZip</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -327,13 +350,13 @@
|
||||
|
||||
if (!string.IsNullOrEmpty(StoreId) && !string.IsNullOrEmpty(InvoiceId))
|
||||
{
|
||||
if (Invoice == null)
|
||||
Dispatcher.Dispatch(new StoreState.FetchInvoice(StoreId, InvoiceId));
|
||||
Dispatcher.Dispatch(new StoreState.FetchInvoice(StoreId, InvoiceId));
|
||||
if (PaymentMethods == null)
|
||||
Dispatcher.Dispatch(new StoreState.FetchInvoicePaymentMethods(StoreId, InvoiceId));
|
||||
}
|
||||
}
|
||||
|
||||
private string? _successMessage;
|
||||
private string? StoreId => AccountManager.CurrentStore?.Id;
|
||||
private AppUserStoreInfo? StoreInfo => StoreState.Value.StoreInfo;
|
||||
private InvoiceData? Invoice => !string.IsNullOrEmpty(InvoiceId) ? StoreState.Value.GetInvoice(InvoiceId!)?.Data : null;
|
||||
@ -353,4 +376,14 @@
|
||||
|
||||
private string GetTitle() => $"Invoice {Invoice?.Id}".Trim();
|
||||
private bool CanCheckout => Invoice is { Status: InvoiceStatus.New };
|
||||
|
||||
private async Task ToggleArchive()
|
||||
{
|
||||
await AccountManager.GetClient().ArchiveInvoice(StoreId, InvoiceId);
|
||||
_successMessage = "The invoice has been archived and will no longer appear in the invoice list by default";
|
||||
if (!string.IsNullOrEmpty(StoreId) && !string.IsNullOrEmpty(InvoiceId))
|
||||
{
|
||||
Dispatcher.Dispatch(new StoreState.FetchInvoice(StoreId, InvoiceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@ else
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task CreateInvoice(CreatePosInvoiceRequest req)
|
||||
private async Task CreateInvoice(Core.Models.CreatePosInvoiceRequest req)
|
||||
{
|
||||
_errorMessage = null;
|
||||
req.AppId = AppId;
|
||||
|
||||
@ -79,6 +79,10 @@
|
||||
<NavLink href="@Routes.Connect">Connect to a BTCPay Server</NavLink>
|
||||
</p>
|
||||
}
|
||||
|
||||
<div class="mt-5">
|
||||
<BTCPaySupporters />
|
||||
</div>
|
||||
</ValidationEditContext>
|
||||
<QrScanModal OnScan="@OnQrCodeScan"/>
|
||||
</NotAuthorized>
|
||||
|
||||
@ -58,6 +58,10 @@
|
||||
<p class="mt-4 text-center">
|
||||
<NavLink href="@GetLoginUrl()">Back to login</NavLink>
|
||||
</p>
|
||||
|
||||
<div class="mt-5">
|
||||
<BTCPaySupporters />
|
||||
</div>
|
||||
</ValidationEditContext>
|
||||
</NotAuthorized>
|
||||
<Authorized>
|
||||
|
||||
34
BTCPayApp.UI/Services/ToastService.cs
Normal file
34
BTCPayApp.UI/Services/ToastService.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace BTCPayApp.UI.Services
|
||||
{
|
||||
public class ToastService
|
||||
{
|
||||
public event Action<string, bool>? OnToastShow;
|
||||
public event Action? OnToastHide;
|
||||
|
||||
public void ShowToast(string message, bool isError = false)
|
||||
{
|
||||
OnToastShow?.Invoke(message, isError);
|
||||
|
||||
// Auto-hide after 3 seconds
|
||||
_ = Task.Delay(3000).ContinueWith(_ => OnToastHide?.Invoke());
|
||||
}
|
||||
|
||||
public void ShowSuccess(string message)
|
||||
{
|
||||
ShowToast(message, false);
|
||||
}
|
||||
|
||||
public void ShowError(string message)
|
||||
{
|
||||
ShowToast(message, true);
|
||||
}
|
||||
|
||||
public void HideToast()
|
||||
{
|
||||
OnToastHide?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -10,24 +10,26 @@ Interop = {
|
||||
if (!$el) return console.warn('Selector does not exist:', selector);
|
||||
$el.contentWindow.postMessage(JSON.stringify({ context: 'btcpayapp' }), origin);
|
||||
},
|
||||
sendNfcDataToIframe: function (iframeId, data) {
|
||||
sendNfcDataToIframe: function (iframeId, data, origin) {
|
||||
const iframe = document.getElementById(iframeId);
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage({
|
||||
action: 'nfc:data',
|
||||
data: data
|
||||
}, window.location.origin);
|
||||
data: data,
|
||||
origin: origin
|
||||
}, origin);
|
||||
} else {
|
||||
console.error('Iframe not found or inaccessible');
|
||||
}
|
||||
},
|
||||
sendNfcErrorToIframe: function (iframeId, error) {
|
||||
sendNfcErrorToIframe: function (iframeId, error, origin) {
|
||||
const iframe = document.getElementById(iframeId);
|
||||
if (iframe && iframe.contentWindow) {
|
||||
iframe.contentWindow.postMessage({
|
||||
action: 'nfc:error',
|
||||
data: error
|
||||
}, window.location.origin);
|
||||
data: error,
|
||||
origin: origin
|
||||
}, origin);
|
||||
}
|
||||
},
|
||||
openModal(selector) {
|
||||
|
||||
@ -24,7 +24,7 @@ docker-compose up dev
|
||||
Now you can open up the IDE and run `DEV ALL` profile which builds both the App and the BTCPay Server.
|
||||
|
||||
The app should open in the browser and you should see the Welcome screen.
|
||||
Click the Connect button, use `http://localhost:14142` as the server URL and an existing account for the server.
|
||||
Click the Connect button, use `https://localhost:14142` as the server URL and an existing account for the server.
|
||||
|
||||
## Lightning Channels
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user