mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 15:44:26 +01:00
Add File Seller plugin
This commit is contained in:
@@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Dynami
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Prism", "Plugins\BTCPayServer.Plugins.Prism\BTCPayServer.Plugins.Prism.csproj", "{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.Prism", "Plugins\BTCPayServer.Plugins.Prism\BTCPayServer.Plugins.Prism.csproj", "{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BTCPayServer.Plugins.FileSeller", "Plugins\BTCPayServer.Plugins.FileSeller\BTCPayServer.Plugins.FileSeller.csproj", "{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -253,6 +255,14 @@ Global
|
|||||||
{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}.Altcoins-Release|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
{9BADDA0B-A5AB-4D51-9EBE-67C08C459DC7}.Altcoins-Release|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Altcoins-Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Altcoins-Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Altcoins-Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F2D81B6A-E1EA-4900-BF9A-924D2CA951DD}.Altcoins-Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
|
{B19C9F52-DC47-466D-8B5C-2D202B7B003F} = {9E04ECE9-E304-4FF2-9CBC-83256E6C6962}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=b4e2ed08_002D4ad3_002D4648_002D8bdb_002D3107200460b9_0023BTCPayServer_002EPlugins_002ELiquidPlus/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Nostr/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Nostr/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<LangVersion>10</LangVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<!-- -->
|
||||||
|
<!-- Plugin specific properties -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<Product>File Seller</Product>
|
||||||
|
<Description>Allows you to sell files through the point of sale/crowdfund apps.</Description>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
<!-- Plugin development properties -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||||
|
<PreserveCompilationContext>false</PreserveCompilationContext>
|
||||||
|
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- This will make sure that referencing BTCPayServer doesn't put any artifact in the published directory -->
|
||||||
|
<ItemDefinitionGroup>
|
||||||
|
<ProjectReference>
|
||||||
|
<Properties>StaticWebAssetsEnabled=false</Properties>
|
||||||
|
<Private>false</Private>
|
||||||
|
<ExcludeAssets>runtime;native;build;buildTransitive;contentFiles</ExcludeAssets>
|
||||||
|
</ProjectReference>
|
||||||
|
</ItemDefinitionGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Resources\**" />
|
||||||
|
<ProjectReference Include="..\..\submodules\btcpayserver\BTCPayServer\BTCPayServer.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Resources\js\" />
|
||||||
|
<Folder Include="Views\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<AdditionalFiles Include="Views\Shared\FileSeller\Detect.cshtml" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
27
Plugins/BTCPayServer.Plugins.FileSeller/FileSellerPlugin.cs
Normal file
27
Plugins/BTCPayServer.Plugins.FileSeller/FileSellerPlugin.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using BTCPayServer.Abstractions.Contracts;
|
||||||
|
using BTCPayServer.Abstractions.Models;
|
||||||
|
using BTCPayServer.Abstractions.Services;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Plugins.FileSeller;
|
||||||
|
|
||||||
|
public class FileSellerPlugin : BaseBTCPayServerPlugin
|
||||||
|
{
|
||||||
|
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =
|
||||||
|
{
|
||||||
|
new() { Identifier = nameof(BTCPayServer), Condition = ">=1.10.3" }
|
||||||
|
};
|
||||||
|
public override void Execute(IServiceCollection applicationBuilder)
|
||||||
|
{
|
||||||
|
applicationBuilder.AddHostedService<FileSellerService>();
|
||||||
|
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FileSeller/Detect",
|
||||||
|
"header-nav"));
|
||||||
|
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FileSeller/Detect",
|
||||||
|
"checkout-end"));
|
||||||
|
applicationBuilder.AddSingleton<IUIExtension>(new UIExtension("FileSeller/FileSellerTemplateEditorItemDetail",
|
||||||
|
"app-template-editor-item-detail"));
|
||||||
|
|
||||||
|
|
||||||
|
base.Execute(applicationBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
144
Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs
Normal file
144
Plugins/BTCPayServer.Plugins.FileSeller/FileSellerService.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BTCPayServer.Events;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
|
using BTCPayServer.Plugins.Crowdfund;
|
||||||
|
using BTCPayServer.Plugins.PointOfSale;
|
||||||
|
using BTCPayServer.Services.Apps;
|
||||||
|
using BTCPayServer.Services.Invoices;
|
||||||
|
using BTCPayServer.Storage.Services;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Plugins.FileSeller
|
||||||
|
{
|
||||||
|
public class FileSellerService:EventHostedServiceBase
|
||||||
|
{
|
||||||
|
private readonly AppService _appService;
|
||||||
|
private readonly FileService _fileService;
|
||||||
|
private readonly InvoiceRepository _invoiceRepository;
|
||||||
|
private readonly StoredFileRepository _storedFileRepository;
|
||||||
|
|
||||||
|
public FileSellerService(EventAggregator eventAggregator,
|
||||||
|
ILogger<FileSellerService> logger,
|
||||||
|
AppService appService,
|
||||||
|
FileService fileService,
|
||||||
|
InvoiceRepository invoiceRepository,
|
||||||
|
StoredFileRepository storedFileRepository) : base(eventAggregator, logger)
|
||||||
|
{
|
||||||
|
_appService = appService;
|
||||||
|
_fileService = fileService;
|
||||||
|
_invoiceRepository = invoiceRepository;
|
||||||
|
_storedFileRepository = storedFileRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Uri UrlToUse { get; set; }
|
||||||
|
|
||||||
|
protected override void SubscribeToEvents()
|
||||||
|
{
|
||||||
|
Subscribe<InvoiceEvent>();
|
||||||
|
base.SubscribeToEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (evt is not InvoiceEvent invoiceEvent) return;
|
||||||
|
Dictionary<string, int> cartItems = null;
|
||||||
|
if (invoiceEvent.Name is not (InvoiceEvent.Completed or InvoiceEvent.MarkedCompleted or InvoiceEvent.Confirmed ))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice);
|
||||||
|
|
||||||
|
if (!appIds.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(invoiceEvent.Invoice.Metadata.AdditionalData.TryGetValue("fileselleractivated", out var activated))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
|
||||||
|
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems)))
|
||||||
|
{
|
||||||
|
var items = cartItems ?? new Dictionary<string, int>();
|
||||||
|
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode))
|
||||||
|
{
|
||||||
|
items.TryAdd(invoiceEvent.Invoice.Metadata.ItemCode, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var apps = (await _appService.GetApps(appIds)).Select(data =>
|
||||||
|
{
|
||||||
|
switch (data.AppType)
|
||||||
|
{
|
||||||
|
case PointOfSaleAppType.AppType:
|
||||||
|
var possettings = data.GetSettings<PointOfSaleSettings>();
|
||||||
|
return (Data: data, Settings: (object) possettings,
|
||||||
|
Items: AppService.Parse(possettings.Template));
|
||||||
|
case CrowdfundAppType.AppType:
|
||||||
|
var cfsettings = data.GetSettings<CrowdfundSettings>();
|
||||||
|
return (Data: data, Settings: cfsettings,
|
||||||
|
Items: AppService.Parse(cfsettings.PerksTemplate));
|
||||||
|
default:
|
||||||
|
return (null, null, null);
|
||||||
|
}
|
||||||
|
}).Where(tuple => tuple.Data != null && tuple.Items.Any(item =>
|
||||||
|
item.AdditionalData?.ContainsKey("file") is true &&
|
||||||
|
items.ContainsKey(item.Id)));
|
||||||
|
|
||||||
|
var fileIds = new HashSet<string>();
|
||||||
|
|
||||||
|
foreach (var valueTuple in apps)
|
||||||
|
{
|
||||||
|
foreach (var item1 in valueTuple.Items.Where(item =>
|
||||||
|
item.AdditionalData?.ContainsKey("file") is true &&
|
||||||
|
items.ContainsKey(item.Id)))
|
||||||
|
{
|
||||||
|
var fileId = item1.AdditionalData["file"].Value<string>();
|
||||||
|
fileIds.Add(fileId);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var loadedFiles = await _storedFileRepository.GetFiles(new StoredFileRepository.FilesQuery()
|
||||||
|
{
|
||||||
|
Id = fileIds.ToArray()
|
||||||
|
});
|
||||||
|
var productLinkTasks = loadedFiles.ToDictionary(file =>file,file => _fileService.GetTemporaryFileUrl(UrlToUse, file.Id, DateTimeOffset.MaxValue, true));
|
||||||
|
|
||||||
|
var res = await Task.WhenAll(productLinkTasks.Values);
|
||||||
|
|
||||||
|
|
||||||
|
if (res.Any(s => !string.IsNullOrEmpty(s)))
|
||||||
|
{
|
||||||
|
var productTitleToFile = productLinkTasks.Select(pair => (pair.Key.FileName, pair.Value.Result))
|
||||||
|
.Where(s => s.Result is not null)
|
||||||
|
.ToDictionary(tuple => tuple.FileName, tuple => tuple.Result);
|
||||||
|
|
||||||
|
var receiptData = new JObject();
|
||||||
|
receiptData.Add("Downloadable Content", JObject.FromObject(productTitleToFile) );
|
||||||
|
|
||||||
|
if (invoiceEvent.Invoice.Metadata.AdditionalData?.TryGetValue("receiptData",
|
||||||
|
out var existingReceiptData) is true && existingReceiptData is JObject existingReceiptDataObj )
|
||||||
|
{
|
||||||
|
receiptData.Merge(existingReceiptDataObj);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
invoiceEvent.Invoice.Metadata.SetAdditionalData("receiptData", receiptData);
|
||||||
|
|
||||||
|
}
|
||||||
|
invoiceEvent.Invoice.Metadata.SetAdditionalData("fileselleractivated", "true");
|
||||||
|
await _invoiceRepository.UpdateInvoiceMetadata(invoiceEvent.InvoiceId, invoiceEvent.Invoice.StoreId,
|
||||||
|
invoiceEvent.Invoice.Metadata.ToJObject());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
await base.ProcessEvent(evt, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Plugins/BTCPayServer.Plugins.FileSeller/README.md
Normal file
17
Plugins/BTCPayServer.Plugins.FileSeller/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# File Seller Plugin
|
||||||
|
|
||||||
|
This plugin allows you to sell files on your BTCPay Server.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Go to the "Plugins" page of your BTCPay Server
|
||||||
|
2. Click on install under the "File Seller" plugin listing
|
||||||
|
3. Restart BTCPay Server
|
||||||
|
4. Go to Server Settings => Files
|
||||||
|
5. Ensure that there is a file storage provider configured
|
||||||
|
6. Click on "Add File"
|
||||||
|
7. Go to your point of sale or crowdfund app settings
|
||||||
|
8. Click on "Edit" on the item/perk you'd like to provide a file upon purchase.
|
||||||
|
9. Select the file you'd like to provide from the new dropdown list titled "File"
|
||||||
|
10. Close the editor and save the app
|
||||||
|
11. Upon purchase (invoice marked as settled), a new section will appear on the invoice receipt with a link for each file unlocked.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@using BTCPayServer.Abstractions.Extensions
|
||||||
|
@using BTCPayServer.Plugins.FileSeller
|
||||||
|
@{
|
||||||
|
|
||||||
|
FileSellerService.UrlToUse = Context.Request.GetAbsoluteRootUri();
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
@using BTCPayServer.Storage.Services
|
||||||
|
@using BTCPayServer.Services
|
||||||
|
@using Microsoft.AspNetCore.Identity
|
||||||
|
@using BTCPayServer.Data
|
||||||
|
@inject StoredFileRepository StoredFileRepository
|
||||||
|
@inject UserManager<ApplicationUser> UserManager
|
||||||
|
@inject UserService UserService
|
||||||
|
@{
|
||||||
|
var userId = UserManager.GetUserId(User);
|
||||||
|
var files = (await StoredFileRepository.GetFiles(new StoredFileRepository.FilesQuery()
|
||||||
|
{
|
||||||
|
UserIds = await UserService.IsAdminUser(userId) ? Array.Empty<string>() : new[] {userId},
|
||||||
|
})).Select(file => new SelectListItem(file.FileName, file.Id)).Prepend(new SelectListItem("No file", ""));
|
||||||
|
}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Downloadable file</label>
|
||||||
|
<select :value="editingItem['file'] || ''" asp-items="files" class="form-select w-auto" v-on:change=" if(event.target.value) editingItem['file']= event.target.value; else delete editingItem['file'];"></select>
|
||||||
|
<span class="form-text">If a file is selected, when a user buys this item, a download link is generated in the payment receipt once the invoice is settled. <a target="_blank" asp-action="Files" asp-controller="UIServer">Upload files here</a></span>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
@using BTCPayServer.Abstractions.Services
|
||||||
|
@inject Safe Safe
|
||||||
|
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
@addTagHelper *, BTCPayServer
|
||||||
|
@addTagHelper *, BTCPayServer.Abstractions
|
||||||
Reference in New Issue
Block a user