Skip to content

Commit b1313f1

Browse files
authored
Review and fix "My Projects" section (block-core#688)
1 parent 551a992 commit b1313f1

File tree

129 files changed

+1537
-750
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

129 files changed

+1537
-750
lines changed

src/Angor/Avalonia/AngorApp.Model/Funded/Fund/Samples/FundProjectSample.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class FundProjectSample : IFundProject
1414
public string NostrNpubKeyHex { get; } = "ca6e84aa974d00af805a754b34bc4e3c9a899aac14487a6f2e21fe9ea4b9fe43";
1515
public Uri? InformationUri { get; } = null;
1616
public IEnhancedCommand<Result> Invest { get; } = EnhancedCommand.CreateWithResult(Result.Success);
17+
public IEnhancedCommand ManageFunds { get; } = EnhancedCommand.Create(() => { }, Observable.Return(false), text: "Manage Funds");
1718
public IEnhancedCommand Refresh { get; } = EnhancedCommand.Create(() => { });
1819
public IAmountUI Goal { get; set; } = AmountUI.FromBtc(2m);
1920
public IObservable<IAmountUI> Funded { get; } = Observable.Return(AmountUI.FromBtc(1.5m));

src/Angor/Avalonia/AngorApp.Model/Funded/Investment/Samples/InvestmentProjectSample.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ public class InvestmentProjectSample : IInvestmentProject
1515
public string NostrNpubKeyHex { get; } = "ca6e84aa974d00af805a754b34bc4e3c9a899aac14487a6f2e21fe9ea4b9fe43";
1616
public Uri? InformationUri { get; } = null;
1717
public IEnhancedCommand<Result> Invest { get; } = EnhancedCommand.CreateWithResult(Result.Success);
18+
public IEnhancedCommand ManageFunds { get; } = EnhancedCommand.Create(() => { });
1819
public IEnhancedCommand Refresh { get; } = EnhancedCommand.Create(() => { });
1920
public IAmountUI Target { get; set; } = new AmountUI(100_000_000_000);
2021
public IObservable<IAmountUI> Raised { get; set; } = Observable.Return(new AmountUI(24_000_000_000));
22+
public IObservable<IAmountUI> TotalInvestment { get; set; } = Observable.Return(new AmountUI(24_000_000_000));
23+
public IObservable<IAmountUI> AvailableBalance { get; set; } = Observable.Return(new AmountUI(15_000_000_000));
24+
public IObservable<IAmountUI> Withdrawable { get; set; } = Observable.Return(new AmountUI(8_000_000_000));
25+
public IObservable<int> TotalStages { get; set; } = Observable.Return(4);
2126
public IObservable<int> InvestorCount { get; set; } = Observable.Return(120);
2227
public IObservable<IReadOnlyCollection<IStage>> Stages { get; } = Observable.Return(new[] { new StageSample() });
2328
public DateTimeOffset FundingStart { get; } = DateTimeOffset.Now.AddDays(-60);

src/Angor/Avalonia/AngorApp.Model/ProjectsV2/FundProject/FundProject.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,19 @@ namespace AngorApp.Model.ProjectsV2.FundProject
77
{
88
public class FundProject : Project, IFundProject
99
{
10-
public FundProject(ProjectDto seed, IProjectAppService projectAppService, IEnhancedCommand<Result> invest) : base(seed, invest)
10+
public FundProject(ProjectDto seed, IProjectAppService projectAppService, IEnhancedCommand<Result> invest, IEnhancedCommand? manageFunds = null) : base(seed, invest, manageFunds ?? CreateUnsupportedManageFundsCommand())
1111
{
1212
var refresh = EnhancedCommand.CreateWithResult(() => projectAppService.GetProjectStatistics(seed.Id));
1313
var projectStatistics = refresh.Successes();
1414

15-
Funded = projectStatistics.Select(ToFundedAmount);
16-
FunderCount = projectStatistics.Select(ToFunderCount);
15+
Funded = projectStatistics.Select(ToFundedAmount).ReplayLastActive();
16+
FunderCount = projectStatistics.Select(ToFunderCount).ReplayLastActive();
1717
Goal = new AmountUI(seed.TargetAmount);
1818
Refresh = refresh;
1919
Payments = projectStatistics
2020
.Select(ToPayments)
21-
.StartWith([]);
21+
.StartWith([])
22+
.ReplayLastActive();
2223
}
2324

2425
public IAmountUI Goal { get; }

src/Angor/Avalonia/AngorApp.Model/ProjectsV2/IProject.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public interface IProject
1313
string NostrNpubKeyHex { get; }
1414
Uri? InformationUri { get; }
1515
IEnhancedCommand<Result> Invest { get; }
16+
IEnhancedCommand ManageFunds { get; }
1617
IEnhancedCommand Refresh { get; }
1718
}
1819
}

src/Angor/Avalonia/AngorApp.Model/ProjectsV2/InvestmentProject/IInvestmentProject.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ public interface IInvestmentProject : IProject
66
{
77
public IAmountUI Target { get; }
88
public IObservable<IAmountUI> Raised { get; }
9+
public IObservable<IAmountUI> TotalInvestment { get; }
10+
public IObservable<IAmountUI> AvailableBalance { get; }
11+
public IObservable<IAmountUI> Withdrawable { get; }
12+
public IObservable<int> TotalStages { get; }
913
public IObservable<IReadOnlyCollection<IStage>> Stages { get; }
1014
public IObservable<int> InvestorCount { get; }
1115
public DateTimeOffset FundingStart { get; }

src/Angor/Avalonia/AngorApp.Model/ProjectsV2/InvestmentProject/InvestmentProject.cs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,43 @@
11
using Angor.Sdk.Funding.Projects;
22
using Angor.Sdk.Funding.Projects.Dtos;
33
using Zafiro.CSharpFunctionalExtensions;
4+
using Zafiro.Reactive;
45

56
namespace AngorApp.Model.ProjectsV2.InvestmentProject
67
{
78
public class InvestmentProject : Project, IInvestmentProject
89
{
9-
public InvestmentProject(ProjectDto seed, IProjectAppService projectAppService, IEnhancedCommand<Result> invest) : base(seed, invest)
10+
public InvestmentProject(ProjectDto seed, IProjectAppService projectAppService, IEnhancedCommand<Result> invest, IEnhancedCommand? manageFunds = null) : base(seed, invest, manageFunds ?? CreateUnsupportedManageFundsCommand())
1011
{
1112
var refresh = EnhancedCommand.CreateWithResult(() => projectAppService.GetProjectStatistics(seed.Id));
1213
var projectStatistics = refresh.Successes();
1314
var seedStages = seed.Stages ?? [];
1415
IReadOnlyCollection<IStage> mapStages() => Stage.MapFrom(seedStages, seed.TargetAmount);
1516

16-
Raised = projectStatistics.Select(ToRaisedAmount);
17-
InvestorCount = projectStatistics.Select(ToInvestorCount);
17+
Raised = projectStatistics.Select(ToRaisedAmount).ReplayLastActive();
18+
TotalInvestment = projectStatistics.Select(ToTotalInvestment).ReplayLastActive();
19+
AvailableBalance = projectStatistics.Select(ToAvailableBalance).ReplayLastActive();
20+
Withdrawable = projectStatistics.Select(ToWithdrawable).ReplayLastActive();
21+
TotalStages = projectStatistics.Select(ToTotalStages).ReplayLastActive();
22+
InvestorCount = projectStatistics.Select(ToInvestorCount).ReplayLastActive();
1823
Target = new AmountUI(seed.TargetAmount);
1924
FundingStart = seed.FundingStartDate;
2025
FundingEnd = seed.FundingEndDate;
2126
Refresh = refresh;
2227
Stages = projectStatistics
2328
.Select(_ => mapStages())
24-
.StartWith(mapStages());
29+
.StartWith(mapStages())
30+
.ReplayLastActive();
2531
PenaltyDuration = seed.PenaltyDuration;
2632
PenaltyThreshold = ToPenaltyThreshold(seed);
2733
}
2834

2935
public IAmountUI Target { get; }
3036
public IObservable<IAmountUI> Raised { get; }
37+
public IObservable<IAmountUI> TotalInvestment { get; }
38+
public IObservable<IAmountUI> AvailableBalance { get; }
39+
public IObservable<IAmountUI> Withdrawable { get; }
40+
public IObservable<int> TotalStages { get; }
3141
public IObservable<int> InvestorCount { get; }
3242
public DateTimeOffset FundingStart { get; }
3343
public DateTimeOffset FundingEnd { get; }
@@ -44,6 +54,26 @@ private static IAmountUI ToRaisedAmount(ProjectStatisticsDto statistics)
4454
return new AmountUI(statistics.TotalInvested);
4555
}
4656

57+
private static IAmountUI ToTotalInvestment(ProjectStatisticsDto statistics)
58+
{
59+
return new AmountUI(statistics.TotalInvested);
60+
}
61+
62+
private static IAmountUI ToAvailableBalance(ProjectStatisticsDto statistics)
63+
{
64+
return new AmountUI(statistics.AvailableBalance);
65+
}
66+
67+
private static IAmountUI ToWithdrawable(ProjectStatisticsDto statistics)
68+
{
69+
return new AmountUI(statistics.WithdrawableAmount);
70+
}
71+
72+
private static int ToTotalStages(ProjectStatisticsDto statistics)
73+
{
74+
return statistics.TotalStages;
75+
}
76+
4777
private static int ToInvestorCount(ProjectStatisticsDto statistics)
4878
{
4979
return statistics.TotalInvestors ?? 0;
Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
using Angor.Sdk.Funding.Projects;
21
using Angor.Sdk.Funding.Projects.Dtos;
32
using Angor.Sdk.Funding.Shared;
43

54
namespace AngorApp.Model.ProjectsV2
65
{
76
public abstract class Project : IProject
87
{
9-
protected Project(ProjectDto seed, IEnhancedCommand<Result> invest)
8+
protected Project(ProjectDto seed, IEnhancedCommand<Result> invest, IEnhancedCommand manageFunds)
109
{
1110
Name = seed.Name;
1211
Description = seed.ShortDescription;
@@ -17,6 +16,7 @@ protected Project(ProjectDto seed, IEnhancedCommand<Result> invest)
1716
NostrNpubKeyHex = seed.NostrNpubKeyHex;
1817
InformationUri = seed.InformationUri;
1918
Invest = invest;
19+
ManageFunds = manageFunds;
2020
}
2121

2222
public string Name { get; }
@@ -28,19 +28,17 @@ protected Project(ProjectDto seed, IEnhancedCommand<Result> invest)
2828
public string NostrNpubKeyHex { get; }
2929
public Uri? InformationUri { get; }
3030
public IEnhancedCommand<Result> Invest { get; }
31+
public IEnhancedCommand ManageFunds { get; }
3132
public abstract IEnhancedCommand Refresh { get; }
3233
public abstract IAmountUI FundingTarget { get; }
3334
public abstract IObservable<IAmountUI> FundingRaised { get; }
3435
public abstract IObservable<int> SupporterCount { get; }
3536

36-
public static IProject Create(ProjectDto seed, IProjectAppService projectAppService, IEnhancedCommand<Result> invest)
37+
protected static IEnhancedCommand CreateUnsupportedManageFundsCommand()
3738
{
38-
return seed.ProjectType switch
39-
{
40-
Angor.Shared.Models.ProjectType.Invest => new InvestmentProject.InvestmentProject(seed, projectAppService, invest),
41-
Angor.Shared.Models.ProjectType.Fund => new FundProject.FundProject(seed, projectAppService, invest),
42-
_ => throw new ArgumentOutOfRangeException(nameof(seed.ProjectType), "Unsupported project type")
43-
};
39+
return EnhancedCommand.Create(
40+
() => throw new NotSupportedException("Manage funds is not supported for this project type."),
41+
Observable.Return(false), text: "N/A");
4442
}
4543
}
4644
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using Angor.Sdk.Funding.Projects;
2+
using Angor.Sdk.Funding.Projects.Dtos;
3+
using Angor.Sdk.Funding.Shared;
4+
using Angor.Shared.Models;
5+
using AngorApp.Core.Factories;
6+
using AngorApp.Model.ProjectsV2.FundProject;
7+
using AngorApp.Model.ProjectsV2.InvestmentProject;
8+
using AngorApp.UI.Sections.MyProjects.ManageFunds.Investment;
9+
using CSharpFunctionalExtensions;
10+
using FluentAssertions;
11+
using Moq;
12+
using Zafiro.UI.Commands;
13+
using Zafiro.UI.Navigation;
14+
using FundManageFunds = AngorApp.UI.Sections.MyProjects.ManageFunds.Fund;
15+
16+
namespace AngorApp.Tests.Core.Factories;
17+
18+
public class ProjectFactoryTests
19+
{
20+
[Fact]
21+
public void Create_returns_investment_project_with_enabled_manage_funds_for_invest_type()
22+
{
23+
var sut = CreateSut();
24+
25+
var result = sut.Create(CreateSeed(ProjectType.Invest));
26+
27+
result.Should().BeAssignableTo<IInvestmentProject>();
28+
result.ManageFunds.CanExecute(null).Should().BeTrue();
29+
}
30+
31+
[Fact]
32+
public void Create_returns_fund_project_with_enabled_manage_funds_for_fund_type()
33+
{
34+
var sut = CreateSut();
35+
36+
var result = sut.Create(CreateSeed(ProjectType.Fund));
37+
38+
result.Should().BeAssignableTo<IFundProject>();
39+
result.ManageFunds.CanExecute(null).Should().BeTrue();
40+
}
41+
42+
private static IProjectFactory CreateSut()
43+
{
44+
Mock<IProjectAppService> projectAppService = new();
45+
Mock<IProjectInvestCommandFactory> projectInvestCommandFactory = new();
46+
Mock<INavigator> navigator = new();
47+
var manageFundsFactory = new Mock<Func<IInvestmentProject, IManageFundsViewModel>>();
48+
var fundManageFundsFactory = new Mock<Func<IFundProject, FundManageFunds.IManageFundsViewModel>>();
49+
50+
projectInvestCommandFactory
51+
.Setup(x => x.Create(It.IsAny<ProjectId>(), It.IsAny<DateTimeOffset>(), It.IsAny<DateTimeOffset>(), It.IsAny<ProjectType>()))
52+
.Returns(EnhancedCommand.CreateWithResult(Result.Success));
53+
54+
return new ProjectFactory(projectAppService.Object, projectInvestCommandFactory.Object, manageFundsFactory.Object, fundManageFundsFactory.Object, navigator.Object);
55+
}
56+
57+
private static ProjectDto CreateSeed(ProjectType projectType)
58+
{
59+
return new ProjectDto
60+
{
61+
Id = new ProjectId($"project-{projectType}"),
62+
Name = "Project",
63+
ShortDescription = "Desc",
64+
TargetAmount = 200_000_000,
65+
FundingStartDate = DateTime.UtcNow.AddDays(-1),
66+
FundingEndDate = DateTime.UtcNow.AddDays(30),
67+
PenaltyDuration = TimeSpan.FromDays(1),
68+
NostrNpubKeyHex = "npub",
69+
FounderPubKey = "founder",
70+
Stages = [],
71+
ProjectType = projectType
72+
};
73+
}
74+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using Angor.Sdk.Funding.Projects;
2+
using Angor.Sdk.Funding.Projects.Dtos;
3+
using Angor.Sdk.Funding.Shared;
4+
using AngorApp.Model.ProjectsV2.InvestmentProject;
5+
using CSharpFunctionalExtensions;
6+
using FluentAssertions;
7+
using Moq;
8+
using System.Reactive.Linq;
9+
using System.Reactive.Threading.Tasks;
10+
using Zafiro.UI.Commands;
11+
12+
namespace AngorApp.Tests.UI.Sections.MyProjects.ManageFunds;
13+
14+
public class ManageFundsProjectStatisticsTests
15+
{
16+
[Fact]
17+
public async Task InvestmentProject_maps_statistics_from_project_statistics_dto()
18+
{
19+
var dto = new ProjectStatisticsDto
20+
{
21+
TotalInvested = 120_000_000,
22+
AvailableBalance = 80_000_000,
23+
WithdrawableAmount = 50_000_000,
24+
TotalStages = 7
25+
};
26+
27+
Mock<IProjectAppService> projectAppService = new();
28+
projectAppService
29+
.Setup(x => x.GetProjectStatistics(It.IsAny<ProjectId>()))
30+
.ReturnsAsync(Result.Success(dto));
31+
32+
var seed = new ProjectDto
33+
{
34+
Id = new ProjectId("project-1"),
35+
Name = "Project",
36+
ShortDescription = "Desc",
37+
TargetAmount = 200_000_000,
38+
FundingStartDate = DateTime.UtcNow.AddDays(-1),
39+
FundingEndDate = DateTime.UtcNow.AddDays(30),
40+
PenaltyDuration = TimeSpan.FromDays(1),
41+
NostrNpubKeyHex = "npub",
42+
Stages = [],
43+
FounderPubKey = "founder"
44+
};
45+
46+
var invest = new Mock<IEnhancedCommand<Result>>().Object;
47+
var sut = new InvestmentProject(seed, projectAppService.Object, invest);
48+
49+
var totalInvestmentTask = sut.TotalInvestment.FirstAsync().ToTask();
50+
var availableBalanceTask = sut.AvailableBalance.FirstAsync().ToTask();
51+
var withdrawableTask = sut.Withdrawable.FirstAsync().ToTask();
52+
var totalStagesTask = sut.TotalStages.FirstAsync().ToTask();
53+
54+
sut.Refresh.Execute(null);
55+
56+
(await totalInvestmentTask.WaitAsync(TimeSpan.FromSeconds(1))).Sats.Should().Be(dto.TotalInvested);
57+
(await availableBalanceTask.WaitAsync(TimeSpan.FromSeconds(1))).Sats.Should().Be(dto.AvailableBalance);
58+
(await withdrawableTask.WaitAsync(TimeSpan.FromSeconds(1))).Sats.Should().Be(dto.WithdrawableAmount);
59+
(await totalStagesTask.WaitAsync(TimeSpan.FromSeconds(1))).Should().Be(dto.TotalStages);
60+
}
61+
}

src/Angor/Avalonia/AngorApp/Composition/Registrations/ViewModels/ViewModels.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using ProjectId = Angor.Sdk.Funding.Shared.ProjectId;
21
using AngorApp.Core.Factories;
32
using AngorApp.UI.Sections.MyProjects;
43
using AngorApp.UI.Sections.Settings;
@@ -13,8 +12,11 @@
1312
using AngorApp.UI.Sections.Funds.Accounts;
1413
using AngorApp.UI.Sections.Funds.Empty;
1514
using AngorApp.UI.Sections.FindProjects.Details;
16-
using AngorApp.UI.Sections.MyProjects.ManageFunds;
1715
using AngorApp.Model.ProjectsV2;
16+
using AngorApp.Model.ProjectsV2.FundProject;
17+
using AngorApp.Model.ProjectsV2.InvestmentProject;
18+
using AngorApp.UI.Sections.MyProjects.ManageFunds.Investment;
19+
using FundManageFunds = AngorApp.UI.Sections.MyProjects.ManageFunds.Fund;
1820

1921
namespace AngorApp.Composition.Registrations.ViewModels;
2022

@@ -24,8 +26,10 @@ public static IServiceCollection AddViewModels(this IServiceCollection services)
2426
{
2527
return services
2628
.AddScoped<IProjectInvestCommandFactory, ProjectInvestCommandFactory>()
29+
.AddScoped<IProjectFactory, ProjectFactory>()
2730
.AddScoped<Func<IProject, IDetailsViewModel>>(provider => project => ActivatorUtilities.CreateInstance<DetailsViewModel>(provider, project))
28-
.AddScoped<Func<ProjectId, IManageFundsViewModel>>(provider => project => ActivatorUtilities.CreateInstance<ManageFundsViewModel>(provider, project))
31+
.AddScoped<Func<IInvestmentProject, IManageFundsViewModel>>(provider => project => ActivatorUtilities.CreateInstance<ManageFundsViewModel>(provider, project))
32+
.AddScoped<Func<IFundProject, FundManageFunds.IManageFundsViewModel>>(provider => project => ActivatorUtilities.CreateInstance<FundManageFunds.ManageFundsViewModel>(provider, project))
2933
.AddTransient<IAccountsViewModel, AccountsViewModel>()
3034
.AddTransient<ISeedBackupFileService, SeedBackupFileService>()
3135
.AddTransient<IAddWalletFlow, AddWalletFlow>()

0 commit comments

Comments
 (0)