forked from Kieran/void.cat
Finish stike paywall
This commit is contained in:
parent
0eda25ba00
commit
edf26c2137
@ -37,7 +37,7 @@ public class DownloadController : Controller
|
||||
var voidFile = await SetupDownload(gid);
|
||||
if (voidFile == default) return;
|
||||
|
||||
var egressReq = new EgressRequest(gid, GetRanges(Request, (long)voidFile!.Metadata!.Size));
|
||||
var egressReq = new EgressRequest(gid, GetRanges(Request, (long) voidFile!.Metadata!.Size));
|
||||
if (egressReq.Ranges.Count() > 1)
|
||||
{
|
||||
_logger.LogWarning("Multi-range request not supported!");
|
||||
@ -49,10 +49,10 @@ public class DownloadController : Controller
|
||||
}
|
||||
else if (egressReq.Ranges.Count() == 1)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.PartialContent;
|
||||
Response.StatusCode = (int) HttpStatusCode.PartialContent;
|
||||
if (egressReq.Ranges.Sum(a => a.Size) == 0)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
|
||||
Response.StatusCode = (int) HttpStatusCode.RequestedRangeNotSatisfiable;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -79,26 +79,17 @@ public class DownloadController : Controller
|
||||
if (meta == null)
|
||||
{
|
||||
Response.StatusCode = 404;
|
||||
return null;
|
||||
return default;
|
||||
}
|
||||
|
||||
// check paywall
|
||||
if (meta.Paywall != default)
|
||||
{
|
||||
var orderId = Request.Headers.GetHeader("V-OrderId");
|
||||
if (string.IsNullOrEmpty(orderId))
|
||||
var orderId = Request.Headers.GetHeader("V-OrderId") ?? Request.Query["orderId"];
|
||||
if (!await IsOrderPaid(orderId))
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var order = await _paywall.GetOrder(orderId.FromBase58Guid());
|
||||
if (order?.Status != PaywallStatus.Paid)
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.PaymentRequired;
|
||||
return null;
|
||||
}
|
||||
Response.StatusCode = (int) HttpStatusCode.PaymentRequired;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,6 +100,20 @@ public class DownloadController : Controller
|
||||
return meta;
|
||||
}
|
||||
|
||||
private async ValueTask<bool> IsOrderPaid(string orderId)
|
||||
{
|
||||
if (Guid.TryParse(orderId, out var oid))
|
||||
{
|
||||
var order = await _paywall.GetOrder(oid);
|
||||
if (order?.Status == PaywallOrderStatus.Paid)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerable<RangeRequest> GetRanges(HttpRequest request, long totalSize)
|
||||
{
|
||||
foreach (var rangeHeader in request.Headers.Range)
|
||||
@ -134,4 +139,4 @@ public class DownloadController : Controller
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,12 +14,15 @@ namespace VoidCat.Controllers
|
||||
private readonly IFileStore _storage;
|
||||
private readonly IFileMetadataStore _metadata;
|
||||
private readonly IPaywallStore _paywall;
|
||||
private readonly IPaywallFactory _paywallFactory;
|
||||
|
||||
public UploadController(IFileStore storage, IFileMetadataStore metadata, IPaywallStore paywall)
|
||||
public UploadController(IFileStore storage, IFileMetadataStore metadata, IPaywallStore paywall,
|
||||
IPaywallFactory paywallFactory)
|
||||
{
|
||||
_storage = storage;
|
||||
_metadata = metadata;
|
||||
_paywall = paywall;
|
||||
_paywallFactory = paywallFactory;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -85,11 +88,27 @@ namespace VoidCat.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/paywall")]
|
||||
public ValueTask<PaywallOrder?> CreateOrder([FromRoute] string id)
|
||||
public async ValueTask<PaywallOrder?> CreateOrder([FromRoute] string id)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var gid = id.FromBase58Guid();
|
||||
var file = await _storage.Get(gid);
|
||||
var config = await _paywall.GetConfig(gid);
|
||||
|
||||
var provider = await _paywallFactory.CreateProvider(config!.Service);
|
||||
return await provider.CreateOrder(file!);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Route("{id}/paywall/{order:guid}")]
|
||||
public async ValueTask<PaywallOrder?> GetOrderStatus([FromRoute] string id, [FromRoute]Guid order)
|
||||
{
|
||||
var gid = id.FromBase58Guid();
|
||||
var config = await _paywall.GetConfig(gid);
|
||||
|
||||
var provider = await _paywallFactory.CreateProvider(config!.Service);
|
||||
return await provider.GetOrderStatus(order);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{id}/paywall")]
|
||||
public async Task<IActionResult> SetPaywallConfig([FromRoute] string id, [FromBody] SetPaywallConfigRequest req)
|
||||
@ -99,7 +118,7 @@ namespace VoidCat.Controllers
|
||||
if (meta == default) return NotFound();
|
||||
|
||||
if (req.EditSecret != meta.EditSecret) return Unauthorized();
|
||||
|
||||
|
||||
if (req.Strike != default)
|
||||
{
|
||||
await _paywall.SetConfig(gid, req.Strike!);
|
||||
@ -139,7 +158,7 @@ namespace VoidCat.Controllers
|
||||
{
|
||||
[JsonConverter(typeof(Base58GuidConverter))]
|
||||
public Guid EditSecret { get; init; }
|
||||
|
||||
|
||||
public StrikePaywallConfig? Strike { get; init; }
|
||||
}
|
||||
}
|
@ -16,7 +16,8 @@ public static class Extensions
|
||||
|
||||
public static string? GetHeader(this IHeaderDictionary headers, string key)
|
||||
{
|
||||
return headers
|
||||
.FirstOrDefault(a => a.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)).Value.ToString();
|
||||
var h = headers
|
||||
.FirstOrDefault(a => a.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase));
|
||||
return !string.IsNullOrEmpty(h.Value.ToString()) ? h.Value.ToString() : default;
|
||||
}
|
||||
}
|
@ -8,5 +8,5 @@ public enum PaywallServices
|
||||
Strike
|
||||
}
|
||||
|
||||
public abstract record PaywallConfig(PaywallServices Service, [property: JsonConverter(typeof(JsonStringEnumConverter))]PaywallMoney Cost);
|
||||
public abstract record PaywallConfig(PaywallServices Service, PaywallMoney Cost);
|
||||
public record StrikePaywallConfig(string Handle, PaywallMoney Cost) : PaywallConfig(PaywallServices.Strike, Cost);
|
||||
|
@ -1,11 +1,13 @@
|
||||
namespace VoidCat.Model.Paywall;
|
||||
|
||||
public enum PaywallStatus : byte
|
||||
public enum PaywallOrderStatus : byte
|
||||
{
|
||||
Unpaid,
|
||||
Paid,
|
||||
Expired
|
||||
}
|
||||
|
||||
public abstract record PaywallOrder(Guid Id, PaywallMoney Price, PaywallStatus Status);
|
||||
public record LightningPaywallOrder(Guid Id, PaywallMoney Price, PaywallStatus Status, string LnInvoice) : PaywallOrder(Id, Price, Status);
|
||||
public record PaywallOrder(Guid Id, PaywallMoney Price, PaywallOrderStatus Status);
|
||||
|
||||
public record LightningPaywallOrder(Guid Id, PaywallMoney Price, PaywallOrderStatus Status, string LnInvoice,
|
||||
DateTimeOffset Expire) : PaywallOrder(Id, Price, Status);
|
@ -1,4 +1,5 @@
|
||||
using VoidCat.Services.Abstractions;
|
||||
using VoidCat.Services;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Model
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ using VoidCat.Services;
|
||||
using VoidCat.Services.Abstractions;
|
||||
using VoidCat.Services.InMemory;
|
||||
using VoidCat.Services.Migrations;
|
||||
using VoidCat.Services.Paywall;
|
||||
using VoidCat.Services.Redis;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@ -17,6 +18,7 @@ var services = builder.Services;
|
||||
var configuration = builder.Configuration;
|
||||
var voidSettings = configuration.GetSection("Settings").Get<VoidSettings>();
|
||||
services.AddSingleton(voidSettings);
|
||||
services.AddSingleton(voidSettings.Strike ?? new());
|
||||
|
||||
var seqSettings = configuration.GetSection("Seq");
|
||||
builder.Logging.AddSeq(seqSettings);
|
||||
@ -49,25 +51,34 @@ services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||
});
|
||||
|
||||
// void.cat services
|
||||
//
|
||||
services.AddVoidMigrations();
|
||||
services.AddScoped<IFileMetadataStore, LocalDiskFileMetadataStore>();
|
||||
services.AddScoped<IFileStore, LocalDiskFileStore>();
|
||||
services.AddScoped<IAggregateStatsCollector, AggregateStatsCollector>();
|
||||
services.AddScoped<IStatsCollector, PrometheusStatsCollector>();
|
||||
|
||||
// file storage
|
||||
services.AddTransient<IFileMetadataStore, LocalDiskFileMetadataStore>();
|
||||
services.AddTransient<IFileStore, LocalDiskFileStore>();
|
||||
|
||||
// stats
|
||||
services.AddTransient<IAggregateStatsCollector, AggregateStatsCollector>();
|
||||
services.AddTransient<IStatsCollector, PrometheusStatsCollector>();
|
||||
|
||||
// paywall
|
||||
services.AddVoidPaywall();
|
||||
|
||||
if (useRedis)
|
||||
{
|
||||
services.AddScoped<RedisStatsController>();
|
||||
services.AddScoped<IStatsCollector>(svc => svc.GetRequiredService<RedisStatsController>());
|
||||
services.AddScoped<IStatsReporter>(svc => svc.GetRequiredService<RedisStatsController>());
|
||||
services.AddScoped<IPaywallStore, RedisPaywallStore>();
|
||||
services.AddTransient<RedisStatsController>();
|
||||
services.AddTransient<IStatsCollector>(svc => svc.GetRequiredService<RedisStatsController>());
|
||||
services.AddTransient<IStatsReporter>(svc => svc.GetRequiredService<RedisStatsController>());
|
||||
services.AddTransient<IPaywallStore, RedisPaywallStore>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddMemoryCache();
|
||||
services.AddScoped<InMemoryStatsController>();
|
||||
services.AddScoped<IStatsReporter>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
||||
services.AddScoped<IStatsCollector>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
||||
services.AddScoped<IPaywallStore, InMemoryPaywallStore>();
|
||||
services.AddTransient<InMemoryStatsController>();
|
||||
services.AddTransient<IStatsReporter>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
||||
services.AddTransient<IStatsCollector>(svc => svc.GetRequiredService<InMemoryStatsController>());
|
||||
services.AddTransient<IPaywallStore, InMemoryPaywallStore>();
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
@ -90,4 +101,4 @@ app.UseEndpoints(ep =>
|
||||
ep.MapFallbackToFile("index.html");
|
||||
});
|
||||
|
||||
app.Run();
|
||||
app.Run();
|
@ -1,15 +1,16 @@
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Model.Paywall;
|
||||
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
public interface IPaywallFactory
|
||||
{
|
||||
ValueTask<IPaywallProvider> CreateStrikeProvider();
|
||||
ValueTask<IPaywallProvider> CreateProvider(PaywallServices svc);
|
||||
}
|
||||
|
||||
public interface IPaywallProvider
|
||||
{
|
||||
ValueTask<PaywallOrder?> CreateOrder(PaywallConfig config);
|
||||
ValueTask<PaywallOrder?> CreateOrder(PublicVoidFile file);
|
||||
|
||||
ValueTask<PaywallOrder?> GetOrderStatus(Guid id);
|
||||
}
|
||||
|
@ -31,7 +31,8 @@ public class InMemoryPaywallStore : IPaywallStore
|
||||
|
||||
public ValueTask SaveOrder(PaywallOrder order)
|
||||
{
|
||||
_cache.Set(order.Id, order);
|
||||
_cache.Set(order.Id, order,
|
||||
order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5));
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
38
VoidCat/Services/Paywall/PaywallFactory.cs
Normal file
38
VoidCat/Services/Paywall/PaywallFactory.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using VoidCat.Model.Paywall;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Paywall;
|
||||
|
||||
public class PaywallFactory : IPaywallFactory
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
public PaywallFactory(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public ValueTask<IPaywallProvider> CreateProvider(PaywallServices svc)
|
||||
{
|
||||
return ValueTask.FromResult<IPaywallProvider>(svc switch
|
||||
{
|
||||
PaywallServices.Strike => _services.GetRequiredService<StrikePaywallProvider>(),
|
||||
_ => throw new ArgumentException("Must have a paywall config", nameof(svc))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class Paywall
|
||||
{
|
||||
public static IServiceCollection AddVoidPaywall(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<IPaywallFactory, PaywallFactory>();
|
||||
|
||||
// strike
|
||||
services.AddTransient<StrikeApi>();
|
||||
services.AddTransient<StrikePaywallProvider>();
|
||||
services.AddTransient<IPaywallProvider>((svc) => svc.GetRequiredService<StrikePaywallProvider>());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
130
VoidCat/Services/Paywall/StrikePaywallProvider.cs
Normal file
130
VoidCat/Services/Paywall/StrikePaywallProvider.cs
Normal file
@ -0,0 +1,130 @@
|
||||
using System.Globalization;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Model.Paywall;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Paywall;
|
||||
|
||||
public class StrikePaywallProvider : IPaywallProvider
|
||||
{
|
||||
private readonly ILogger<StrikePaywallProvider> _logger;
|
||||
private readonly StrikeApi _strike;
|
||||
private readonly IPaywallStore _store;
|
||||
|
||||
public StrikePaywallProvider(ILogger<StrikePaywallProvider> logger, StrikeApi strike, IPaywallStore store)
|
||||
{
|
||||
_logger = logger;
|
||||
_strike = strike;
|
||||
_store = store;
|
||||
}
|
||||
|
||||
public async ValueTask<PaywallOrder?> CreateOrder(PublicVoidFile file)
|
||||
{
|
||||
IsStrikePaywall(file.Paywall, out var strikeConfig);
|
||||
var config = file.Paywall!;
|
||||
|
||||
_logger.LogInformation("Generating invoice for {Currency} {Amount}", config.Cost.Currency, config.Cost.Amount);
|
||||
|
||||
var currency = MapCurrency(strikeConfig!.Cost.Currency);
|
||||
if (currency == Currencies.USD)
|
||||
{
|
||||
// map USD to USDT if USD is not available and USDT is
|
||||
var profile = await _strike.GetProfile(strikeConfig!.Handle);
|
||||
if (profile != default)
|
||||
{
|
||||
var usd = profile.Currencies.FirstOrDefault(a => a.Currency == Currencies.USD);
|
||||
var usdt = profile.Currencies.FirstOrDefault(a => a.Currency == Currencies.USDT);
|
||||
if (!(usd?.IsAvailable ?? false) && (usdt?.IsAvailable ?? false))
|
||||
{
|
||||
currency = Currencies.USDT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var invoice = await _strike.GenerateInvoice(new()
|
||||
{
|
||||
Handle = strikeConfig.Handle,
|
||||
Amount = new()
|
||||
{
|
||||
Amount = strikeConfig.Cost.Amount.ToString(CultureInfo.InvariantCulture),
|
||||
Currency = currency
|
||||
},
|
||||
Description = file.Metadata?.Name
|
||||
});
|
||||
if (invoice != default)
|
||||
{
|
||||
var quote = await _strike.GetInvoiceQuote(invoice.InvoiceId);
|
||||
if (quote != default)
|
||||
{
|
||||
var order = new LightningPaywallOrder(invoice.InvoiceId, config.Cost, PaywallOrderStatus.Unpaid,
|
||||
quote.LnInvoice!,
|
||||
quote.Expiration);
|
||||
await _store.SaveOrder(order);
|
||||
return order;
|
||||
}
|
||||
|
||||
_logger.LogWarning("Failed to get quote for invoice: {Id}", invoice.InvoiceId);
|
||||
}
|
||||
|
||||
_logger.LogWarning("Failed to get invoice for config: {Config}", config);
|
||||
return default;
|
||||
}
|
||||
|
||||
public async ValueTask<PaywallOrder?> GetOrderStatus(Guid id)
|
||||
{
|
||||
var order = await _store.GetOrder(id);
|
||||
if (order == default)
|
||||
{
|
||||
var invoice = await _strike.GetInvoice(id);
|
||||
if (invoice != default)
|
||||
{
|
||||
order = new(id, new(decimal.Parse(invoice.Amount!.Amount!), MapCurrency(invoice.Amount.Currency)),
|
||||
MapStatus(invoice.State));
|
||||
await _store.SaveOrder(order);
|
||||
}
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
private static Currencies MapCurrency(PaywallCurrencies c)
|
||||
=> c switch
|
||||
{
|
||||
PaywallCurrencies.BTC => Currencies.BTC,
|
||||
PaywallCurrencies.USD => Currencies.USD,
|
||||
PaywallCurrencies.EUR => Currencies.EUR,
|
||||
PaywallCurrencies.GBP => Currencies.GBP,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(c), c, null)
|
||||
};
|
||||
|
||||
private static PaywallCurrencies MapCurrency(Currencies? c)
|
||||
=> c switch
|
||||
{
|
||||
Currencies.BTC => PaywallCurrencies.BTC,
|
||||
Currencies.USD => PaywallCurrencies.USD,
|
||||
Currencies.EUR => PaywallCurrencies.EUR,
|
||||
Currencies.GBP => PaywallCurrencies.GBP,
|
||||
Currencies.USDT => PaywallCurrencies.USD,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(c), c, null)
|
||||
};
|
||||
|
||||
private static PaywallOrderStatus MapStatus(InvoiceState s)
|
||||
=> s switch
|
||||
{
|
||||
InvoiceState.UNPAID => PaywallOrderStatus.Unpaid,
|
||||
InvoiceState.PENDING => PaywallOrderStatus.Unpaid,
|
||||
InvoiceState.PAID => PaywallOrderStatus.Paid,
|
||||
InvoiceState.CANCELLED => PaywallOrderStatus.Expired,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(s), s, null)
|
||||
};
|
||||
|
||||
private static void IsStrikePaywall(PaywallConfig? cfg, out StrikePaywallConfig? strikeConfig)
|
||||
{
|
||||
if (cfg?.Service != PaywallServices.Strike)
|
||||
{
|
||||
throw new ArgumentException("Must be strike paywall");
|
||||
}
|
||||
|
||||
strikeConfig = cfg as StrikePaywallConfig;
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ public class RedisPaywallStore : IPaywallStore
|
||||
public async ValueTask<PaywallConfig?> GetConfig(Guid id)
|
||||
{
|
||||
var json = await _database.StringGetAsync(ConfigKey(id));
|
||||
var cfg = JsonConvert.DeserializeObject<PaywallConfig>(json);
|
||||
var cfg = json.HasValue ? JsonConvert.DeserializeObject<PaywallConfig>(json) : default;
|
||||
return cfg?.Service switch
|
||||
{
|
||||
PaywallServices.Strike => JsonConvert.DeserializeObject<StrikePaywallConfig>(json),
|
||||
@ -33,14 +33,15 @@ public class RedisPaywallStore : IPaywallStore
|
||||
public async ValueTask<PaywallOrder?> GetOrder(Guid id)
|
||||
{
|
||||
var json = await _database.StringGetAsync(OrderKey(id));
|
||||
return JsonConvert.DeserializeObject<PaywallOrder>(json);
|
||||
return json.HasValue ? JsonConvert.DeserializeObject<PaywallOrder>(json) : default;
|
||||
}
|
||||
|
||||
public async ValueTask SaveOrder(PaywallOrder order)
|
||||
{
|
||||
await _database.StringSetAsync(OrderKey(order.Id), JsonConvert.SerializeObject(order));
|
||||
await _database.StringSetAsync(OrderKey(order.Id), JsonConvert.SerializeObject(order),
|
||||
order.Status == PaywallOrderStatus.Paid ? TimeSpan.FromDays(1) : TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
private RedisKey ConfigKey(Guid id) => $"paywall:config:{id}";
|
||||
private RedisKey OrderKey(Guid id) => $"paywall:order:{id}";
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
namespace VoidCat.Services.Abstractions;
|
||||
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
|
||||
namespace VoidCat.Services;
|
||||
|
||||
public class StrikeApi
|
||||
{
|
||||
private readonly ILogger<StrikeApi> _logger;
|
||||
@ -93,56 +93,41 @@ public class StrikeApi
|
||||
|
||||
public class Profile
|
||||
{
|
||||
[JsonProperty("handle")]
|
||||
public string Handle { get; init; } = null;
|
||||
[JsonProperty("handle")] public string Handle { get; init; } = null;
|
||||
|
||||
[JsonProperty("avatarUrl")]
|
||||
public string? AvatarUrl { get; init; }
|
||||
[JsonProperty("avatarUrl")] public string? AvatarUrl { get; init; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; init; }
|
||||
[JsonProperty("description")] public string? Description { get; init; }
|
||||
|
||||
[JsonProperty("canReceive")]
|
||||
public bool CanReceive { get; init; }
|
||||
[JsonProperty("canReceive")] public bool CanReceive { get; init; }
|
||||
|
||||
[JsonProperty("currencies")]
|
||||
public List<AvailableCurrency> Currencies { get; init; } = new();
|
||||
[JsonProperty("currencies")] public List<AvailableCurrency> Currencies { get; init; } = new();
|
||||
}
|
||||
|
||||
public class InvoiceQuote
|
||||
{
|
||||
[JsonProperty("quoteId")]
|
||||
public Guid QuoteId { get; init; }
|
||||
[JsonProperty("quoteId")] public Guid QuoteId { get; init; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; init; }
|
||||
[JsonProperty("description")] public string? Description { get; init; }
|
||||
|
||||
[JsonProperty("lnInvoice")]
|
||||
public string? LnInvoice { get; init; }
|
||||
[JsonProperty("lnInvoice")] public string? LnInvoice { get; init; }
|
||||
|
||||
[JsonProperty("onchainAddress")]
|
||||
public string? OnChainAddress { get; init; }
|
||||
[JsonProperty("onchainAddress")] public string? OnChainAddress { get; init; }
|
||||
|
||||
[JsonProperty("expiration")]
|
||||
public DateTimeOffset Expiration { get; init; }
|
||||
[JsonProperty("expiration")] public DateTimeOffset Expiration { get; init; }
|
||||
|
||||
[JsonProperty("expirationInSec")]
|
||||
public ulong ExpirationSec { get; init; }
|
||||
[JsonProperty("expirationInSec")] public ulong ExpirationSec { get; init; }
|
||||
|
||||
[JsonProperty("targetAmount")]
|
||||
public CurrencyAmount? TargetAmount { get; init; }
|
||||
[JsonProperty("targetAmount")] public CurrencyAmount? TargetAmount { get; init; }
|
||||
|
||||
[JsonProperty("sourceAmount")]
|
||||
public CurrencyAmount? SourceAmount { get; init; }
|
||||
[JsonProperty("sourceAmount")] public CurrencyAmount? SourceAmount { get; init; }
|
||||
|
||||
[JsonProperty("conversionRate")]
|
||||
public ConversionRate? ConversionRate { get; init; }
|
||||
[JsonProperty("conversionRate")] public ConversionRate? ConversionRate { get; init; }
|
||||
}
|
||||
|
||||
public class ConversionRate
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public string? Amount { get; init; }
|
||||
[JsonProperty("amount")] public string? Amount { get; init; }
|
||||
|
||||
[JsonProperty("sourceCurrency")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
@ -162,23 +147,18 @@ public class ErrorResponse : Exception
|
||||
|
||||
public class CreateInvoiceRequest
|
||||
{
|
||||
[JsonProperty("correlationId")]
|
||||
public string? CorrelationId { get; init; }
|
||||
[JsonProperty("correlationId")] public string? CorrelationId { get; init; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; init; }
|
||||
[JsonProperty("description")] public string? Description { get; init; }
|
||||
|
||||
[JsonProperty("amount")]
|
||||
public CurrencyAmount? Amount { get; init; }
|
||||
[JsonProperty("amount")] public CurrencyAmount? Amount { get; init; }
|
||||
|
||||
[JsonProperty("handle")]
|
||||
public string? Handle { get; init; }
|
||||
[JsonProperty("handle")] public string? Handle { get; init; }
|
||||
}
|
||||
|
||||
public class CurrencyAmount
|
||||
{
|
||||
[JsonProperty("amount")]
|
||||
public string? Amount { get; init; }
|
||||
[JsonProperty("amount")] public string? Amount { get; init; }
|
||||
|
||||
[JsonProperty("currency")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
@ -187,14 +167,11 @@ public class CurrencyAmount
|
||||
|
||||
public class AvailableCurrency
|
||||
{
|
||||
[JsonProperty("currency")]
|
||||
public Currencies Currency { get; init; }
|
||||
[JsonProperty("currency")] public Currencies Currency { get; init; }
|
||||
|
||||
[JsonProperty("isDefaultCurrency")]
|
||||
public bool IsDefault { get; init; }
|
||||
[JsonProperty("isDefaultCurrency")] public bool IsDefault { get; init; }
|
||||
|
||||
[JsonProperty("isAvailable")]
|
||||
public bool IsAvailable { get; init; }
|
||||
[JsonProperty("isAvailable")] public bool IsAvailable { get; init; }
|
||||
}
|
||||
|
||||
public enum Currencies
|
||||
@ -208,93 +185,70 @@ public enum Currencies
|
||||
|
||||
public class Invoice
|
||||
{
|
||||
[JsonProperty("invoiceId")]
|
||||
public Guid InvoiceId { get; init; }
|
||||
[JsonProperty("invoiceId")] public Guid InvoiceId { get; init; }
|
||||
|
||||
[JsonProperty("amount")]
|
||||
public CurrencyAmount? Amount { get; init; }
|
||||
[JsonProperty("amount")] public CurrencyAmount? Amount { get; init; }
|
||||
|
||||
[JsonProperty("state")]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public InvoiceState State { get; set; }
|
||||
|
||||
[JsonProperty("created")]
|
||||
public DateTimeOffset? Created { get; init; }
|
||||
[JsonProperty("created")] public DateTimeOffset? Created { get; init; }
|
||||
|
||||
[JsonProperty("correlationId")]
|
||||
public string? CorrelationId { get; init; }
|
||||
[JsonProperty("correlationId")] public string? CorrelationId { get; init; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; init; }
|
||||
[JsonProperty("description")] public string? Description { get; init; }
|
||||
|
||||
[JsonProperty("issuerId")]
|
||||
public Guid? IssuerId { get; init; }
|
||||
[JsonProperty("issuerId")] public Guid? IssuerId { get; init; }
|
||||
|
||||
[JsonProperty("receiverId")]
|
||||
public Guid? ReceiverId { get; init; }
|
||||
[JsonProperty("receiverId")] public Guid? ReceiverId { get; init; }
|
||||
|
||||
[JsonProperty("payerId")]
|
||||
public Guid? PayerId { get; init; }
|
||||
[JsonProperty("payerId")] public Guid? PayerId { get; init; }
|
||||
}
|
||||
|
||||
public abstract class WebhookBase
|
||||
{
|
||||
[JsonProperty("webhookUrl")]
|
||||
public Uri? Uri { get; init; }
|
||||
[JsonProperty("webhookUrl")] public Uri? Uri { get; init; }
|
||||
|
||||
[JsonProperty("webhookVersion")]
|
||||
public string? Version { get; init; }
|
||||
[JsonProperty("webhookVersion")] public string? Version { get; init; }
|
||||
|
||||
[JsonProperty("enabled")]
|
||||
public bool? Enabled { get; init; }
|
||||
[JsonProperty("enabled")] public bool? Enabled { get; init; }
|
||||
|
||||
[JsonProperty("eventTypes")]
|
||||
public HashSet<string>? EventTypes { get; init; }
|
||||
[JsonProperty("eventTypes")] public HashSet<string>? EventTypes { get; init; }
|
||||
}
|
||||
|
||||
public sealed class NewWebhook : WebhookBase
|
||||
{
|
||||
[JsonProperty("secret")]
|
||||
public string? Secret { get; init; }
|
||||
[JsonProperty("secret")] public string? Secret { get; init; }
|
||||
}
|
||||
|
||||
public sealed class WebhookSubscription : WebhookBase
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public Guid? Id { get; init; }
|
||||
[JsonProperty("id")] public Guid? Id { get; init; }
|
||||
|
||||
[JsonProperty("created")]
|
||||
public DateTimeOffset? Created { get; init; }
|
||||
[JsonProperty("created")] public DateTimeOffset? Created { get; init; }
|
||||
}
|
||||
|
||||
public class WebhookData
|
||||
{
|
||||
[JsonProperty("entityId")]
|
||||
public Guid? EntityId { get; set; }
|
||||
[JsonProperty("entityId")] public Guid? EntityId { get; set; }
|
||||
|
||||
[JsonProperty("changes")]
|
||||
public List<string>? Changes { get; set; }
|
||||
[JsonProperty("changes")] public List<string>? Changes { get; set; }
|
||||
}
|
||||
|
||||
public class WebhookEvent
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public Guid? Id { get; set; }
|
||||
[JsonProperty("id")] public Guid? Id { get; set; }
|
||||
|
||||
[JsonProperty("eventType")]
|
||||
public string? EventType { get; set; }
|
||||
[JsonProperty("eventType")] public string? EventType { get; set; }
|
||||
|
||||
[JsonProperty("webhookVersion")]
|
||||
public string? WebhookVersion { get; set; }
|
||||
[JsonProperty("webhookVersion")] public string? WebhookVersion { get; set; }
|
||||
|
||||
[JsonProperty("data")]
|
||||
public WebhookData? Data { get; set; }
|
||||
[JsonProperty("data")] public WebhookData? Data { get; set; }
|
||||
|
||||
[JsonProperty("created")]
|
||||
public DateTimeOffset? Created { get; set; }
|
||||
[JsonProperty("created")] public DateTimeOffset? Created { get; set; }
|
||||
|
||||
[JsonProperty("deliverySuccess")]
|
||||
public bool? DeliverySuccess { get; set; }
|
||||
[JsonProperty("deliverySuccess")] public bool? DeliverySuccess { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
@ -314,4 +268,4 @@ public class StrikeApiSettings
|
||||
{
|
||||
public Uri? Uri { get; init; }
|
||||
public string? ApiKey { get; init; }
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
"proxy": "https://localhost:7195",
|
||||
"dependencies": {
|
||||
"feather-icons-react": "^0.5.0",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
|
@ -36,4 +36,15 @@ export const PaywallCurrencies = {
|
||||
USD: 1,
|
||||
EUR: 2,
|
||||
GBP: 3
|
||||
}
|
||||
|
||||
export const PaywallServices = {
|
||||
None: 0,
|
||||
Strike: 1
|
||||
}
|
||||
|
||||
export const PaywallOrderState = {
|
||||
Unpaid: 0,
|
||||
Paid: 1,
|
||||
Expired: 2
|
||||
}
|
21
VoidCat/spa/src/Countdown.js
Normal file
21
VoidCat/spa/src/Countdown.js
Normal file
@ -0,0 +1,21 @@
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
export function Countdown(props) {
|
||||
const [time, setTime] = useState(0);
|
||||
const onEnded = props.onEnded;
|
||||
|
||||
useEffect(() => {
|
||||
let t = setInterval(() => {
|
||||
let to = new Date(props.to).getTime();
|
||||
let now = new Date().getTime();
|
||||
let seconds = (to - now) / 1000.0;
|
||||
setTime(Math.max(0, seconds));
|
||||
if(seconds <= 0 && typeof onEnded === "function") {
|
||||
onEnded();
|
||||
}
|
||||
}, 100);
|
||||
return () => clearInterval(t);
|
||||
}, [])
|
||||
|
||||
return <div>{time.toFixed(1)}s</div>
|
||||
}
|
@ -6,4 +6,9 @@
|
||||
|
||||
.file-edit > div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-edit svg {
|
||||
vertical-align: middle;
|
||||
margin-left: 10px;
|
||||
}
|
@ -4,9 +4,10 @@ import "./FileEdit.css";
|
||||
import {StrikePaywallConfig} from "./StrikePaywallConfig";
|
||||
|
||||
export function FileEdit(props) {
|
||||
const [paywall, setPaywall] = useState();
|
||||
const file = props.file;
|
||||
const [paywall, setPaywall] = useState(file.paywall?.service);
|
||||
|
||||
const privateFile = JSON.parse(window.localStorage.getItem(props.file.id));
|
||||
const privateFile = JSON.parse(window.localStorage.getItem(file.id));
|
||||
if (!privateFile) {
|
||||
return null;
|
||||
}
|
||||
@ -14,13 +15,13 @@ export function FileEdit(props) {
|
||||
function renderPaywallConfig() {
|
||||
switch (paywall) {
|
||||
case 1: {
|
||||
return <StrikePaywallConfig file={privateFile}/>
|
||||
return <StrikePaywallConfig file={file} privateFile={privateFile}/>
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const meta = props.file.metadata;
|
||||
const meta = file.metadata;
|
||||
return (
|
||||
<div className="file-edit">
|
||||
<div>
|
||||
@ -36,7 +37,7 @@ export function FileEdit(props) {
|
||||
<div>
|
||||
<h3>Paywall Config</h3>
|
||||
Type:
|
||||
<select onChange={(e) => setPaywall(parseInt(e.target.value))}>
|
||||
<select onChange={(e) => setPaywall(parseInt(e.target.value))} value={paywall}>
|
||||
<option value={0}>None</option>
|
||||
<option value={1}>Strike</option>
|
||||
</select>
|
||||
|
@ -1,21 +1,49 @@
|
||||
import {ConstName} from "./Util";
|
||||
import {PaywallCurrencies} from "./Const";
|
||||
import {ConstName, FormatCurrency} from "./Util";
|
||||
import {PaywallCurrencies, PaywallServices} from "./Const";
|
||||
import {useState} from "react";
|
||||
import {LightningPaywall} from "./LightningPaywall";
|
||||
|
||||
export function FilePaywall(props) {
|
||||
const file = props.file;
|
||||
const pw = file.paywall;
|
||||
|
||||
const paywallKey = `paywall-${file.id}`;
|
||||
const onPaid = props.onPaid;
|
||||
|
||||
const [order, setOrder] = useState();
|
||||
|
||||
async function fetchOrder() {
|
||||
let req = await fetch("")
|
||||
|
||||
async function fetchOrder(e) {
|
||||
e.target.disabled = true;
|
||||
let req = await fetch(`/upload/${file.id}/paywall`);
|
||||
if (req.ok) {
|
||||
setOrder(await req.json());
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
setOrder(undefined);
|
||||
}
|
||||
|
||||
function handlePaid(order) {
|
||||
window.localStorage.setItem(paywallKey, JSON.stringify(order));
|
||||
if (typeof onPaid === "function") {
|
||||
onPaid();
|
||||
}
|
||||
}
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<div className="paywall">
|
||||
<h3>You must pay {FormatCurrency(pw.cost.amount, pw.cost.currency)} to view this
|
||||
file.</h3>
|
||||
<button onClick={fetchOrder}>Pay</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
switch (pw.service) {
|
||||
case PaywallServices.Strike: {
|
||||
return <LightningPaywall file={file} order={order} onReset={reset} onPaid={handlePaid}/>;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="paywall">
|
||||
<h3>You must pay {ConstName(PaywallCurrencies, pw.cost.currency)} {pw.cost.amount} to view this file.</h3>
|
||||
<button onClick={fetchOrder}>Pay</button>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -9,6 +9,8 @@ import {FilePaywall} from "./FilePaywall";
|
||||
export function FilePreview() {
|
||||
const params = useParams();
|
||||
const [info, setInfo] = useState();
|
||||
const [order, setOrder] = useState();
|
||||
const [link, setLink] = useState("#");
|
||||
|
||||
async function loadInfo() {
|
||||
let req = await fetch(`/upload/${params.id}`);
|
||||
@ -19,11 +21,12 @@ export function FilePreview() {
|
||||
}
|
||||
|
||||
function renderTypes() {
|
||||
if(info.paywall) {
|
||||
return <FilePaywall file={info}/>;
|
||||
if (info.paywall) {
|
||||
if (!order) {
|
||||
return <FilePaywall file={info} onPaid={loadInfo}/>;
|
||||
}
|
||||
}
|
||||
|
||||
let link = `/d/${info.id}`;
|
||||
|
||||
if (info.metadata) {
|
||||
switch (info.metadata.mimeType) {
|
||||
case "image/jpg":
|
||||
@ -53,11 +56,24 @@ export function FilePreview() {
|
||||
loadInfo();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (info) {
|
||||
let order = window.localStorage.getItem(`paywall-${info.id}`);
|
||||
if (order) {
|
||||
let orderObj = JSON.parse(order);
|
||||
setOrder(orderObj);
|
||||
setLink(`/d/${info.id}?orderId=${orderObj.id}`);
|
||||
} else {
|
||||
setLink(`/d/${info.id}`);
|
||||
}
|
||||
}
|
||||
}, [info]);
|
||||
|
||||
return (
|
||||
<div className="preview">
|
||||
{info ? (
|
||||
<Fragment>
|
||||
this.Download(<a className="btn" href={`/d/${info.id}`}>{info.metadata?.name ?? info.id}</a>)
|
||||
this.Download(<a className="btn" href={link}>{info.metadata?.name ?? info.id}</a>)
|
||||
{renderTypes()}
|
||||
<FileEdit file={info}/>
|
||||
</Fragment>
|
||||
|
46
VoidCat/spa/src/LightningPaywall.js
Normal file
46
VoidCat/spa/src/LightningPaywall.js
Normal file
@ -0,0 +1,46 @@
|
||||
import QRCode from "qrcode.react";
|
||||
import {Countdown} from "./Countdown";
|
||||
import {useEffect} from "react";
|
||||
import {PaywallOrderState} from "./Const";
|
||||
|
||||
export function LightningPaywall(props) {
|
||||
const file = props.file;
|
||||
const order = props.order;
|
||||
const onPaid = props.onPaid;
|
||||
const link = `lightning:${order.lnInvoice}`;
|
||||
|
||||
function openInvoice() {
|
||||
let a = document.createElement("a");
|
||||
a.href = link;
|
||||
a.click();
|
||||
}
|
||||
|
||||
async function checkStatus() {
|
||||
let req = await fetch(`/upload/${file.id}/paywall/${order.id}`);
|
||||
if (req.ok) {
|
||||
let order = await req.json();
|
||||
|
||||
if (order.status === PaywallOrderState.Paid && typeof onPaid === "function") {
|
||||
onPaid(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let t = setInterval(checkStatus, 2500);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="lightning-invoice" onClick={openInvoice}>
|
||||
<QRCode
|
||||
value={link}
|
||||
size={512}
|
||||
includeMargin={true}/>
|
||||
<dl>
|
||||
<dt>Expires:</dt>
|
||||
<dd><Countdown to={order.expire} onEnded={props.onReset}/></dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,15 +1,21 @@
|
||||
import {useState} from "react";
|
||||
import FeatherIcon from "feather-icons-react";
|
||||
import {PaywallCurrencies} from "./Const";
|
||||
|
||||
export function StrikePaywallConfig(props) {
|
||||
const editSecret = props.file.metadata.editSecret;
|
||||
const id = props.file.id;
|
||||
const file = props.file;
|
||||
const privateFile = props.privateFile;
|
||||
const paywall = file.paywall;
|
||||
const editSecret = privateFile.metadata.editSecret;
|
||||
const id = file.id;
|
||||
|
||||
const [username, setUsername] = useState("hrf");
|
||||
const [currency, setCurrency] = useState(PaywallCurrencies.USD);
|
||||
const [price, setPrice] = useState(1);
|
||||
|
||||
async function saveStrikeConfig() {
|
||||
const [username, setUsername] = useState(paywall?.handle ?? "hrf");
|
||||
const [currency, setCurrency] = useState(paywall?.cost.currency ?? PaywallCurrencies.USD);
|
||||
const [price, setPrice] = useState(paywall?.cost.amount ?? 1);
|
||||
const [saveStatus, setSaveStatus] = useState();
|
||||
|
||||
async function saveStrikeConfig(e) {
|
||||
e.target.disabled = true;
|
||||
let cfg = {
|
||||
editSecret,
|
||||
strike: {
|
||||
@ -31,6 +37,8 @@ export function StrikePaywallConfig(props) {
|
||||
if (!req.ok) {
|
||||
alert("Error settings paywall config!");
|
||||
}
|
||||
setSaveStatus(req.ok);
|
||||
e.target.disabled = false;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -40,17 +48,18 @@ export function StrikePaywallConfig(props) {
|
||||
<dd><input type="text" value={username} onChange={(e) => setUsername(e.target.value)}/></dd>
|
||||
<dt>Currency:</dt>
|
||||
<dd>
|
||||
<select onChange={(e) => setCurrency(parseInt(e.target.value))}>
|
||||
<option selected={currency === PaywallCurrencies.BTC} value={PaywallCurrencies.BTC}>BTC</option>
|
||||
<option selected={currency === PaywallCurrencies.USD} value={PaywallCurrencies.USD}>USD</option>
|
||||
<option selected={currency === PaywallCurrencies.EUR} value={PaywallCurrencies.EUR}>EUR</option>
|
||||
<option selected={currency === PaywallCurrencies.GBP} value={PaywallCurrencies.GBP}>GBP</option>
|
||||
<select onChange={(e) => setCurrency(parseInt(e.target.value))} value={currency}>
|
||||
<option value={PaywallCurrencies.BTC}>BTC</option>
|
||||
<option value={PaywallCurrencies.USD}>USD</option>
|
||||
<option value={PaywallCurrencies.EUR}>EUR</option>
|
||||
<option value={PaywallCurrencies.GBP}>GBP</option>
|
||||
</select>
|
||||
</dd>
|
||||
<dt>Price:</dt>
|
||||
<dd><input type="number" value={price} onChange={(e) => setPrice(parseFloat(e.target.value))}/></dd>
|
||||
</dl>
|
||||
<button onClick={saveStrikeConfig}>Save</button>
|
||||
{saveStatus ? <FeatherIcon icon="check-circle"/> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -36,4 +36,42 @@ export function ConstName(type, val) {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function FormatCurrency(value, currency) {
|
||||
if (typeof value !== "number") {
|
||||
value = parseFloat(value);
|
||||
}
|
||||
switch (currency) {
|
||||
case 0:
|
||||
case "BTC": {
|
||||
let hasDecimals = (value % 1) > 0;
|
||||
return `₿${value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: hasDecimals ? 8 : 0, // Sats
|
||||
maximumFractionDigits: 11 // MSats
|
||||
})}`;
|
||||
}
|
||||
case 1:
|
||||
case "USD":{
|
||||
return value.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "USD"
|
||||
});
|
||||
}
|
||||
case 2:
|
||||
case "EUR": {
|
||||
return value.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "EUR"
|
||||
});
|
||||
}
|
||||
case 3:
|
||||
case "GBP": {
|
||||
return value.toLocaleString(undefined, {
|
||||
style: "currency",
|
||||
currency: "GBP"
|
||||
});
|
||||
}
|
||||
}
|
||||
return value.toString();
|
||||
}
|
@ -6655,7 +6655,7 @@ prompts@^2.0.1, prompts@^2.4.2:
|
||||
kleur "^3.0.3"
|
||||
sisteransi "^1.0.5"
|
||||
|
||||
prop-types@^15.7.2:
|
||||
prop-types@^15.6.0, prop-types@^15.7.2:
|
||||
version "15.8.1"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
|
||||
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
|
||||
@ -6687,6 +6687,20 @@ q@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
||||
|
||||
qr.js@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
|
||||
integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=
|
||||
|
||||
qrcode.react@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-1.0.1.tgz#2834bb50e5e275ffe5af6906eff15391fe9e38a5"
|
||||
integrity sha512-8d3Tackk8IRLXTo67Y+c1rpaiXjoz/Dd2HpcMdW//62/x8J1Nbho14Kh8x974t9prsLHN6XqVgcnRiBGFptQmg==
|
||||
dependencies:
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.0"
|
||||
qr.js "0.0.0"
|
||||
|
||||
qs@6.9.6:
|
||||
version "6.9.6"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.6.tgz#26ed3c8243a431b2924aca84cc90471f35d5a0ee"
|
||||
|
Loading…
Reference in New Issue
Block a user