Dons PayPal

This commit is contained in:
Paul Schneider 2026-11-02 10:40:29 +00:00
commit e3b4414ab1
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
45 changed files with 7476 additions and 9 deletions

View file

@ -89,6 +89,20 @@ public class BlogReaderTests
{
var vm = new VideoPageViewModel(new Uri(source), "video/mp4");
Assert.Equal(expected, vm.CanOpenExternal);
Assert.Equal(vm.Source, vm.ExternalUri);
}
[Theory]
[InlineData("https://org.example.test/Podcast", true)]
[InlineData("about:blank", false)]
public void Video_external_action_uses_the_web_resource_instead_of_the_playback_stream(string externalUrl, bool expected)
{
var source = new Uri("https://api.example.test/api/v1/podcasts/123/index.m3u8");
var externalUri = new Uri(externalUrl);
var vm = new VideoPageViewModel(source, "application/vnd.apple.mpegurl", externalUri);
Assert.Equal(source, vm.Source);
Assert.Equal(externalUri, vm.ExternalUri);
Assert.Equal(expected, vm.CanOpenExternal);
}
[AvaloniaFact]

View file

@ -0,0 +1,72 @@
using Avalonia.Controls;
using Avalonia.Headless.XUnit;
using PostIt.Services;
using PostIt.ViewModels;
using PostIt.Views;
using Yavsc.Abstract.Workflow;
using Yavsc.Api.Client;
namespace PostIt.Tests;
public sealed class PerformerDonationSettingsTests
{
[Theory]
[InlineData("performer@payments.example", "performer@payments.example")]
[InlineData(" performer@payments.example ", "performer@payments.example")]
[InlineData("", "")]
public void Performer_editor_includes_the_paypal_recipient_in_the_save_payload(string value, string expected)
{
var vm = new PerformerConfigurationPageViewModel { DonationPayPalEmail = value };
Assert.Equal(expected, vm.BuildPerformerProfileSettings().DonationPayPalEmail);
}
[AvaloniaFact]
public async Task Performer_editor_loads_displays_edits_and_saves_the_private_recipient()
{
var tokenPath = Path.Combine(Path.GetTempPath(), "postit-donation-" + Guid.NewGuid().ToString("N") + ".json");
try
{
await using var api = new PerformerApi(tokenPath);
var vm = new PerformerConfigurationPageViewModel(api, new ActivityApiClient(api, api.Settings.ApiUrl));
await vm.RefreshAsync();
Assert.Equal("initial@payments.example", vm.DonationPayPalEmail);
var page = new PerformerConfigurationPage { DataContext = vm };
var input = page.FindControl<TextBox>("DonationPayPalEmailInput")!;
Assert.Equal("initial@payments.example", input.Text);
input.Text = "updated@payments.example";
Assert.Equal("updated@payments.example", vm.DonationPayPalEmail);
await vm.SaveAsync();
Assert.Equal("updated@payments.example", api.Saved?.DonationPayPalEmail);
input.Text = "";
await vm.SaveAsync();
Assert.Equal("", api.Saved?.DonationPayPalEmail);
}
finally
{
if (File.Exists(tokenPath)) File.Delete(tokenPath);
}
}
private sealed class PerformerApi(string tokenPath) : YavscApiClient(new Settings
{
Authentication = new AuthenticationSettings
{
Authority = "https://org.example.test", ClientId = "postit", Scopes = ["openid", "api"]
}
}, new TokenStore(tokenPath))
{
public PerformerProfileSettings? Saved { get; private set; }
public override Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
{
if (typeof(T) == typeof(List<ActivityInfo>))
return Task.FromResult((T)(object)new List<ActivityInfo>());
Assert.EndsWith("/account/performer-profile", path);
if (method == HttpMethod.Put) Saved = Assert.IsType<PerformerProfileSettings>(body);
return Task.FromResult((T)(object)new PerformerProfileSettings
{
PerformerId = "author", DonationPayPalEmail = "initial@payments.example"
});
}
}
}

View file

