Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject;
using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.Model;
using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject.Stages;
using AngorApp.UI.Flows.CreateProject.Wizard.FundProject;
using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model;
using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Payouts;
using AngorApp.UI.Shared.Controls.Common.Success;
using Serilog;
using Zafiro.Avalonia.Controls.Wizards.Slim;
using Zafiro.UI.Navigation;
using Zafiro.UI.Wizards.Slim;
using Zafiro.UI.Wizards.Slim.Builder;
using ProjectType = AngorApp.UI.Flows.CreateProject.Wizard.ProjectType;
using Avalonia.Threading;

namespace AngorApp.UI.Flows.CreateProject
{
Expand Down Expand Up @@ -41,7 +44,7 @@ private async Task<Result<Maybe<string>>> Create(WalletId walletId, ProjectSeedD
.StartWith(() => new WelcomeViewModel()).NextCommand(model => model.Start)
.Then(_ => new ProjectTypeViewModel())
.NextCommand<Unit, ProjectTypeViewModel, string>(vm => CreateProjectOftype(
vm.ProjectType,
vm,
walletId,
seed))
.Then(txId => new SuccessViewModel($"Project {txId} created successfully!"), "Success").Next((_, s) => s, "Finish").Always()
Expand All @@ -51,20 +54,23 @@ private async Task<Result<Maybe<string>>> Create(WalletId walletId, ProjectSeedD
}

private IEnhancedCommand<Result<string>> CreateProjectOftype(
ProjectType projectType,
ProjectTypeViewModel vm,
WalletId walletId,
ProjectSeedDto seed
)
{
// TODO: We support only investment projects for now. That's why we ignore projectType.
return CreateInvestmentProject(walletId, seed);
}
var canExecute = vm.WhenAnyValue(x => x.ProjectType).Select(x => x != null);

private IEnhancedCommand<Result<string>> CreateInvestmentProject(WalletId walletId, ProjectSeedDto seed)
{
return ReactiveCommand
.CreateFromTask(() => CreateInvestmentProjectWizard(walletId, seed).Navigate(navigator).ToResult("Wizard was cancelled by user"))
.Enhance();
return ReactiveCommand.CreateFromTask(async () =>
{
var projectType = vm.ProjectType;
return await Dispatcher.UIThread.InvokeAsync(() => projectType.Name switch
{
"Investment" => CreateInvestmentProjectWizard(walletId, seed).Navigate(navigator).ToResult("Wizard was cancelled by user"),
"Fund" => CreateFundProjectWizard(walletId, seed).Navigate(navigator).ToResult("Wizard was cancelled by user"),
_ => throw new NotImplementedException($"Project type {projectType.Name} not implemented")
});
}, canExecute).Enhance();
}

private SlimWizard<string> CreateInvestmentProjectWizard(WalletId walletId, ProjectSeedDto seed)
Expand Down Expand Up @@ -93,6 +99,31 @@ private SlimWizard<string> CreateInvestmentProjectWizard(WalletId walletId, Proj
return wizard;
}

private SlimWizard<string> CreateFundProjectWizard(WalletId walletId, ProjectSeedDto seed)
{
var newProject = new FundProjectConfig();

SlimWizard<string> wizard = WizardBuilder
.StartWith(() => new ProjectProfileViewModel(newProject)).NextUnit().WhenValid()
.Then(_ => new ProjectImagesViewModel(newProject)).NextUnit().Always()
.Then(_ => new GoalViewModel(newProject)).NextUnit().WhenValid()
.Then(_ => new FundPayoutsViewModel(newProject)).NextUnit().WhenValid()
.Then(_ => new FundReviewAndDeployViewModel(
newProject,
new ProjectDeploymentOrchestrator(
projectAppService,
founderAppService,
uiServices,
logger),
walletId,
seed,
uiServices))
.NextCommand(review => review.DeployCommand)
.WithCommitFinalStep();

return wizard;
}

private async Task<Result<ProjectSeedDto>> GetProjectSeed(WalletId walletId)
{
var result = await founderAppService.CreateProjectKeys(new CreateProjectKeys.CreateProjectKeysRequest(walletId));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Reactive.Disposables;
using Angor.Sdk.Common;
using Angor.Sdk.Funding.Founder.Dtos;
using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Mappers;
using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model;
using AngorApp.UI.Flows.CreateProject.Wizard.InvestmentProject;
using System.Collections.ObjectModel;
using DynamicData;
using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Helpers;
using System.Linq;

namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject
{
public class FundReviewAndDeployViewModel : IFundReviewAndDeployViewModel, IDisposable
{
private readonly ProjectDeploymentOrchestrator orchestrator;
private readonly WalletId walletId;
private readonly ProjectSeedDto projectSeed;
private readonly CompositeDisposable disposables = new();

public IFundProjectConfig NewProject { get; }
public IEnhancedCommand<Result<string>> DeployCommand { get; }

public FundReviewAndDeployViewModel(
IFundProjectConfig newProject,
ProjectDeploymentOrchestrator orchestrator,
WalletId walletId,
ProjectSeedDto projectSeed,
UIServices uiServices)
{
NewProject = newProject;
this.orchestrator = orchestrator;
this.walletId = walletId;
this.projectSeed = projectSeed;

DeployCommand = ReactiveCommand.CreateFromTask(Deploy).Enhance("Deploy").DisposeWith(disposables);
DeployCommand.HandleErrorsWith(uiServices.NotificationService, "Failed to deploy project").DisposeWith(disposables);


var payoutsSource = new SourceList<IPayoutConfig>();
payoutsSource.Connect().Bind(out var payouts).Subscribe().DisposeWith(disposables);
Payouts = payouts;

if (newProject.PayoutFrequency != null)
{
var maxInstallments = newProject.SelectedInstallments.SelectedItems.DefaultIfEmpty(0).Max();
if (maxInstallments > 0)
{
var generated = PayoutGenerator.Generate(
newProject.PayoutFrequency.Value,
maxInstallments,
DateTime.Now,
newProject.MonthlyPayoutDate,
newProject.WeeklyPayoutDay
);
payoutsSource.AddRange(generated);
}
}
}

public ReadOnlyObservableCollection<IPayoutConfig> Payouts { get; }

private async Task<Result<string>> Deploy()
{
var dto = NewProject.ToDto();
return await orchestrator.Deploy(walletId, dto, projectSeed);
}

public IObservable<string> Title => Observable.Return("Review & Deploy");

public void Dispose()
{
disposables.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Collections.ObjectModel;
using Angor.Sdk.Common;
using AngorApp.UI.Flows.CreateProject.Wizard.FundProject.Model;

namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject;


public class FundReviewAndDeployViewModelSample : IFundReviewAndDeployViewModel
{
public FundReviewAndDeployViewModelSample()
{
NewProject = new FundProjectConfigSample();


var samplePayouts = new ObservableCollection<IPayoutConfig>
{
new PayoutConfigSample { Percent = 0.25m, PayoutDate = DateTime.Now.AddMonths(1) },
new PayoutConfigSample { Percent = 0.25m, PayoutDate = DateTime.Now.AddMonths(2) },
new PayoutConfigSample { Percent = 0.25m, PayoutDate = DateTime.Now.AddMonths(3) },
new PayoutConfigSample { Percent = 0.25m, PayoutDate = DateTime.Now.AddMonths(4) }
};

Payouts = new ReadOnlyObservableCollection<IPayoutConfig>(samplePayouts);
}

public IEnhancedCommand<Result<string>> DeployCommand { get; } = null!;

public IFundProjectConfig NewProject { get; }

public ReadOnlyObservableCollection<IPayoutConfig> Payouts { get; }

public IObservable<string> Title => Observable.Return("Review & Deploy");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:fund="clr-namespace:AngorApp.UI.Flows.CreateProject.Wizard.FundProject"
xmlns:controls="clr-namespace:AngorApp.UI.Shared.Controls"
mc:Ignorable="d" d:DesignWidth="800"
x:Class="AngorApp.UI.Flows.CreateProject.Wizard.FundProject.GoalView" x:DataType="fund:IGoalViewModel">

<Design.DataContext>
<fund:GoalViewModelSample />
</Design.DataContext>

<UserControl.Styles>
<Style Selector="TextBlock">
<Setter Property="TextWrapping" Value="Wrap" />
</Style>

<Style Selector="TextBlock.FormLabel">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="{DynamicResource TextStrong}" />
</Style>

<Style Selector="TextBlock.AccentHint">
<Setter Property="FontSize" Value="{StaticResource FontSizeSmall}" />
<Setter Property="Foreground" Value="{DynamicResource BitcoinAccent}" />
</Style>

<Style Selector="NumericUpDown.AmountInput">
<Setter Property="FontSize" Value="{StaticResource FontSizeBig}" />
<Setter Property="FontWeight" Value="Bold" />
</Style>

<Style Selector="TextBlock.AmountUnitInline">
<Setter Property="FontSize" Value="{StaticResource FontSizeNormal}" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Foreground" Value="{DynamicResource TextMuted}" />
</Style>

<Style Selector="Border.HelpBadge">
<Setter Property="Background" Value="{DynamicResource BitcoinAccent}" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="8 3" />
<Setter Property="TextElement.Foreground" Value="White" />
<Setter Property="TextElement.FontSize" Value="{StaticResource FontSizeSmall}" />
<Setter Property="TextElement.FontWeight" Value="Bold" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</UserControl.Styles>

<controls:ScrollableView MaxContentWidth="766">
<StackPanel VerticalAlignment="Center" Spacing="{StaticResource BigGap}">
<TextBlock Classes="Subtitle">Configure your funding goal and project nature.</TextBlock>


<HeaderedContainer Classes="WizardHighlight">
<HeaderedContainer.Header>
<EdgePanel Content="Goal">
<EdgePanel.StartContent>
<PathIcon Width="20"
Height="20"
Foreground="{DynamicResource BitcoinAccent}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="{StaticResource BitcoinSymbol}" />
</EdgePanel.StartContent>
</EdgePanel>
</HeaderedContainer.Header>
<StackPanel Classes="BigGap">
<StackPanel Spacing="10">
<HeaderedContainer HeaderPadding="0" ContentPadding="0 12 0 0">
<HeaderedContainer.Header>
<TextBlock Classes="FormLabel">Goal (BTC) *</TextBlock>
</HeaderedContainer.Header>

<NumericUpDown Classes="AmountInput"
Value="{Binding FundProject.GoalAmount, Mode=TwoWay, Converter={x:Static controls:AngorConverters.BtcToAmountUI}}"
FormatString="0.########"
Minimum="0"
Increment="0.01"
x:Name="AmountBox"
Watermark="0.00">
<NumericUpDown.InnerRightContent>
<TextBlock Classes="AmountUnitInline"
VerticalAlignment="Center"
Margin="0 0 12 0">
BTC
</TextBlock>
</NumericUpDown.InnerRightContent>
</NumericUpDown>
</HeaderedContainer>

<TextBlock Classes="AccentHint">The amount you want to get funded</TextBlock>
</StackPanel>

<ListBox ItemsSource="{Binding AmountPresets}"
HorizontalAlignment="Right"
Background="Transparent"
SelectedValueBinding="{Binding Sats}"
SelectedValue="{Binding SelectedPresetSats, Mode=TwoWay}"
ItemContainerTheme="{StaticResource ButtonizedListBoxItem}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<BalancedWrapGrid />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel HorizontalAlignment="Center" Classes="TinyGap">
<TextBlock Classes="Center Bold" Text="{Binding Btc}" />
<TextBlock Classes="Center Tiny" Text="BTC" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</HeaderedContainer>

<HeaderedContainer Classes="BlueThreshold" HeaderPadding="24 24 24 0" ContentPadding="12 12 12 12">
<HeaderedContainer.Header>
<EdgePanel Content="Threshold for Approval">
<EdgePanel.StartContent>
<PathIcon Foreground="#3b82f6" Data="{StaticResource Shield}"></PathIcon>
</EdgePanel.StartContent>
</EdgePanel>
</HeaderedContainer.Header>
<HeaderedContainer Header="Threshold" Foreground="Black">
<StackPanel Classes="SmallGap">
<NumericUpDown Watermark="0.001" Classes="AmountInput"
FontSize="{StaticResource FontSizeBig}"
FontWeight="Bold"
Value="{Binding ThresholdBtc, Mode=TwoWay}"
FormatString="0.########"
Minimum="0"
Increment="0.001">
<NumericUpDown.InnerRightContent>
<TextBlock Classes="AmountUnitInline"
VerticalAlignment="Center"
Margin="0 0 12 0">
BTC
</TextBlock>
</NumericUpDown.InnerRightContent>
</NumericUpDown>
<TextBlock Foreground="#3b82f6" Classes="Weight-Light Size-XS">
<TextBlock.Inlines>
<Run>Supporters who contribute over</Run>
<Run Text="{Binding FundProject.Threshold.DecimalString}" />
<Run>will require approval by you</Run>
</TextBlock.Inlines>
</TextBlock>
</StackPanel>
</HeaderedContainer>
</HeaderedContainer>
</StackPanel>
</controls:ScrollableView>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Avalonia.Controls;

namespace AngorApp.UI.Flows.CreateProject.Wizard.FundProject
{
public partial class GoalView : UserControl
{
public GoalView()
{
InitializeComponent();
}
}
}
Loading
Loading