forked from Kieran/void.cat
Optional payments
This commit is contained in:
parent
af62bd74eb
commit
150579c509
@ -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))
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
VoidCat/Services/Migrations/Database/04-OptionalPayments.cs
Normal file
20
VoidCat/Services/Migrations/Database/04-OptionalPayments.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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}/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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: {
|
||||||
|
@ -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"/>
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user