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 // 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"]; var orderId = Request.Headers.GetHeader("V-OrderId") ?? Request.Query["orderId"];
if (!await IsOrderPaid(orderId)) if (!await IsOrderPaid(orderId))

View File

@ -15,7 +15,7 @@ namespace VoidCat.Controllers
{ {
private readonly FileStoreFactory _storage; private readonly FileStoreFactory _storage;
private readonly IFileMetadataStore _metadata; private readonly IFileMetadataStore _metadata;
private readonly IPaymentStore _payment; private readonly IPaymentStore _paymentStore;
private readonly IPaymentFactory _paymentFactory; private readonly IPaymentFactory _paymentFactory;
private readonly FileInfoManager _fileInfo; private readonly FileInfoManager _fileInfo;
private readonly IUserUploadsStore _userUploads; private readonly IUserUploadsStore _userUploads;
@ -29,7 +29,7 @@ namespace VoidCat.Controllers
{ {
_storage = storage; _storage = storage;
_metadata = metadata; _metadata = metadata;
_payment = payment; _paymentStore = payment;
_paymentFactory = paymentFactory; _paymentFactory = paymentFactory;
_fileInfo = fileInfo; _fileInfo = fileInfo;
_userUploads = userUploads; _userUploads = userUploads;
@ -214,7 +214,7 @@ namespace VoidCat.Controllers
{ {
var gid = id.FromBase58Guid(); var gid = id.FromBase58Guid();
var file = await _fileInfo.Get(gid); 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); var provider = await _paymentFactory.CreateProvider(config!.Service);
return await provider.CreateOrder(file!.Payment!); return await provider.CreateOrder(file!.Payment!);
@ -231,7 +231,7 @@ namespace VoidCat.Controllers
public async ValueTask<PaymentOrder?> GetOrderStatus([FromRoute] string id, [FromRoute] Guid order) public async ValueTask<PaymentOrder?> GetOrderStatus([FromRoute] string id, [FromRoute] Guid order)
{ {
var gid = id.FromBase58Guid(); var gid = id.FromBase58Guid();
var config = await _payment.Get(gid); var config = await _paymentStore.Get(gid);
var provider = await _paymentFactory.CreateProvider(config!.Service); var provider = await _paymentFactory.CreateProvider(config!.Service);
return await provider.GetOrderStatus(order); return await provider.GetOrderStatus(order);
@ -254,18 +254,19 @@ namespace VoidCat.Controllers
if (req.Strike != default) if (req.Strike != default)
{ {
await _payment.Add(gid, new StrikePaymentConfig() await _paymentStore.Add(gid, new StrikePaymentConfig()
{ {
Service = PaymentServices.Strike, Service = PaymentServices.Strike,
Handle = req.Strike.Handle, Handle = req.Strike.Handle,
Cost = req.Strike.Cost Cost = req.Strike.Cost,
Required = req.Required
}); });
return Ok(); return Ok();
} }
// if none set, delete config // if none set, delete config
await _payment.Delete(gid); await _paymentStore.Delete(gid);
return Ok(); return Ok();
} }
@ -341,5 +342,7 @@ namespace VoidCat.Controllers
public Guid EditSecret { get; init; } public Guid EditSecret { get; init; }
public StrikePaymentConfig? Strike { 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) public async ValueTask<PaymentConfig?> Get(Guid id)
{ {
await using var conn = await _connection.Get(); 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}); @"select * from ""Payment"" where ""File"" = :file", new {file = id});
if (svc != default) if (dto != default)
{ {
switch (svc.Service) switch (dto.Service)
{ {
case PaymentServices.Strike: case PaymentServices.Strike:
{ {
@ -31,10 +31,11 @@ public sealed class PostgresPaymentStore : IPaymentStore
@"select ""Handle"" from ""PaymentStrike"" where ""File"" = :file", new {file = id}); @"select ""Handle"" from ""PaymentStrike"" where ""File"" = :file", new {file = id});
return new StrikePaymentConfig return new StrikePaymentConfig
{ {
Cost = new(svc.Amount, svc.Currency), Cost = new(dto.Amount, dto.Currency),
File = svc.File, File = dto.File,
Handle = handle, 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 conn = await _connection.Get();
await using var txn = await conn.BeginTransactionAsync(); await using var txn = await conn.BeginTransactionAsync();
await conn.ExecuteAsync( await conn.ExecuteAsync(
@"insert into ""Payment""(""File"", ""Service"", ""Amount"", ""Currency"") values(:file, :service, :amount, :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", on conflict(""File"") do update set ""Service"" = :service, ""Amount"" = :amount, ""Currency"" = :currency, ""Required"" = :required",
new new
{ {
file = id, file = id,
service = (int)obj.Service, service = (int)obj.Service,
amount = obj.Cost.Amount, amount = obj.Cost.Amount,
currency = obj.Cost.Currency currency = obj.Cost.Currency,
required = obj.Required
}); });
if (obj is StrikePaymentConfig sc) if (obj is StrikePaymentConfig sc)

View File

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

View File

@ -34,14 +34,20 @@ export function FilePayment(props) {
} }
if (!order) { if (!order) {
return ( if (pw.required) {
<div className="payment"> return (
<h3> <div className="payment">
You must pay {FormatCurrency(pw.cost.amount, pw.cost.currency)} to view this file. <h3>
</h3> You must pay {FormatCurrency(pw.cost.amount, pw.cost.currency)} to view this file.
<VoidButton onClick={fetchOrder}>Pay</VoidButton> </h3>
</div> <VoidButton onClick={fetchOrder}>Pay</VoidButton>
); </div>
);
} else {
return (
<VoidButton onClick={fetchOrder}>Tip {FormatCurrency(pw.cost.amount, pw.cost.currency)}</VoidButton>
);
}
} else { } else {
switch (pw.service) { switch (pw.service) {
case PaymentServices.Strike: { 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 (info.payment && info.payment.service !== 0) {
if (!order) { if (!order) {
return <FilePayment file={info} onPaid={loadInfo}/>; return <FilePayment file={info} onPaid={loadInfo}/>;
} }
} }
return null;
}
function renderPreview() {
if (info.metadata) { if (info.metadata) {
switch (info.metadata.mimeType) { switch (info.metadata.mimeType) {
case "image/avif": case "image/avif":
@ -109,7 +120,7 @@ export function FilePreview() {
} }
function renderVirusWarning() { function renderVirusWarning() {
if(info.virusScan && info.virusScan.isVirus === true) { if (info.virusScan && info.virusScan.isVirus === true) {
let scanResult = info.virusScan; let scanResult = info.virusScan;
return ( return (
<div className="virus-warning"> <div className="virus-warning">
@ -122,9 +133,9 @@ export function FilePreview() {
</pre> </pre>
</div> </div>
); );
} }
} }
useEffect(() => { useEffect(() => {
loadInfo(); loadInfo();
}, []); }, []);
@ -149,7 +160,8 @@ export function FilePreview() {
<Fragment> <Fragment>
<Helmet> <Helmet>
<title>void.cat - {info.metadata?.name ?? info.id}</title> <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()} {renderOpenGraphTags()}
</Helmet> </Helmet>
{renderVirusWarning()} {renderVirusWarning()}
@ -158,10 +170,13 @@ export function FilePreview() {
{info.uploader ? <InlineProfile profile={info.uploader}/> : null} {info.uploader ? <InlineProfile profile={info.uploader}/> : null}
</div> </div>
<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>
</div> </div>
{renderTypes()} {renderPayment()}
{canAccessFile() ? renderPreview() : null}
<div className="file-stats"> <div className="file-stats">
<div> <div>
<FeatherIcon icon="download-cloud"/> <FeatherIcon icon="download-cloud"/>

View File

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

View File

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