Repository: btcpayserver/btcpayserver (csharp)

Issue:
Add updated image upload support on Crowdfund plugin
Just realized we have image upload form support, but not on the Crowdfund.

For those who pick up, see other examples in the app to apply to this instance.

<img width="697" alt="Screen Shot 2024-09-23 at 10 16 53 PM" src="https://github.com/user-attachments/assets/daa156a8-006d-432e-a87e-b1e615d58004">

Pull request description:
Resolves #6242 

By changing the featured image from an input field to a custom upload

@dstrukt  @dennisreimann

Patch:
diff --git a/BTCPayServer.Abstractions/Contracts/IFileService.cs b/BTCPayServer.Abstractions/Contracts/IFileService.cs
index 9a680e3324..db1427dcea 100644
--- a/BTCPayServer.Abstractions/Contracts/IFileService.cs
+++ b/BTCPayServer.Abstractions/Contracts/IFileService.cs
@@ -2,6 +2,7 @@
 using System;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Http;
+using BTCPayServer.Abstractions.Models;
 
 namespace BTCPayServer.Abstractions.Contracts;
 
@@ -14,4 +15,5 @@ public interface IFileService
     Task<string?> GetTemporaryFileUrl(Uri baseUri, string fileId, DateTimeOffset expiry,
         bool isDownload);
     Task RemoveFile(string fileId, string userId);
+    Task<UploadImageResultModel> UploadImage(IFormFile file, string userId, long maxFileSizeInBytes = 1_000_000);
 }
diff --git a/BTCPayServer.Abstractions/Models/UploadImageResultModel.cs b/BTCPayServer.Abstractions/Models/UploadImageResultModel.cs
new file mode 100644
index 0000000000..2c19af7195
--- /dev/null
+++ b/BTCPayServer.Abstractions/Models/UploadImageResultModel.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using BTCPayServer.Abstractions.Contracts;
+
+namespace BTCPayServer.Abstractions.Models;
+
+public class UploadImageResultModel
+{
+    public bool Success { get; set; }
+    public string Response { get; set; } = string.Empty;
+    public IStoredFile? StoredFile { get; set; }
+}
diff --git a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs
index e17a7fb165..1b33362d13 100644
--- a/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs
+++ b/BTCPayServer/Controllers/GreenField/GreenfieldAppsController.cs
@@ -9,6 +9,7 @@
 using BTCPayServer.Data;
 using BTCPayServer.Plugins.Crowdfund;
 using BTCPayServer.Plugins.PointOfSale;
+using BTCPayServer.Services;
 using BTCPayServer.Services.Apps;
 using BTCPayServer.Services.Rates;
 using BTCPayServer.Services.Stores;
@@ -28,12 +29,14 @@ namespace BTCPayServer.Controllers.Greenfield
     public class GreenfieldAppsController : ControllerBase
     {
         private readonly AppService _appService;
+        private readonly UriResolver _uriResolver;
         private readonly StoreRepository _storeRepository;
         private readonly CurrencyNameTable _currencies;
         private readonly UserManager<ApplicationUser> _userManager;
 
         public GreenfieldAppsController(
             AppService appService,
+            UriResolver uriResolver,
             StoreRepository storeRepository,
             BTCPayNetworkProvider btcPayNetworkProvider,
             CurrencyNameTable currencies,
@@ -41,6 +44,7 @@ UserManager<ApplicationUser> userManager
         )
         {
             _appService = appService;
+            _uriResolver = uriResolver;
             _storeRepository = storeRepository;
             _currencies = currencies;
             _userManager = userManager;
@@ -72,12 +76,12 @@ public async Task<IActionResult> CreateCrowdfundApp(string storeId, CrowdfundApp
                 Archived = request.Archived ?? false
             };
 
-            var settings = ToCrowdfundSettings(request, new CrowdfundSettings { Title = request.Title ?? request.AppName });
+            var settings = ToCrowdfundSettings(request);
             appData.SetSettings(settings);
 
             await _appService.UpdateOrCreateApp(appData);
-
-            return Ok(ToCrowdfundModel(appData));
+            var model = await ToCrowdfundModel(appData);
+            return Ok(model);
         }
 
         [HttpPost("~/api/v1/stores/{storeId}/apps/pos")]
@@ -208,7 +212,8 @@ public async Task<IActionResult> GetCrowdfundApp(string appId)
                 return AppNotFound();
             }
 
