diff --git a/src/design/App/UI/Sections/Portfolio/InvestmentDetailView.axaml b/src/design/App/UI/Sections/Portfolio/InvestmentDetailView.axaml
index 392456128..17db4e7a4 100644
--- a/src/design/App/UI/Sections/Portfolio/InvestmentDetailView.axaml
+++ b/src/design/App/UI/Sections/Portfolio/InvestmentDetailView.axaml
@@ -541,91 +541,91 @@
-
-
+
-
-
+ IsVisible="{Binding !IsRefreshing}"
+ HorizontalAlignment="Center" />
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/design/App/UI/Sections/Portfolio/InvestmentDetailView.axaml.cs b/src/design/App/UI/Sections/Portfolio/InvestmentDetailView.axaml.cs
index 2ee03ad75..53f4e0ce1 100644
--- a/src/design/App/UI/Sections/Portfolio/InvestmentDetailView.axaml.cs
+++ b/src/design/App/UI/Sections/Portfolio/InvestmentDetailView.axaml.cs
@@ -320,61 +320,61 @@ private async Task ConfirmInvestmentAsync()
investVm.IsProcessing = false;
}
- ///
- /// Cancel a pending investment request (Gap 2: CancelInvestmentRequest).
- /// Available at Step 1 (PendingFounderSignatures) and Step 2 (FounderSignaturesReceived).
- ///
- private async Task CancelInvestmentAsync()
- {
- if (DataContext is not InvestmentViewModel investVm) return;
- if (investVm.IsProcessing) return;
-
- investVm.IsProcessing = true;
-
- var portfolioVm = App.Services.GetService();
- if (portfolioVm != null)
- {
- await portfolioVm.CancelInvestmentAsync(investVm);
- }
-
- // Always reset IsProcessing — even on success the VM may still be observed by tests
- // or pending bindings. On success the detail is already closed (SelectedInvestment=null).
- investVm.IsProcessing = false;
- }
-
- ///
- /// Refresh the current investment's data from the SDK, including approval status changes.
- /// Reloads all investments to pick up founder approval, then re-selects this investment
- /// and refreshes its recovery status.
- ///
- private async Task RefreshInvestmentAsync()
- {
- if (DataContext is not InvestmentViewModel investVm) return;
- if (investVm.IsProcessing) return;
-
- investVm.IsProcessing = true;
-
- var portfolioVm = App.Services.GetService();
- if (portfolioVm != null)
- {
- var projectId = investVm.ProjectIdentifier;
-
- // Reload all investments from SDK to pick up approval status changes (#7)
- await portfolioVm.LoadInvestmentsFromSdkAsync();
-
- // Re-select the same investment (LoadInvestmentsFromSdkAsync recreates VMs)
- var refreshed = portfolioVm.Investments.FirstOrDefault(i => i.ProjectIdentifier == projectId);
- if (refreshed != null)
- {
- portfolioVm.OpenInvestmentDetail(refreshed);
- // OpenInvestmentDetail already triggers LoadRecoveryStatusAsync
- }
- }
-
- // Note: investVm may be stale now (replaced by refreshed VM), but IsProcessing
- // is set on the old VM which is no longer displayed — this is fine.
- investVm.IsProcessing = false;
- }
+ ///
+ /// Cancel a pending investment request (Gap 2: CancelInvestmentRequest).
+ /// Available at Step 1 (PendingFounderSignatures) and Step 2 (FounderSignaturesReceived).
+ ///
+ private async Task CancelInvestmentAsync()
+ {
+ if (DataContext is not InvestmentViewModel investVm) return;
+ if (investVm.IsCancelling) return;
+
+ investVm.IsCancelling = true;
+
+ var portfolioVm = App.Services.GetService();
+ if (portfolioVm != null)
+ {
+ await portfolioVm.CancelInvestmentAsync(investVm);
+ }
+
+ // Always reset IsCancelling — even on success the VM may still be observed by tests
+ // or pending bindings. On success the detail is already closed (SelectedInvestment=null).
+ investVm.IsCancelling = false;
+ }
+
+ ///
+ /// Refresh the current investment's data from the SDK, including approval status changes.
+ /// Reloads all investments to pick up founder approval, then re-selects this investment
+ /// and refreshes its recovery status.
+ ///
+ private async Task RefreshInvestmentAsync()
+ {
+ if (DataContext is not InvestmentViewModel investVm) return;
+ if (investVm.IsRefreshing) return;
+
+ investVm.IsRefreshing = true;
+
+ var portfolioVm = App.Services.GetService();
+ if (portfolioVm != null)
+ {
+ var projectId = investVm.ProjectIdentifier;
+
+ // Reload all investments from SDK to pick up approval status changes (#7)
+ await portfolioVm.LoadInvestmentsFromSdkAsync();
+
+ // Re-select the same investment (LoadInvestmentsFromSdkAsync recreates VMs)
+ var refreshed = portfolioVm.Investments.FirstOrDefault(i => i.ProjectIdentifier == projectId);
+ if (refreshed != null)
+ {
+ portfolioVm.OpenInvestmentDetail(refreshed);
+ // OpenInvestmentDetail already triggers LoadRecoveryStatusAsync
+ }
+ }
+
+ // Note: investVm may be stale now (replaced by refreshed VM), but IsRefreshing
+ // is set on the old VM which is no longer displayed — this is fine.
+ investVm.IsRefreshing = false;
+ }
///
/// Open the investment transaction in the system browser via the indexer explorer.
diff --git a/src/design/App/UI/Sections/Portfolio/PortfolioViewModel.cs b/src/design/App/UI/Sections/Portfolio/PortfolioViewModel.cs
index 293c51950..a5b7b33a9 100644
--- a/src/design/App/UI/Sections/Portfolio/PortfolioViewModel.cs
+++ b/src/design/App/UI/Sections/Portfolio/PortfolioViewModel.cs
@@ -385,6 +385,20 @@ public bool IsProcessing
set { if (_isProcessing == value) return; _isProcessing = value; OnPropertyChanged(); }
}
+ private bool _isCancelling;
+ public bool IsCancelling
+ {
+ get => _isCancelling;
+ set { if (_isCancelling == value) return; _isCancelling = value; OnPropertyChanged(); }
+ }
+
+ private bool _isRefreshing;
+ public bool IsRefreshing
+ {
+ get => _isRefreshing;
+ set { if (_isRefreshing == value) return; _isRefreshing = value; OnPropertyChanged(); }
+ }
+
private string? _errorMessage;
/// Error message shown when a recovery operation fails.
public string? ErrorMessage