Some checks failed
Build Linux / build (push) Failing after 24s
Features Analytics Page: Full-featured analytics dashboard with KPI cards, cash flow trend chart, net worth progression, spending patterns by day-of-week, top spending categories, and income sources breakdown. Includes PDF export via QuestPDF for selected periods. Implemented on both desktop and mobile (simplified). Auth Error Handling: Map Supabase GotrueException errors to AuthError enum with user-friendly messages for login and signup. Display errors in sign-in and sign-up panels. Dynamic Transaction/Account Counts: Replace hardcoded "46 transactions" and "4 accounts" text with FilteredTransactionCount and ActiveAccountCount properties bound to actual data. Fixes Budget Period Navigation: Fix year-aware date comparison in CanGoToPreviousPeriod and CanGoToNextPeriod. Previously only compared months, preventing navigation before January of current year. Changes AnalyticsViewModel: Period selector, KPI calculations, chart data builders (cash flow, net worth, day-of-week, top categories, income sources), PDF export PdfExportService: QuestPDF report generation with print-optimized styling AuthViewModel: Error display with GotrueException mapping BudgetViewModel: Year-aware period navigation TransactionsViewModel: FilteredTransactionCount property AccountsViewModel: ActiveAccountCount property MainViewModel: Analytics navigation and AnalyticsViewModel integration Views: Analytics button wired, error messages displayed, count bindings updated
199 lines
7.8 KiB
C#
199 lines
7.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using Avalonia;
|
|
using Avalonia.Controls.ApplicationLifetimes;
|
|
using Clario.Enums;
|
|
using Clario.Models;
|
|
using Clario.Services;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using Supabase.Gotrue;
|
|
using Supabase.Gotrue.Exceptions;
|
|
|
|
namespace Clario.ViewModels;
|
|
|
|
public partial class AuthViewModel : ViewModelBase
|
|
{
|
|
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand))]
|
|
private string _firstName;
|
|
|
|
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand))]
|
|
private string _lastName;
|
|
|
|
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
|
|
private string _email;
|
|
|
|
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
|
|
private string _password;
|
|
|
|
[ObservableProperty] [NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand))]
|
|
private string _confirmPassword;
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(isSignin), nameof(isCreateAccount))]
|
|
[NotifyCanExecuteChangedFor(nameof(ConfirmCreateAccountCommand), nameof(ConfirmLoginCommand))]
|
|
private string _operation = "login";
|
|
|
|
[ObservableProperty] [NotifyPropertyChangedFor(nameof(HasError))]
|
|
private string? _errorMessage;
|
|
|
|
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
|
|
|
|
public AuthViewModel()
|
|
{
|
|
DebugLogger.Log("auth vm loaded");
|
|
setDefaults();
|
|
}
|
|
|
|
[Conditional("DEBUG")]
|
|
private void setDefaults()
|
|
{
|
|
if (!File.Exists("devsettings.json")) return;
|
|
|
|
var json = File.ReadAllText("devsettings.json");
|
|
var config = JsonSerializer.Deserialize<Wrapper>(json);
|
|
if (config?.TestDefaults is null) return;
|
|
|
|
FirstName = config.TestDefaults.FirstName;
|
|
LastName = config.TestDefaults.LastName;
|
|
Email = config.TestDefaults.Email;
|
|
Password = config.TestDefaults.Password;
|
|
ConfirmPassword = config.TestDefaults.Password;
|
|
|
|
ThemeService.SwitchToTheme("system");
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void SetOperation(string operation)
|
|
{
|
|
Operation = operation;
|
|
ErrorMessage = null;
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(canSignin))]
|
|
private async Task ConfirmLogin()
|
|
{
|
|
ErrorMessage = null;
|
|
try
|
|
{
|
|
await SupabaseService.Client.Auth.SignIn(_email, _password);
|
|
|
|
var user = SupabaseService.Client.Auth.CurrentUser;
|
|
|
|
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
{
|
|
desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
|
}
|
|
else if (Application.Current.ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
|
|
{
|
|
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
|
}
|
|
}
|
|
catch (GotrueException e)
|
|
{
|
|
DebugLogger.Log(e);
|
|
ErrorMessage = GetLoginErrorMessage(e.Reason);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
DebugLogger.Log(e);
|
|
ErrorMessage = GetErrorMessage(AuthError.Unknown);
|
|
}
|
|
}
|
|
|
|
[RelayCommand(CanExecute = nameof(canCreateAccount))]
|
|
private async Task ConfirmCreateAccount()
|
|
{
|
|
ErrorMessage = null;
|
|
try
|
|
{
|
|
var session = await SupabaseService.Client.Auth.SignUp(
|
|
_email,
|
|
_password,
|
|
new SignUpOptions
|
|
{
|
|
Data = new Dictionary<string, object>
|
|
{
|
|
{
|
|
"display_name", $"{FirstName.Trim()} {LastName.Trim()}"
|
|
}
|
|
}
|
|
});
|
|
if (session is null) return;
|
|
await SupabaseService.Client.Auth.SetSession(session.AccessToken, session.RefreshToken);
|
|
var user = session.User;
|
|
|
|
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
{
|
|
desktop.MainWindow!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
|
}
|
|
else if (Application.Current.ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
|
|
{
|
|
singleViewPlatform.MainView!.DataContext = user is not null ? new MainViewModel() : new AuthViewModel();
|
|
}
|
|
}
|
|
catch (GotrueException e)
|
|
{
|
|
DebugLogger.Log(e);
|
|
ErrorMessage = GetSignupErrorMessage(e.Reason);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
DebugLogger.Log(e);
|
|
ErrorMessage = GetErrorMessage(AuthError.Unknown);
|
|
}
|
|
}
|
|
|
|
private static string GetLoginErrorMessage(FailureHint.Reason reason) => reason switch
|
|
{
|
|
FailureHint.Reason.UserBadLogin => GetErrorMessage(AuthError.InvalidCredentials),
|
|
FailureHint.Reason.UserBadPassword => GetErrorMessage(AuthError.InvalidCredentials),
|
|
FailureHint.Reason.UserEmailNotConfirmed => GetErrorMessage(AuthError.EmailNotConfirmed),
|
|
FailureHint.Reason.UserTooManyRequests => GetErrorMessage(AuthError.RateLimited),
|
|
FailureHint.Reason.UserBadEmailAddress => GetErrorMessage(AuthError.InvalidEmail),
|
|
FailureHint.Reason.Offline => GetErrorMessage(AuthError.Unknown),
|
|
_ => GetErrorMessage(AuthError.Unknown),
|
|
};
|
|
|
|
private static string GetSignupErrorMessage(FailureHint.Reason reason) => reason switch
|
|
{
|
|
FailureHint.Reason.UserAlreadyRegistered => GetErrorMessage(AuthError.EmailAlreadyExists),
|
|
FailureHint.Reason.UserBadPassword => GetErrorMessage(AuthError.WeakPassword),
|
|
FailureHint.Reason.UserBadEmailAddress => GetErrorMessage(AuthError.InvalidEmail),
|
|
FailureHint.Reason.UserTooManyRequests => GetErrorMessage(AuthError.RateLimited),
|
|
FailureHint.Reason.Offline => GetErrorMessage(AuthError.Unknown),
|
|
_ => GetErrorMessage(AuthError.Unknown),
|
|
};
|
|
|
|
private static string GetErrorMessage(AuthError error) => error switch
|
|
{
|
|
AuthError.InvalidCredentials => "Invalid email or password.",
|
|
AuthError.EmailAlreadyExists => "An account with this email already exists.",
|
|
AuthError.EmailNotConfirmed => "Please confirm your email before signing in.",
|
|
AuthError.WeakPassword => "Password must be at least 6 characters.",
|
|
AuthError.InvalidEmail => "Please enter a valid email address.",
|
|
AuthError.SignupDisabled => "Sign-ups are currently disabled.",
|
|
AuthError.RateLimited => "Too many attempts. Please wait and try again.",
|
|
AuthError.SessionExpired => "Your session has expired. Please sign in again.",
|
|
_ => "Something went wrong. Please try again.",
|
|
};
|
|
|
|
|
|
public bool isSignin => Operation == "login";
|
|
public bool isCreateAccount => Operation == "signup";
|
|
|
|
public bool canSignin => isSignin && !string.IsNullOrWhiteSpace(_email) && !string.IsNullOrWhiteSpace(_password);
|
|
|
|
public bool canCreateAccount => isCreateAccount && !string.IsNullOrWhiteSpace(_firstName) && !string.IsNullOrWhiteSpace(_lastName) &&
|
|
!string.IsNullOrWhiteSpace(_email) &&
|
|
!string.IsNullOrWhiteSpace(_password) && _password == _confirmPassword;
|
|
}
|
|
|
|
class Wrapper
|
|
{
|
|
public TestDefaults TestDefaults { get; set; }
|
|
} |