-            return Ok(ToCrowdfundModel(app));
+            var model = await ToCrowdfundModel(app);
+            return Ok(model);
         }
 
         [HttpDelete("~/api/v1/apps/{appId}")]
@@ -255,7 +260,7 @@ private IActionResult AppNotFound()
             return this.CreateAPIError(404, "app-not-found", "The app with specified ID was not found");
         }
 
-        private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request, CrowdfundSettings settings)
+        private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request)
         {
             var parsedSounds = ValidateStringArray(request.Sounds);
             var parsedColors = ValidateStringArray(request.AnimationColors);
@@ -271,7 +276,7 @@ private CrowdfundSettings ToCrowdfundSettings(CrowdfundAppRequest request, Crowd
                 Description = request.Description?.Trim(),
                 EndDate = request.EndDate?.UtcDateTime,
                 TargetAmount = request.TargetAmount,
-                MainImageUrl = request.MainImageUrl?.Trim(),
+                MainImageUrl = request.MainImageUrl == null ? null : UnresolvedUri.Create(request.MainImageUrl),
                 NotificationUrl = request.NotificationUrl?.Trim(),
                 Tagline = request.Tagline?.Trim(),
                 PerksTemplate = request.PerksTemplate is not null ? AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate.Trim())) : null,
@@ -411,7 +416,7 @@ private void ValidatePOSAppRequest(PointOfSaleAppRequest request)
             }
         }
 
-        private CrowdfundAppData ToCrowdfundModel(AppData appData)
+        private async Task<CrowdfundAppData> ToCrowdfundModel(AppData appData)
         {
             var settings = appData.GetSettings<CrowdfundSettings>();
             Enum.TryParse<CrowdfundResetEvery>(settings.ResetEvery.ToString(), true, out var resetEvery);
@@ -432,7 +437,7 @@ private CrowdfundAppData ToCrowdfundModel(AppData appData)
                 Description = settings.Description,
                 EndDate = settings.EndDate,
                 TargetAmount = settings.TargetAmount,
-                MainImageUrl = settings.MainImageUrl,
+                MainImageUrl = settings.MainImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), settings.MainImageUrl),
                 NotificationUrl = settings.NotificationUrl,
                 Tagline = settings.Tagline,
                 DisqusEnabled = settings.DisqusEnabled,
diff --git a/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs b/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs
index 321eab3923..7a61350811 100644
--- a/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs
+++ b/BTCPayServer/Plugins/Crowdfund/Controllers/UICrowdfundController.cs
@@ -4,8 +4,10 @@
 using System.Threading;
 using System.Threading.Tasks;
 using BTCPayServer.Abstractions.Constants;
+using BTCPayServer.Abstractions.Contracts;
 using BTCPayServer.Abstractions.Extensions;
 using BTCPayServer.Abstractions.Form;
+using BTCPayServer.Abstractions.Models;
 using BTCPayServer.Client;
 using BTCPayServer.Client.Models;
 using BTCPayServer.Controllers;
@@ -44,6 +46,7 @@ public UICrowdfundController(
             EventAggregator eventAggregator,
             UriResolver uriResolver,
             StoreRepository storeRepository,
+            IFileService fileService,
             UIInvoiceController invoiceController,
             UserManager<ApplicationUser> userManager,
             FormDataService formDataService,
@@ -53,6 +56,7 @@ public UICrowdfundController(
             _appService = appService;
             _userManager = userManager;
             _app = app;
+            _fileService = fileService;
             _storeRepository = storeRepository;
             _eventAggregator = eventAggregator;
             _uriResolver = uriResolver;
@@ -61,6 +65,7 @@ public UICrowdfundController(
         }
 
         private readonly EventAggregator _eventAggregator;
+        private readonly IFileService _fileService;
         private readonly UriResolver _uriResolver;
         private readonly CurrencyNameTable _currencies;
         private readonly StoreRepository _storeRepository;
@@ -393,6 +398,7 @@ public async Task<IActionResult> UpdateCrowdfund(string appId)
 
             var settings = app.GetSettings<CrowdfundSettings>();
             var resetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery);
+
             var vm = new UpdateCrowdfundViewModel
             {
                 Title = settings.Title,
@@ -405,8 +411,8 @@ public async Task<IActionResult> UpdateCrowdfund(string appId)
                 EnforceTargetAmount = settings.EnforceTargetAmount,
                 StartDate = settings.StartDate,
                 TargetCurrency = settings.TargetCurrency,
+                MainImageUrl = settings.MainImageUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), settings.MainImageUrl),
                 Description = settings.Description,
