Optional payments

This commit is contained in:
Kieran 2022-09-07 16:25:31 +01:00
parent af62bd74eb
commit 150579c509
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 92 additions and 36 deletions

View File

@ -100,7 +100,7 @@ public class DownloadController : Controller
}
// check payment order
if (meta.Payment != default && meta.Payment.Service != PaymentServices.None)
if (meta.Payment != default && meta.Payment.Service != PaymentServices.None && meta.Payment.Required)
{
var orderId = Request.Headers.GetHeader("V-OrderId") ?? Request.Query["orderId"];
if (!await IsOrderPaid(orderId))

View File

@ -15,7 +15,7 @@ namespace VoidCat.Controllers
{
private readonly FileStoreFactory _storage;
private readonly IFileMetadataStore _metadata;
private readonly IPaymentStore _payment;
private readonly IPaymentStore _paymentStore;
private readonly IPaymentFactory _paymentFactory;
private readonly FileInfoManager _fileInfo;
private readonly IUserUploadsStore _userUploads;
@ -29,7 +29,7 @@ namespace VoidCat.Controllers
{
_storage = storage;
_metadata = metadata;
_payment = payment;
_paymentStore = payment;
_paymentFactory = paymentFactory;
_fileInfo = fileInfo;
_userUploads = userUploads;
@ -214,7 +214,7 @@ namespace VoidCat.Controllers
{
var gid = id.FromBase58Guid();
var file = await _fileInfo.Get(gid);
var config = await _payment.Get(gid);
var config = await _paymentStore.Get(gid);
var provider = await _paymentFactory.CreateProvider(config!.Service);
return await provider.CreateOrder(file!.Payment!);
@ -231,7 +231,7 @@ namespace VoidCat.Controllers
public async ValueTask<PaymentOrder?> GetOrderStatus([FromRoute] string id, [FromRoute] Guid order)
{
var gid = id.FromBase58Guid();
var config = await _payment.Get(gid);
var config = await _paymentStore.Get(gid);
var provider = await _paymentFactory.CreateProvider(config!.Service);
return await provider.GetOrderStatus(order);
@ -254,18 +254,19 @@ namespace VoidCat.Controllers
if (req.Strike != default)
{
await _payment.Add(gid, new StrikePaymentConfig()
await _paymentStore.Add(gid, new StrikePaymentConfig()
{
Service = PaymentServices.Strike,
Handle = req.Strike.Handle,
Cost = req.Strike.Cost
Cost = req.Strike.Cost,
Required = req.Required
});
return Ok();
}
// if none set, delete config
await _payment.Delete(gid);
await _paymentStore.Delete(gid);
return Ok();
}
@ -341,5 +342,7 @@ namespace VoidCat.Controllers
public Guid EditSecret { get; init; }
public StrikePaymentConfig? Strike { get; init; }
public bool Required { get; init; }
}
}

View File

@ -0,0 +1,20 @@
using FluentMigrator;
namespace VoidCat.Services.Migrations.Database;
[Migration(20220908_1602)]
public class OptionalPayments : Migration{
public override void Up()
{
Create.Column("Required")
.OnTable("Payment")
.AsBoolean()
.WithDefaultValue(true);
}
public override void Down()
{
Delete.Column("Required")
.FromTable("Payment");
}
}

View File

@ -18,11 +18,11 @@ public sealed class PostgresPaymentStore : IPaymentStore
public async ValueTask<PaymentConfig?> Get(Guid id)
{
await using var conn = await _connection.Get();
var svc = await conn.QuerySingleOrDefaultAsync<DtoPaymentConfig>(
var dto = await conn.QuerySingleOrDefaultAsync<DtoPaymentConfig>(
@"select * from ""Payment"" where ""File"" = :file", new {file = id});
if (svc != default)
if (dto != default)
{
switch (svc.Service)
switch (dto.Service)
{
case PaymentServices.Strike:
{
@ -31,10 +31,11 @@ public sealed class PostgresPaymentStore : IPaymentStore
@"select ""Handle"" from ""PaymentStrike"" where ""File"" = :file", new {file = id});
return new StrikePaymentConfig
{
Cost = new(svc.Amount, svc.Currency),
File = svc.File,
Cost = new(dto.Amount, dto.Currency),
File = dto.File,
Handle = handle,
Service = PaymentServices.Strike
Service = PaymentServices.Strike,
Required = dto.Required
};
}
}
@ -55,14 +56,15 @@ public sealed class PostgresPaymentStore : IPaymentStore
await using var conn = await _connection.Get();
await using var txn = await conn.BeginTransactionAsync();
await conn.ExecuteAsync(
@"insert into ""Payment""(""File"", ""Service"", ""Amount"", ""Currency"") values(:file, :service, :amount, :currency)
on conflict(""File"") do update set ""Service"" = :service, ""Amount"" = :amount, ""Currency"" = :currency",
@"insert into ""Payment""(""File"", ""Service"", ""Amount"", ""Currency"", ""Required"") values(:file, :service, :amount, :currency, :required)
on conflict(""File"") do update set ""Service"" = :service, ""Amount"" = :amount, ""Currency"" = :currency, ""Required"" = :required",
new
{
file = id,
service = (int)obj.Service,
amount = obj.Cost.Amount,
currency = obj.Cost.Currency
currency = obj.Cost.Currency,
required = obj.Required
});
if (obj is StrikePaymentConfig sc)

View File

@ -7,6 +7,7 @@ import "./FileEdit.css";
import {useSelector} from "react-redux";
import {VoidButton} from "./VoidButton";
import moment from "moment";
import {PaymentServices} from "./Const";
export function FileEdit(props) {
const {Api} = useApi();
@ -42,10 +43,10 @@ export function FileEdit(props) {
function renderPaymentConfig() {
switch (payment) {
case 0: {
case PaymentServices.None: {
return <NoPaymentConfig privateFile={privateFile} onSaveConfig={saveConfig}/>;
}
case 1: {
case PaymentServices.Strike: {
return <StrikePaymentConfig file={file} privateFile={privateFile} onSaveConfig={saveConfig}/>
}
}

View File

@ -34,6 +34,7 @@ export function FilePayment(props) {
}
if (!order) {
if (pw.required) {
return (
<div className="payment">
<h3>
@ -42,6 +43,11 @@ export function FilePayment(props) {
<VoidButton onClick={fetchOrder}>Pay</VoidButton>
</div>
);
} else {
return (
<VoidButton onClick={fetchOrder}>Tip {FormatCurrency(pw.cost.amount, pw.cost.currency)}</VoidButton>
);
}
} else {
switch (pw.service) {
case PaymentServices.Strike: {

View File

@ -26,13 +26,24 @@ export function FilePreview() {
}
}
function renderTypes() {
function canAccessFile() {
if (info?.payment?.required === true && !order) {
return false;
}
return true;
}
function renderPayment() {
if (info.payment && info.payment.service !== 0) {
if (!order) {
return <FilePayment file={info} onPaid={loadInfo}/>;
}
}
return null;
}
function renderPreview() {
if (info.metadata) {
switch (info.metadata.mimeType) {
case "image/avif":
@ -109,7 +120,7 @@ export function FilePreview() {
}
function renderVirusWarning() {
if(info.virusScan && info.virusScan.isVirus === true) {
if (info.virusScan && info.virusScan.isVirus === true) {
let scanResult = info.virusScan;
return (
<div className="virus-warning">
@ -149,7 +160,8 @@ export function FilePreview() {
<Fragment>
<Helmet>
<title>void.cat - {info.metadata?.name ?? info.id}</title>
{info.metadata?.description ? <meta name="description" content={info.metadata?.description}/> : null}
{info.metadata?.description ?
<meta name="description" content={info.metadata?.description}/> : null}
{renderOpenGraphTags()}
</Helmet>
{renderVirusWarning()}
@ -158,10 +170,13 @@ export function FilePreview() {
{info.uploader ? <InlineProfile profile={info.uploader}/> : null}
</div>
<div>
<a className="btn" href={link} download={info.metadata?.name ?? info.id}>Download</a>
{canAccessFile() ?
<a className="btn" href={link}
download={info.metadata?.name ?? info.id}>Download</a> : null}
</div>
</div>
{renderTypes()}
{renderPayment()}
{canAccessFile() ? renderPreview() : null}
<div className="file-stats">
<div>
<FeatherIcon icon="download-cloud"/>

View File

@ -13,6 +13,7 @@ export function StrikePaymentConfig(props) {
const [username, setUsername] = useState(payment?.handle ?? "hrf");
const [currency, setCurrency] = useState(payment?.cost.currency ?? PaymentCurrencies.USD);
const [price, setPrice] = useState(payment?.cost.amount ?? 1);
const [required, setRequired] = useState(payment?.required);
const [saveStatus, setSaveStatus] = useState();
async function saveStrikeConfig(e) {
@ -24,7 +25,8 @@ export function StrikePaymentConfig(props) {
currency: currency,
amount: price
}
}
},
required
};
if (typeof onSaveConfig === "function") {
@ -53,6 +55,8 @@ export function StrikePaymentConfig(props) {
</dd>
<dt>Price:</dt>
<dd><input type="number" value={price} onChange={(e) => setPrice(parseFloat(e.target.value))}/></dd>
<dt>Required:</dt>
<dd><input type="checkbox" checked={required} onChange={(e) => setRequired(e.target.checked)}/></dd>
</dl>
<VoidButton onClick={saveStrikeConfig}>Save</VoidButton>
{saveStatus ? <FeatherIcon icon="check-circle"/> : null}

View File

@ -60,7 +60,7 @@ a:hover {
align-items: center;
}
input[type="text"], input[type="number"], input[type="password"], input[type="datetime-local"], select {
input[type="text"], input[type="number"], input[type="password"], input[type="datetime-local"], input[type="checkbox"], select {
display: inline-block;
line-height: 1.1;
border-radius: 10px;
@ -69,6 +69,11 @@ input[type="text"], input[type="number"], input[type="password"], input[type="da
border: 0;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
}
table {
width: 100%;
word-break: keep-all;