@ -5,14 +5,15 @@ using Yavsc.Api.Client;
namespace PostIt.ViewModels;
public partial class VideoPageViewModel(Uri source, string mimeType) : ViewModelBase
public partial class VideoPageViewModel(Uri source, string mimeType, Uri? externalUri = null) : ViewModelBase
{
public Uri Source { get; } = source;
public Uri ExternalUri { get; } = externalUri ?? source;
public string MimeType { get; } = mimeType;
public PodcastApiClient? PodcastClient { get; init; }
public PodcastResponse? Podcast { get; set; }
public bool CanOpenExternal => CanLaunchExternally(Source);
public bool CanOpenExternal => CanLaunchExternally(ExternalUri);
public string Title => Podcast?.Title ?? Uri.UnescapeDataString(System.IO.Path.GetFileName(Source.AbsolutePath));
public override bool CanNavigateNext { get => false; protected set { } }
public override bool CanNavigatePrevious { get => true; protected set { } }

View file

@ -50,6 +50,9 @@ public partial class PerformerConfigurationPageViewModel : ViewModelBase
[ObservableProperty]
public partial string Website { get; set; } = string.Empty;
[ObservableProperty]
public partial string DonationPayPalEmail { get; set; } = string.Empty;
[ObservableProperty]
public partial bool Active { get; set; }
@ -101,6 +104,7 @@ public partial class PerformerConfigurationPageViewModel : ViewModelBase
ExerciseCountryCode = string.IsNullOrWhiteSpace(dto?.ExerciseCountryCode) ? "fr" : dto.ExerciseCountryCode;
SIREN = dto?.SIREN ?? string.Empty;
Website = dto?.WebSite ?? string.Empty;
DonationPayPalEmail = dto?.DonationPayPalEmail ?? string.Empty;
Active = dto?.Active ?? false;
AcceptNotifications = dto?.AcceptNotifications ?? false;
AcceptPublicContact = dto?.AcceptPublicContact ?? false;
@ -142,6 +146,7 @@ public partial class PerformerConfigurationPageViewModel : ViewModelBase
ExerciseCountryCode = string.IsNullOrWhiteSpace(ExerciseCountryCode) ? "fr" : ExerciseCountryCode,
SIREN = SIREN,
WebSite = Website,
DonationPayPalEmail = DonationPayPalEmail.Trim(),
Active = Active,
AcceptNotifications = AcceptNotifications,
AcceptPublicContact = AcceptPublicContact,

View file

@ -263,7 +263,8 @@ public partial class PodcastsPageViewModel : ViewModelBase
var podcast = SelectedPodcast ?? throw new InvalidOperationException("Selectionnez un podcast.");
var app = Application.Current as App ?? throw new InvalidOperationException("Application indisponible.");
var manifestUrl = new Uri(new Uri(_api.Settings.ApiUrl.TrimEnd('/') + "/"), $"podcasts/{podcast.Id}/index.m3u8");
await app.PushPageAsync(new VideoPageViewModel(manifestUrl, "application/vnd.apple.mpegurl")
var catalogueUrl = new Uri(new Uri(_api.Settings.Authentication.Authority.TrimEnd('/') + "/"), "Podcast");
await app.PushPageAsync(new VideoPageViewModel(manifestUrl, "application/vnd.apple.mpegurl", catalogueUrl)
{
PodcastClient = _client, Podcast = podcast
});

View file

@ -27,6 +27,16 @@
<TextBox Text="{Binding Website, Mode=TwoWay}"
PlaceholderText="Site web"/>
<TextBlock Text="Dons PayPal" FontWeight="SemiBold"/>
<TextBlock Text="Adresse e-mail PayPal du bénéficiaire"/>
<TextBox x:Name="DonationPayPalEmailInput"
Text="{Binding DonationPayPalEmail, Mode=TwoWay}"
PlaceholderText="Adresse e-mail de votre compte PayPal"
MaxLength="127"
IsEnabled="{Binding !IsBusy}"/>
<TextBlock Text="Les dons sont versés directement sur ce compte PayPal. Vérifiez l’adresse. Laissez vide pour désactiver les nouveaux dons, puis enregistrez. Le profil doit être actif pour proposer les dons."
TextWrapping="Wrap"/>
<CheckBox Content="Disponible / actif"
IsChecked="{Binding Active, Mode=TwoWay}"/>
<CheckBox Content="Accepter les notifications"

View file

@ -201,13 +201,13 @@ public partial class VideoPage : ContentPage
{
if (!vm.CanOpenExternal)
{
PlaybackStatus.Text = "Ce flux interne ne peut pas être ouvert dans le navigateur.";
PlaybackStatus.Text = "Cette ressource ne peut pas être ouverte dans le navigateur.";
return;
}
var topLevel = TopLevel.GetTopLevel(this)
?? throw new InvalidOperationException("La fenêtre de lecture est indisponible.");
if (!await topLevel.Launcher.LaunchUriAsync(vm.Source))
if (!await topLevel.Launcher.LaunchUriAsync(vm.ExternalUri))
PlaybackStatus.Text = "Impossible d'ouvrir le navigateur.";
}
catch (Exception ex)