-                MainImageUrl = settings.MainImageUrl,
                 EndDate = settings.EndDate,
                 TargetAmount = settings.TargetAmount,
                 NotificationUrl = settings.NotificationUrl,
@@ -434,8 +440,13 @@ public async Task<IActionResult> UpdateCrowdfund(string appId)
 
         [Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
         [HttpPost("{appId}/settings/crowdfund")]
-        public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm, string command)
+        public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm,
+            [FromForm] bool RemoveLogoFile = false)
         {
+            var userId = GetUserId();
+            if (userId is null)
+                return NotFound();
+
             var app = GetCurrentApp();
             if (app == null)
                 return NotFound();
@@ -503,6 +514,16 @@ public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundVi
                 parsedAnimationColors = new CrowdfundSettings().AnimationColors;
             }
 
+            UploadImageResultModel imageUpload = null;
+            if (vm.MainImageFile != null)
+            {
+                imageUpload = await _fileService.UploadImage(vm.MainImageFile, userId);
+                if (!imageUpload.Success)
+                {
+                    ModelState.AddModelError(nameof(vm.MainImageFile), imageUpload.Response);
+                }
+            }
+
             if (!ModelState.IsValid)
             {
                 return View("Crowdfund/UpdateCrowdfund", vm);
@@ -520,7 +541,7 @@ public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundVi
                 Description = vm.Description,
                 EndDate = vm.EndDate?.ToUniversalTime(),
                 TargetAmount = vm.TargetAmount,
-                MainImageUrl = vm.MainImageUrl,
+                MainImageUrl = app.GetSettings<CrowdfundSettings>()?.MainImageUrl,
                 NotificationUrl = vm.NotificationUrl,
                 Tagline = vm.Tagline,
                 PerksTemplate = vm.PerksTemplate,
@@ -538,6 +559,17 @@ public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundVi
                 FormId = vm.FormId
             };
 
+            if (imageUpload?.Success is true)
+            {
+                newSettings.MainImageUrl = new UnresolvedUri.FileIdUri(imageUpload.StoredFile.Id);
+            }
+            else if (RemoveLogoFile)
+            {
+                newSettings.MainImageUrl = null;
+                vm.MainImageUrl = null;
+                vm.MainImageFile = null;
+            }
+
             app.TagAllInvoices = vm.UseAllStoreInvoices;
             app.SetSettings(newSettings);
 
diff --git a/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs b/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs
index 24b3c2c37f..c418935850 100644
--- a/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs
+++ b/BTCPayServer/Plugins/Crowdfund/CrowdfundPlugin.cs
@@ -4,6 +4,7 @@
 using System.Linq;
 using System.Threading.Tasks;
 using BTCPayServer.Abstractions.Contracts;
+using BTCPayServer.Abstractions.Extensions;
 using BTCPayServer.Abstractions.Models;
 using BTCPayServer.Abstractions.Services;
 using BTCPayServer.Client.Models;
@@ -49,22 +50,28 @@ public class CrowdfundAppType : AppBaseType, IHasSaleStatsAppType, IHasItemStats
         private readonly IOptions<BTCPayServerOptions> _options;
         private readonly DisplayFormatter _displayFormatter;
         private readonly CurrencyNameTable _currencyNameTable;
+        private readonly UriResolver _uriResolver;
         private readonly InvoiceRepository _invoiceRepository;
+        private readonly IHttpContextAccessor _httpContextAccessor;
         private readonly PrettyNameProvider _prettyNameProvider;
         public const string AppType = "Crowdfund";
 
         public CrowdfundAppType(
             LinkGenerator linkGenerator,
             IOptions<BTCPayServerOptions> options,
+            UriResolver uriResolver,
             InvoiceRepository invoiceRepository,
             PrettyNameProvider prettyNameProvider,
             DisplayFormatter displayFormatter,
+            IHttpContextAccessor httpContextAccessor,
             CurrencyNameTable currencyNameTable)
         {
             Description = Type = AppType;
             _linkGenerator = linkGenerator;
             _options = options;
+            _uriResolver = uriResolver;
             _displayFormatter = displayFormatter;
+            _httpContextAccessor = httpContextAccessor;
             _currencyNameTable = currencyNameTable;
             _invoiceRepository = invoiceRepository;
             _prettyNameProvider = prettyNameProvider;
@@ -186,12 +193,11 @@ public Task<IEnumerable<AppItemStats>> GetItemStats(AppData appData, InvoiceEnti
                 ? _linkGenerator.GetPathByAction(nameof(UICrowdfundController.CrowdfundForm), "UICrowdfund",
                     new { appId = appData.Id }, _options.Value.RootPath)
                 : null;
-            return new ViewCrowdfundViewModel
+            var vm =  new ViewCrowdfundViewModel
             {
                 Title = settings.Title,
                 Tagline = settings.Tagline,
                 Description = settings.Description,
-                MainImageUrl = settings.MainImageUrl,
                 StoreName = store.StoreName,
                 StoreId = appData.StoreDataId,
                 AppId = appData.Id,
@@ -230,6 +236,12 @@ public Task<IEnumerable<AppItemStats>> GetItemStats(AppData appData, InvoiceEnti
                     CurrentAmount = currentPayments.TotalCurrency
                 }
             };
+            var httpContext = _httpContextAccessor.HttpContext;
+            if (httpContext != null && settings.MainImageUrl != null)
+            {
+                vm.MainImageUrl = await _uriResolver.Resolve(httpContext.Request.GetAbsoluteRootUri(), settings.MainImageUrl);
+            }
+            return vm;
         }
 
         private Dictionary<string, PaymentStat> GetPaymentStats(InvoiceStatistics stats)
diff --git a/BTCPayServer/Plugins/Crowdfund/Models/UpdateCrowdfundViewModel.cs b/BTCPayServer/Plugins/Crowdfund/Models/UpdateCrowdfundViewModel.cs
index 57e7b153a3..2c46c49bf9 100644
--- a/BTCPayServer/Plugins/Crowdfund/Models/UpdateCrowdfundViewModel.cs
+++ b/BTCPayServer/Plugins/Crowdfund/Models/UpdateCrowdfundViewModel.cs
@@ -4,6 +4,8 @@
 using System.Linq;
 using BTCPayServer.Services.Apps;
 using BTCPayServer.Validation;
+using Microsoft.AspNetCore.Http;
+using Newtonsoft.Json;
 
 namespace BTCPayServer.Plugins.Crowdfund.Models
 {
@@ -32,6 +34,10 @@ public class UpdateCrowdfundViewModel
         [Display(Name = "Featured Image URL")]
         public string MainImageUrl { get; set; }
 
+        [Display(Name = "Featured Image URL")]
+        [JsonIgnore]
+        public IFormFile MainImageFile { get; set; }
+
         [Display(Name = "Callback Notification URL")]
         [Uri]
         public string NotificationUrl { get; set; }
diff --git a/BTCPayServer/Services/Apps/CrowdfundSettings.cs b/BTCPayServer/Services/Apps/CrowdfundSettings.cs
index 974e2e3915..72e6434dc9 100644
--- a/BTCPayServer/Services/Apps/CrowdfundSettings.cs
+++ b/BTCPayServer/Services/Apps/CrowdfundSettings.cs
@@ -1,4 +1,6 @@
 using System;
+using BTCPayServer.JsonConverters;
+using Newtonsoft.Json;
 
 namespace BTCPayServer.Services.Apps
 {
@@ -27,7 +29,8 @@ public decimal? TargetAmount
         }
 
         public bool EnforceTargetAmount { get; set; }
-        public string MainImageUrl { get; set; }
+        [JsonConverter(typeof(UnresolvedUriJsonConverter))]
+        public UnresolvedUri MainImageUrl { get; set; }
         public string NotificationUrl { get; set; }
         public string Tagline { get; set; }
         public string PerksTemplate { get; set; }
diff --git a/BTCPayServer/Storage/Services/FileService.cs b/BTCPayServer/Storage/Services/FileService.cs
index 837fb27229..7a7e8be3f3 100644
--- a/BTCPayServer/Storage/Services/FileService.cs
+++ b/BTCPayServer/Storage/Services/FileService.cs
@@ -6,10 +6,12 @@
 using System.Linq;
 using System.Net.Http;
 using System.Text.RegularExpressions;
+using System.Threading;
 using System.Threading.Tasks;
 using System.Web;
 using BTCPayServer.Abstractions.Contracts;
 using BTCPayServer.Abstractions.Extensions;
+using BTCPayServer.Abstractions.Models;
 using BTCPayServer.Configuration;
 using BTCPayServer.Services;
 using BTCPayServer.Storage.Models;
@@ -47,6 +49,44 @@ public async Task<bool> IsAvailable()
             return settings is not null;
         }
 
+        public async Task<UploadImageResultModel> UploadImage(IFormFile file, string userId, long maxFileSizeInBytes = 1_000_000)
+        {
+            var result = new UploadImageResultModel();
+
+            if (file.Length > maxFileSizeInBytes)
+            {
+                result.Success = false;
+                result.Response = $"The uploaded image file should be less than {maxFileSizeInBytes / 1_000_000}MB";
+                return result;
+            }
+            if (!file.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
+            {
+                result.Success = false;
+                result.Response = "The uploaded file needs to be an image (based on content type)";
+                return result;
+            }
+            var formFile = await file.Bufferize();
+            if (!FileTypeDetector.IsPicture(formFile.Buffer, formFile.FileName))
+            {
+                result.Success = false;
+                result.Response = "The uploaded file needs to be an image (based on file content)";
+                return result;
+            }
+            try
+            {
+                result.StoredFile = await AddFile(formFile, userId);
+                result.Success = true;
+                result.Response = "Image uploaded successfully";
+            }
+            catch (Exception e)
+            {
+                result.Success = false;
+                result.Response = $"Could not save image: {e.Message}";
+            }
+
+            return result;
+        }
+
         public async Task<IStoredFile> AddFile(IFormFile file, string userId)
         {
             var settings = await _settingsRepository.GetSettingAsync<StorageSettings>();
diff --git a/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml b/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml
index 3bf9c2b01f..500821fef5 100644
--- a/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml
+++ b/BTCPayServer/Views/Shared/Crowdfund/UpdateCrowdfund.cshtml
@@ -1,4 +1,5 @@
 @using System.Globalization
+@using BTCPayServer.Abstractions.Contracts
 @using BTCPayServer.Abstractions.Models
 @using BTCPayServer.Client
 @using BTCPayServer.TagHelpers
@@ -6,11 +7,13 @@
 @using Microsoft.AspNetCore.Mvc.TagHelpers
 @using BTCPayServer.Forms
 @inject FormDataService FormDataService
+@inject IFileService FileService
 @inject BTCPayServer.Security.ContentSecurityPolicies Csp
 @model BTCPayServer.Plugins.Crowdfund.Models.UpdateCrowdfundViewModel
 @{
     ViewData.SetActivePage(AppsNavPages.Update, "Update Crowdfund", Model.AppId);
 	Csp.UnsafeEval();
+    var canUpload = await FileService.IsAvailable();
     var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
 }
 
@@ -25,7 +28,7 @@
     <script src="~/crowdfund/admin.js" asp-append-version="true"></script>
 }
 
-<form method="post" permissioned="@Policies.CanModifyStoreSettings">
+<form method="post" enctype="multipart/form-data" permissioned="@Policies.CanModifyStoreSettings">
     <div class="sticky-header">
         <h2 text-translate="true">@ViewData["Title"]</h2>
         <div>
@@ -74,9 +77,31 @@
                 <span asp-validation-for="Tagline" class="text-danger"></span>
             </div>
             <div class="form-group">
-                <label asp-for="MainImageUrl" class="form-label"></label>
-                <input asp-for="MainImageUrl" class="form-control" />
-                <span asp-validation-for="MainImageUrl" class="text-danger"></span>
+                <div class="d-flex align-items-center justify-content-between gap-2">
+                    <label asp-for="MainImageFile" class="form-label"></label>
+                    @if (!string.IsNullOrEmpty(Model.MainImageUrl))
+                    {
+                        <button type="submit" class="btn btn-link p-0 text-danger" name="RemoveLogoFile" value="true">
+                            <vc:icon symbol="cross" /> Remove
+                        </button>
+                    }
+                </div>
+                @if (canUpload)
+                {
+                    <div class="d-flex align-items-center gap-3">
+                        <input asp-for="MainImageFile" type="file" class="form-control flex-grow">
+                        @if (!string.IsNullOrEmpty(Model.MainImageUrl))
+                        {
+                            <img src="@Model.MainImageUrl" alt="Logo" style="height:2.1rem;max-width:10.5rem;" />
+                        }
+                    </div>
+                    <span asp-validation-for="MainImageFile" class="text-danger"></span>
+                }
+                else
+                {
+                    <input asp-for="MainImageFile" type="file" class="form-control" disabled>
+                    <div class="form-text">In order to upload an image, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</div>
+                }
             </div>
             <div class="form-group">
                 <div class="d-flex align-items-center">


Test patch:
diff --git a/BTCPayServer.Tests/CrowdfundTests.cs b/BTCPayServer.Tests/CrowdfundTests.cs
index 31bdb9393c..a0b7c764e4 100644
--- a/BTCPayServer.Tests/CrowdfundTests.cs
+++ b/BTCPayServer.Tests/CrowdfundTests.cs
@@ -81,7 +81,7 @@ public async Task CanCreateAndDeleteCrowdfundApp()
             Assert.False(app.Archived);
             var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
             crowdfundViewModel.Enabled = true;
-            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
+            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
             Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
             // Delete
             Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
@@ -121,7 +121,7 @@ public async Task CanContributeOnlyWhenAllowed()
             crowdfundViewModel.Enabled = false;
             crowdfundViewModel.EndDate = null;
 
-            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
+            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
 
             var anonAppPubsController = tester.PayTester.GetController<UICrowdfundController>();
             var crowdfundController = user.GetController<UICrowdfundController>();
@@ -146,7 +146,7 @@ public async Task CanContributeOnlyWhenAllowed()
             crowdfundViewModel.StartDate = DateTime.Today.AddDays(2);
             crowdfundViewModel.Enabled = true;
 
-            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
+            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
             Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
             {
                 Amount = new decimal(0.01)
@@ -157,7 +157,7 @@ public async Task CanContributeOnlyWhenAllowed()
             crowdfundViewModel.EndDate = DateTime.Today.AddDays(-1);
             crowdfundViewModel.Enabled = true;
 
-            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
+            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
             Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
             {
                 Amount = new decimal(0.01)
@@ -170,7 +170,7 @@ public async Task CanContributeOnlyWhenAllowed()
             crowdfundViewModel.TargetAmount = 1;
             crowdfundViewModel.TargetCurrency = "BTC";
             crowdfundViewModel.EnforceTargetAmount = true;
-            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
+            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
             Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(app.Id, new ContributeToCrowdfund()
             {
                 Amount = new decimal(1.01)
@@ -214,7 +214,7 @@ public async Task CanComputeCrowdfundModel()
             crowdfundViewModel.TargetCurrency = "BTC";
             crowdfundViewModel.UseAllStoreInvoices = true;
             crowdfundViewModel.EnforceTargetAmount = true;
-            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
+            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
 
             var publicApps = user.GetController<UICrowdfundController>();
 
@@ -268,7 +268,7 @@ public async Task CanComputeCrowdfundModel()
             Assert.Contains(AppService.GetAppInternalTag(app.Id), invoiceEntity.InternalTags);
 
             crowdfundViewModel.UseAllStoreInvoices = false;
-            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
+            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
 
             TestLogs.LogInformation("Because UseAllStoreInvoices is false, let's make sure the invoice is not tagged");
             invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
@@ -287,7 +287,7 @@ public async Task CanComputeCrowdfundModel()
             TestLogs.LogInformation("After turning setting a softcap, let's check that only actual payments are counted");
             crowdfundViewModel.EnforceTargetAmount = false;
             crowdfundViewModel.UseAllStoreInvoices = true;
-            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
+            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
             invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
             {
                 Buyer = new Buyer { email = "test@fwf.com" },
@@ -356,7 +356,7 @@ public async Task CrowdfundWithFormNoPerk()
             crowdfundViewModel.FormId = lstForms[0].Id;
             crowdfundViewModel.TargetCurrency = "BTC";
             crowdfundViewModel.Enabled = true;
-            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
+            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
 
             var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01).AssertViewModelAsync<FormViewModel>();
             var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "", vm2);
@@ -411,7 +411,7 @@ public async Task CrowdfundWithFormAndPerk()
             crowdfundViewModel.TargetCurrency = "BTC";
             crowdfundViewModel.Enabled = true;
             crowdfundViewModel.PerksTemplate = "[{\"id\": \"xxx\",\"title\": \"Perk 1\",\"priceType\": \"Fixed\",\"price\": \"0.001\",\"image\": \"\",\"description\": \"\",\"categories\": [],\"disabled\": false}]";
-            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
+            Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel).Result);
 
             var vm2 = await crowdfund.CrowdfundForm(app.Id, (decimal?)0.01, "xxx").AssertViewModelAsync<FormViewModel>();
             var res = await crowdfund.CrowdfundFormSubmit(app.Id, (decimal)0.01, "xxx", vm2);